Skip to content

Commit

Permalink
feat: Added host preferences and calendars
Browse files Browse the repository at this point in the history
Hosts can now be created, along with their time preferences and calendars
  • Loading branch information
drewfugate authored Mar 8, 2024
1 parent ac51c2a commit b36a964
Show file tree
Hide file tree
Showing 18 changed files with 605 additions and 66 deletions.
4 changes: 2 additions & 2 deletions application/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/joho/godotenv"
"github.com/rise8-us/neverl8/cli"
"github.com/rise8-us/neverl8/repository"
"github.com/rise8-us/neverl8/service"
meetingSvc "github.com/rise8-us/neverl8/service/meeting"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -95,7 +95,7 @@ func (a *App) Start(ctx context.Context) error {
<-serverStarted
// Initialize repository, service, and CLI
meetingRepo := repository.NewMeetingRepository(db)
meetingService := service.NewMeetingService(meetingRepo)
meetingService := meetingSvc.NewMeetingService(meetingRepo)
cliInstance := cli.NewCLI(meetingService)

// Create a meeting
Expand Down
10 changes: 5 additions & 5 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import (
"time"

"github.com/rise8-us/neverl8/model"
"github.com/rise8-us/neverl8/service"
meetingSvc "github.com/rise8-us/neverl8/service/meeting"
)

type CLI struct {
meetingService *service.MeetingService
meetingService *meetingSvc.MeetingService
}

func NewCLI(meetingService *service.MeetingService) *CLI {
func NewCLI(meetingService *meetingSvc.MeetingService) *CLI {
return &CLI{meetingService}
}

func (c *CLI) CreateMeetingFromCLI() {
// Create new Hosts
hosts := []model.Hosts{
hosts := []model.Host{
{HostName: "Host 1"},
{HostName: "Host 2"},
}
Expand All @@ -38,7 +38,7 @@ func (c *CLI) CreateMeetingFromCLI() {
}

// Create Meeting and Hosts
createdMeeting, err := c.meetingService.CreateMeeting(newMeeting, &hosts)
createdMeeting, err := c.meetingService.CreateMeeting(newMeeting, hosts)
if err != nil {
log.Fatalf("Failed to create meeting and hosts: %v", err)
}
Expand Down
6 changes: 3 additions & 3 deletions controller/meeting_controller.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package meetingcontroller

import (
"github.com/rise8-us/neverl8/service"
meetingSvc "github.com/rise8-us/neverl8/service/meeting"
)

type meetingController struct {
meetingService *service.MeetingService
meetingService *meetingSvc.MeetingService
}

func NewMeetingController(meetingService *service.MeetingService) *meetingController {
func NewMeetingController(meetingService *meetingSvc.MeetingService) *meetingController {
return &meetingController{meetingService}
}

Expand Down
4 changes: 3 additions & 1 deletion db/migrations/000001_create_meetings_tables.down.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
DROP TABLE IF EXISTS candidates;
DROP TABLE IF EXISTS host_meetings;
DROP TABLE IF EXISTS time_preferences;
DROP TABLE IF EXISTS hosts;
DROP TABLE IF EXISTS meetings;
DROP TABLE IF EXISTS calendar_hosts;
DROP TABLE IF EXISTS calendars;
DROP TABLE IF EXISTS hosts;
23 changes: 21 additions & 2 deletions db/migrations/000001_create_meetings_tables.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ CREATE TABLE IF NOT EXISTS host_meetings (
CREATE TABLE IF NOT EXISTS time_preferences (
id SERIAL PRIMARY KEY,
host_id INTEGER REFERENCES hosts(id),
start_window TIMESTAMP,
end_window TIMESTAMP -- TODO: consider using a duration instead of an end time
start_window TEXT DEFAULT '00:00',
end_window TEXT DEFAULT '00:00' -- TODO: consider using a duration instead of an end time
);

CREATE TABLE IF NOT EXISTS calendars (
id SERIAL PRIMARY KEY,
google_calendar_id TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS host_calendars (
calendar_id INTEGER NOT NULL,
host_id INTEGER NOT NULL,
PRIMARY KEY (calendar_id, host_id),
CONSTRAINT fk_calendar
FOREIGN KEY (calendar_id)
REFERENCES calendars(id)
ON DELETE CASCADE,
CONSTRAINT fk_host
FOREIGN KEY (host_id)
REFERENCES hosts(id)
ON DELETE CASCADE
);
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/go-chi/chi v1.5.5
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.28.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0
gorm.io/driver/postgres v1.5.6
Expand Down Expand Up @@ -59,6 +59,7 @@ require (
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
Expand Down
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,15 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8=
github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU=
github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0 h1:ff0s4JdYIdNAVSi/SrpN2Pdt1f+IjIw3AKjbHau8Un4=
Expand Down
43 changes: 28 additions & 15 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ type Meetings struct {
Title string `json:"title" gorm:"type:varchar(255); not null"`
Description string `json:"description" gorm:"type:varchar(255); default: no description provided"`
HasBotGuest bool `json:"has_bot_guest" gorm:"type:bool; default: false"`
StartTime time.Time `json:"start_time" gorm:"type: timestamp with time zone; not null"`
EndTime time.Time `json:"end_time" gorm:"type: timestamp with time zone; not null"`
CreatedAt time.Time `json:"created_at" gorm:"type: timestamp with time zone; not null; default: current_timestamp with time zone"`
Hosts []*Hosts `gorm:"many2many:host_meetings;joinForeignKey:meeting_id;inverseJoinForeignKey:host_id"`
StartTime time.Time `json:"start_time" gorm:"type: timestamp; not null"`
EndTime time.Time `json:"end_time" gorm:"type: timestamp; not null"`
CreatedAt time.Time `json:"created_at" gorm:"type: timestamp; default: current_timestamp"`
Hosts []Host `gorm:"many2many:host_meetings;joinForeignKey:meeting_id;inverseJoinForeignKey:host_id"`
}

type Hosts struct {
ID uint `json:"id" gorm:"primaryKey"`
HostName string `json:"host_name" gorm:"type:varchar(255); not null"`
Candidates []Candidates `json:"candidates_id" gorm:"foreignKey:host_id"`
Meetings []*Meetings `gorm:"many2many:host_meetings;joinForeignKey:host_id;inverseJoinForeignKey:meeting_id"`
TimePreferences []TimePreferences `json:"time_preferences" gorm:"foreignKey:host_id"` // One to many relationship with time preferences
type Host struct {
ID uint `json:"id" gorm:"primaryKey"`
HostName string `json:"host_name" gorm:"type:varchar(255); not null"`
Candidates []Candidates `json:"candidates_id" gorm:"foreignKey:host_id"`
Meetings []Meetings `gorm:"many2many:host_meetings;joinForeignKey:host_id;inverseJoinForeignKey:meeting_id"`
TimePreferences []TimePreference `json:"time_preferences" gorm:"foreignKey:host_id"` // One to many relationship with time preference
Calendars []Calendar `gorm:"many2many:host_calendars;joinForeignKey:calendar_id;inverseJoinForeignKey:host_id"`
}

type Candidates struct {
Expand All @@ -36,16 +37,28 @@ type Candidates struct {
InterviewStatus string `json:"interview_status" gorm:"type:varchar(255); default: unknown interview status"`
}

type Calendar struct {
ID uint `json:"id" gorm:"primaryKey"`
GoogleCalendarID string `json:"google_calendar_id" gorm:"not null"`
Hosts []Host `gorm:"many2many:host_calendars;joinForeignKey:host_id;inverseJoinForeignKey:calendar_id"`
}

// Referential table connecting hosts to meetings. Hosts can have several meetings scheduled, and meetings can have several hosts.
type HostMeetings struct {
HostID uint `json:"host_id" gorm:"primaryKey; autoIncrement:false; not null"`
MeetingID uint `json:"meeting_id" gorm:"primaryKey; autoIncrement:false; not null"`
}

// Referential table connecting hosts to time preferences. Hosts can have several time preferences.
type TimePreferences struct {
ID uint `json:"id" gorm:"primaryKey"`
HostID uint `json:"host_id" gorm:"not null"` // Foreign key to hosts
StartWindow time.Time `json:"start_window" gorm:"type: timestamp with time zone; not null"`
EndWindow time.Time `json:"end_window" gorm:"type: timestamp with time zone; not null"`
type TimePreference struct {
ID uint `json:"id" gorm:"primaryKey"`
HostID uint `json:"host_id" gorm:"not null"` // Foreign key to hosts
StartWindow string `json:"start_window" gorm:"type:string; default: 00:00"`
EndWindow string `json:"end_window" gorm:"type:string; default: 00:00"`
}

// Referential table connecting hosts to calendars. Hosts can have several individual calendars, and group calendars can have several hosts.
type HostCalendar struct {
HostID uint `json:"host_id" gorm:"primaryKey; autoIncrement:false; not null"`
CalendarID uint `json:"calendar_id" gorm:"primaryKey; autoIncrement:false; not null"`
}
71 changes: 71 additions & 0 deletions repository/host_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package repository

import (
"github.com/rise8-us/neverl8/model"
"gorm.io/gorm"
)

type HostRepositoryInterface interface {
CreateHost(host *model.Host) (*model.Host, error)
GetHostByID(id uint) (*model.Host, error)
GetAllHosts() ([]model.Host, error)
CreateTimePreference(timePreference *model.TimePreference) (*model.TimePreference, error)
CreateCalendar(calendar *model.Calendar, host *model.Host) (*model.Calendar, error)
}

// HostRepository handles CRUD operations for hosts and their time preferences.
type HostRepository struct {
db *gorm.DB
}

// NewHostRepository creates a new instance of HostRepository.
func NewHostRepository(db *gorm.DB) *HostRepository {
return &HostRepository{db: db}
}

func (r *HostRepository) CreateHost(host *model.Host) (*model.Host, error) {
if err := r.db.Create(host).Error; err != nil {
return nil, err
}

return host, nil
}

func (r *HostRepository) GetHostByID(id uint) (*model.Host, error) {
var host model.Host
if err := r.db.Preload("TimePreferences").Preload("Calendars").Preload("Meetings").First(&host, id).Error; err != nil {
return nil, err
}
return &host, nil
}

func (r *HostRepository) GetAllHosts() ([]model.Host, error) {
var Host []model.Host
if err := r.db.Preload("TimePreferences").Preload("Calendars").Preload("Meetings").Find(&Host).Error; err != nil {
return nil, err
}
return Host, nil
}

// Adds a new TimePreference to the database for a Host.
func (r *HostRepository) CreateTimePreference(timePreference *model.TimePreference) (*model.TimePreference, error) {
if err := r.db.Create(timePreference).Error; err != nil {
return nil, err
}

return timePreference, nil
}

func (r *HostRepository) CreateCalendar(calendar *model.Calendar, host *model.Host) (*model.Calendar, error) {
if err := r.db.Create(calendar).Error; err != nil {
return nil, err
}

// Create the association between Host and Calendar
err := r.db.Model(host).Association("Calendars").Append(&calendar)
if err != nil {
return nil, err
}

return calendar, nil
}
31 changes: 26 additions & 5 deletions repository/meeting_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import (
"gorm.io/gorm"
)

type MeetingRepositoryInterface interface {
CreateMeeting(meeting *model.Meetings, host []model.Host) (*model.Meetings, error)
GetAllMeetings() ([]model.Meetings, error)
}

// MeetingRepository handles CRUD operations for Meetings.
type MeetingRepository struct {
db *gorm.DB
Expand All @@ -15,11 +20,12 @@ func NewMeetingRepository(db *gorm.DB) *MeetingRepository {
return &MeetingRepository{db: db}
}

// Adds a new Meeting to the database, including creating new Hosts if necessary.
func (r *MeetingRepository) CreateMeeting(meeting *model.Meetings, hosts []model.Hosts) (*model.Meetings, error) {
// Adds a new Meeting to the database, including creating new Host if necessary.
func (r *MeetingRepository) CreateMeeting(meeting *model.Meetings, host []model.Host) (*model.Meetings, error) {
// Step 1: Create and save each Host
for i := range hosts {
if err := r.db.Create(&hosts[i]).Error; err != nil {
// TODO: The user should be able to choose from a list of Host/this should be autopopulated. For now, we'll just create the Host.
for i := range host {
if err := r.db.Create(&host[i]).Error; err != nil {
return nil, err
}
}
Expand All @@ -29,14 +35,29 @@ func (r *MeetingRepository) CreateMeeting(meeting *model.Meetings, hosts []model
return nil, err
}

// Step 3: Create the association between Host and meetings
err := r.db.Model(meeting).Association("Hosts").Append(host)
if err != nil {
return nil, err
}

return meeting, nil
}

// Returns all Meetings from the database.
func (r *MeetingRepository) GetAllMeetings() ([]model.Meetings, error) {
var meetings []model.Meetings
if err := r.db.Preload("Hosts").Preload("Hosts.TimePreferences").Find(&meetings).Error; err != nil {
if err := r.db.Preload("Hosts").Find(&meetings).Error; err != nil {
return nil, err
}
return meetings, nil
}

// Returns a Meeting by its ID.
func (r *MeetingRepository) GetMeetingByID(id uint) (*model.Meetings, error) {
var meeting model.Meetings
if err := r.db.Preload("Hosts").First(&meeting, id).Error; err != nil {
return nil, err
}
return &meeting, nil
}
34 changes: 34 additions & 0 deletions service/host/host_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package host

import (
"github.com/rise8-us/neverl8/model"
repository "github.com/rise8-us/neverl8/repository"
)

type HostService struct {
hostRepo repository.HostRepositoryInterface
}

func NewHostService(hostRepo repository.HostRepositoryInterface) *HostService {
return &HostService{hostRepo}
}

func (s *HostService) CreateHost(host *model.Host) (*model.Host, error) {
return s.hostRepo.CreateHost(host)
}

func (s *HostService) GetAllHosts() ([]model.Host, error) {
return s.hostRepo.GetAllHosts()
}

func (s *HostService) GetHostByID(id uint) (*model.Host, error) {
return s.hostRepo.GetHostByID(id)
}

func (s *HostService) CreateTimePreference(timePreference *model.TimePreference) (*model.TimePreference, error) {
return s.hostRepo.CreateTimePreference(timePreference)
}

func (s *HostService) CreateCalendar(calendar *model.Calendar, host *model.Host) (*model.Calendar, error) {
return s.hostRepo.CreateCalendar(calendar, host)
}
Loading

0 comments on commit b36a964

Please sign in to comment.