diff --git a/back/store.go b/back/store.go index 1c39450..61c3680 100644 --- a/back/store.go +++ b/back/store.go @@ -216,6 +216,7 @@ type GetEventResponseSummaryQuery struct { type GetEventResponseSummaryResult struct { TotalResponses int + PerHourCounts map[date.Date]map[int]int } func (s *Store) GetEventResponseSummary(ctx context.Context, query GetEventResponseSummaryQuery) (GetEventResponseSummaryResult, error) { @@ -253,6 +254,29 @@ func (s *Store) GetEventResponseSummary(ctx context.Context, query GetEventRespo if err != nil { return GetEventResponseSummaryResult{}, err } + + result.PerHourCounts = make(map[date.Date]map[int]int) + const perHourCountQuery = ` + SELECT date, time, COUNT(*) + FROM response + JOIN response_time ON response.id = response_time.response_id + WHERE event_id = ? + GROUP BY date, time;` + err = sqlitex.Exec(conn, perHourCountQuery, + func(stmt *sqlite.Stmt) error { + d, err := date.Parse(dbDateLayout, stmt.ColumnText(0)) + if err != nil { + return err + } + if result.PerHourCounts[d] == nil { + result.PerHourCounts[d] = make(map[int]int) + } + result.PerHourCounts[d][stmt.ColumnInt(1)] = stmt.ColumnInt(2) + return nil + }, query.EventID) + if err != nil { + return GetEventResponseSummaryResult{}, err + } return result, nil } diff --git a/back/store_test.go b/back/store_test.go index c6cbefe..84d3998 100644 --- a/back/store_test.go +++ b/back/store_test.go @@ -69,13 +69,6 @@ func TestGetEventResponseSummary(t *testing.T) { return responses.TotalResponses } - createEmptyResponse := func(is *is.I, eventID string) { - _, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{ - EventID: eventID, - }) - is.NoErr(err) - } - t.Run("TotalResponses counts the number of responses created", func(t *testing.T) { is := is.New(t) @@ -83,26 +76,86 @@ func TestGetEventResponseSummary(t *testing.T) { is.Equal(getTotalResponses(is, eventID), 0) const respondTimes = 5 for i := 1; i <= respondTimes; i++ { - createEmptyResponse(is, eventID) + _, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{ + EventID: eventID, + }) + is.NoErr(err) is.Equal(getTotalResponses(is, eventID), i) } }) - t.Run("TotalResponses does not count responses to other events", func(t *testing.T) { + t.Run("does not include responses to other events", func(t *testing.T) { is := is.New(t) - eventID := createEvent(is) - createEmptyResponse(is, eventID) + d := date.New(1999, 12, 31) + wrongEvent, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ + Name: "Beach party", + Description: "Come on in! The water's fine!", + Earliest: d, + Latest: d, + }) + is.NoErr(err) - // Create a response to another event - createEmptyResponse(is, createEvent(is)) + _, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{ + EventID: wrongEvent.EventID, + GuestName: "Matt Hooper", + DateHours: map[date.Date]map[int]struct{}{d: {0: {}}}, + }) + is.NoErr(err) - is.Equal(getTotalResponses(is, eventID), 1) + summary, err := store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{ + EventID: createEvent(is), + }) + is.NoErr(err) + + is.Equal(summary, back.GetEventResponseSummaryResult{ + TotalResponses: 0, + PerHourCounts: map[date.Date]map[int]int{}, + }) }) -} -func pending(t *testing.T) { - t.Fatalf("pending") + t.Run("PerHourCounts counts votes per hour", func(t *testing.T) { + is := is.New(t) + + dayOne := date.New(2012, 12, 21) + dayTwo := dayOne.Add(1) + createResult, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ + Name: "Apocalypse", + Description: "It's over.", + Earliest: dayOne, + Latest: dayTwo, + }) + is.NoErr(err) + + _, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{ + EventID: createResult.EventID, + GuestName: "Thanos", + DateHours: map[date.Date]map[int]struct{}{ + dayOne: {0: {}, 1: {}, 2: {}, 3: {}}, + dayTwo: {1: {}, 2: {}, 3: {}, 4: {}}, + }, + }) + is.NoErr(err) + + _, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{ + EventID: createResult.EventID, + GuestName: "Magog", + DateHours: map[date.Date]map[int]struct{}{ + dayOne: {1: {}, 2: {}, 3: {}, 4: {}}, + dayTwo: {2: {}, 3: {}, 4: {}, 5: {}}, + }, + }) + is.NoErr(err) + + summary, err := store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{ + EventID: createResult.EventID, + }) + is.NoErr(err) + is.Equal(summary.PerHourCounts, map[date.Date]map[int]int{ + dayOne: {0: 1, 1: 2, 2: 2, 3: 2, 4: 1}, + dayTwo: {1: 1, 2: 2, 3: 2, 4: 2, 5: 1}, + }) + }) } func TestCreateEventResponse(t *testing.T) { diff --git a/front/server.go b/front/server.go index 329c041..fdb0219 100644 --- a/front/server.go +++ b/front/server.go @@ -357,7 +357,7 @@ func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) { return } - responses, err := h.store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{ + summary, err := h.store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{ EventID: eventID, }) if err != nil { @@ -365,6 +365,33 @@ func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) { return } + // Build the counts table + dateSpan := metadata.Latest.Sub(metadata.Earliest) + var dates []date.Date + for offset := date.PeriodOfDays(0); offset <= dateSpan; offset++ { + dates = append(dates, metadata.Earliest.Add(offset)) + } + var ths = hm.Terms{e.Th()()} + for _, date := range dates { + ths = append(ths, e.Th()(hm.Text(date.Format("Jan 2")))) + } + var rows = hm.Terms{e.Thead()(ths)} + for hour := 0; hour < 24; hour++ { + var row = hm.Terms{ + e.Td()(hm.Text(timeLabels[hour])), + } + for _, day := range dates { + hourCounts := summary.PerHourCounts[day] + var count int + if hourCounts != nil { + count = hourCounts[hour] + } + row = append(row, e.Td()(hm.Text(strconv.Itoa(count)))) + } + rows = append(rows, e.Tr()(row)) + } + countsTable := e.Table()(rows) + body := hm.Terms{ e.Form()( e.Label(a.For(fieldNameEventName))(hm.Text("Event name")), @@ -400,7 +427,8 @@ func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) { ), ), e.H3()(hm.Text("Responses")), - e.P()(hm.Text(strconv.Itoa(responses.TotalResponses))), + e.P()(hm.Text(strconv.Itoa(summary.TotalResponses))), + countsTable, } _ = h.writePage(w, "Edit your event", body) } diff --git a/todo.txt b/todo.txt index 3ec6d04..d067ff0 100644 --- a/todo.txt +++ b/todo.txt @@ -1,14 +1,12 @@ Essential: ------------ -Show results on admin page +Show response after submission +Show respondent names on admin page Make earliest and latest dates required for creation Prevent blank event names and guest names Consider some front-end, regex-based field validation Authenticate admin page with admin code It might be sufficient to require that when getting the response summary. -Deal with the admin date fields - Should the date fields be disabled, or should we cull invalid - response times after changing event dates? Consider redirecting from /create/do to admin Cleanup: