From 465231517ecaabf30c097247e4057f4fccfe3a50 Mon Sep 17 00:00:00 2001 From: William Perron Date: Mon, 13 Nov 2023 21:00:05 -0500 Subject: [PATCH] Add absences table and basic queries * Add migration scripts * Add queries to add, get and list absences * Add tests for the above * Add function to get the date of the next Monday Also fixes potential bug in the build where the output binary would be written to a file called `./bin` rather than a file named after the source directory in a _directory_ called `./bin`. --- Dockerfile | 2 +- absences.go | 97 +++++++++++++++++++++++ absences_test.go | 41 ++++++++++ cmd/themis-server/main.go | 3 + migrations/20231113_add_absences.down.sql | 1 + migrations/20231113_add_absences.up.sql | 5 ++ time.go | 13 +++ time_test.go | 46 +++++++++++ 8 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 absences_test.go create mode 100644 migrations/20231113_add_absences.down.sql create mode 100644 migrations/20231113_add_absences.up.sql create mode 100644 time.go create mode 100644 time_test.go diff --git a/Dockerfile b/Dockerfile index e5e29d2..1a2490c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM golang:1.19-buster as builder WORKDIR /app COPY . . -RUN go build -buildvcs=false -o ./bin ./cmd/... +RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/... FROM ubuntu:22.04 as litestream WORKDIR /download diff --git a/absences.go b/absences.go index 93c5f9c..9a54ad1 100644 --- a/absences.go +++ b/absences.go @@ -1 +1,98 @@ package themis + +import ( + "context" + "fmt" + "time" +) + +func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string) error { + if session.Weekday() != time.Monday { + return fmt.Errorf("not a monday") + } + + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Commit() //nolint:errcheck + + stmt, err := s.db.PrepareContext(ctx, "INSERT INTO absences (session_date, userid) VALUES (?, ?)") + if err != nil { + return fmt.Errorf("failed to prepare absence query: %w", err) + } + + _, err = stmt.ExecContext(ctx, session.Format("2006-01-02"), userId) + if err != nil { + return fmt.Errorf("failed to insert absence: %w", err) + } + + return nil +} + +func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string, error) { + tx, err := s.db.Begin() + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Commit() //nolint:errcheck + + stmt, err := s.db.PrepareContext(ctx, `SELECT userid FROM absences WHERE session_date = ?`) + if err != nil { + return nil, fmt.Errorf("failed to prepare query: %w", err) + } + + rows, err := stmt.QueryContext(ctx, session.Format("2006-01-02")) + if err != nil { + return nil, fmt.Errorf("failed to execute query: %w", err) + } + + absentees := make([]string, 0) + for rows.Next() { + var abs string + err = rows.Scan(&abs) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + absentees = append(absentees, abs) + } + + return absentees, nil +} + +type Foo struct { + session_date string + userid string +} + +func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) ([]Foo, error) { + tx, err := s.db.Begin() + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Commit() //nolint:errcheck + + stmt, err := s.db.PrepareContext(ctx, `SELECT session_date, userid FROM absences WHERE session_date BETWEEN ? AND ? ORDER BY session_date ASC`) + if err != nil { + return nil, fmt.Errorf("failed to prepare query: %w", err) + } + + rows, err := stmt.QueryContext(ctx, from.Format("2006-01-02"), to.Format("2006-01-02")) + if err != nil { + return nil, fmt.Errorf("failed to execute query: %w", err) + } + + schedule := make([]Foo, 0) + for rows.Next() { + var foo Foo + err = rows.Scan(&foo.session_date, &foo.userid) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + schedule = append(schedule, foo) + } + + return schedule, nil +} diff --git a/absences_test.go b/absences_test.go new file mode 100644 index 0000000..c6f8403 --- /dev/null +++ b/absences_test.go @@ -0,0 +1,41 @@ +package themis + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddAbsence(t *testing.T) { + store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAddAbsence")) + require.NoError(t, err) + + now := NextMonday() + assert.NoError(t, store.AddAbsence(context.TODO(), now, "foobarbaz")) + absentees, err := store.GetAbsentees(context.TODO(), now) + assert.NoError(t, err) + assert.Equal(t, 1, len(absentees)) + assert.Equal(t, "foobarbaz", absentees[0]) + + assert.NoError(t, store.AddAbsence(context.TODO(), now, "foobarbaz")) + absentees, err = store.GetAbsentees(context.TODO(), now) + assert.NoError(t, err) + assert.Equal(t, 1, len(absentees)) +} + +func TestGetSchedule(t *testing.T) { + store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestGetSchedule")) + require.NoError(t, err) + + now := NextMonday() + + _ = store.AddAbsence(context.TODO(), now.Add(7*24*time.Hour), "foobar") + + schedule, err := store.GetSchedule(context.TODO(), now, now.Add(2*7*24*time.Hour)) + assert.NoError(t, err) + assert.Equal(t, 1, len(schedule)) +} diff --git a/cmd/themis-server/main.go b/cmd/themis-server/main.go index 9813887..3c903d1 100644 --- a/cmd/themis-server/main.go +++ b/cmd/themis-server/main.go @@ -437,6 +437,9 @@ func main() { log.Error().Err(err).Msg("failed to respond to interaction") } }, + "schedule": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + store.GetSchedule(ctx, time.Now(), time.Now().Add(4*7*24*time.Hour)) + }, } registerHandlers(discord, handlers) diff --git a/migrations/20231113_add_absences.down.sql b/migrations/20231113_add_absences.down.sql new file mode 100644 index 0000000..0e2514a --- /dev/null +++ b/migrations/20231113_add_absences.down.sql @@ -0,0 +1 @@ +DROP TABLE absences; diff --git a/migrations/20231113_add_absences.up.sql b/migrations/20231113_add_absences.up.sql new file mode 100644 index 0000000..7efe8c5 --- /dev/null +++ b/migrations/20231113_add_absences.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS absences ( + session_date TEXT, -- 2006-11-23 + userid TEXT, + UNIQUE(session_date, userid) ON CONFLICT IGNORE +); diff --git a/time.go b/time.go new file mode 100644 index 0000000..22b1eaa --- /dev/null +++ b/time.go @@ -0,0 +1,13 @@ +package themis + +import "time" + +var now func() time.Time + +func init() { + now = time.Now +} + +func NextMonday() time.Time { + return now().AddDate(0, 0, int((8-now().Weekday())%7)) +} diff --git a/time_test.go b/time_test.go new file mode 100644 index 0000000..f2d71a7 --- /dev/null +++ b/time_test.go @@ -0,0 +1,46 @@ +package themis + +import ( + "reflect" + "testing" + "time" +) + +func TestNextMonday(t *testing.T) { + tests := []struct { + name string + seed string + want string + }{ + { + name: "on monday", + seed: "2023-11-13T15:04:05Z07:00", + want: "2023-11-13T15:04:05Z07:00", + }, + { + name: "on sunday", + seed: "2023-11-12T15:04:05Z07:00", + want: "2023-11-13T15:04:05Z07:00", + }, + { + name: "on tuesday", + seed: "2023-11-14T15:04:05Z07:00", + want: "2023-11-20T15:04:05Z07:00", + }, + { + name: "on saturday", + seed: "2023-11-18T15:04:05Z07:00", + want: "2023-11-20T15:04:05Z07:00", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + seedt, _ := time.Parse(time.RFC3339, tt.seed) + now = func() time.Time { return seedt } + wantt, _ := time.Parse(time.RFC3339, tt.want) + if got := NextMonday(); !reflect.DeepEqual(got, wantt) { + t.Errorf("NextMonday() = %v, want %v", got, wantt) + } + }) + } +}