Show responses on admin page

This commit is contained in:
Brandon Dyck 2020-10-04 14:24:04 -06:00
parent 0da89f0e6c
commit 235b826dbb
4 changed files with 126 additions and 23 deletions

View File

@ -216,6 +216,7 @@ type GetEventResponseSummaryQuery struct {
type GetEventResponseSummaryResult struct { type GetEventResponseSummaryResult struct {
TotalResponses int TotalResponses int
PerHourCounts map[date.Date]map[int]int
} }
func (s *Store) GetEventResponseSummary(ctx context.Context, query GetEventResponseSummaryQuery) (GetEventResponseSummaryResult, error) { 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 { if err != nil {
return GetEventResponseSummaryResult{}, err 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 return result, nil
} }

View File

@ -69,13 +69,6 @@ func TestGetEventResponseSummary(t *testing.T) {
return responses.TotalResponses 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) { t.Run("TotalResponses counts the number of responses created", func(t *testing.T) {
is := is.New(t) is := is.New(t)
@ -83,26 +76,86 @@ func TestGetEventResponseSummary(t *testing.T) {
is.Equal(getTotalResponses(is, eventID), 0) is.Equal(getTotalResponses(is, eventID), 0)
const respondTimes = 5 const respondTimes = 5
for i := 1; i <= respondTimes; i++ { 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) 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) is := is.New(t)
eventID := createEvent(is) d := date.New(1999, 12, 31)
createEmptyResponse(is, eventID) wrongEvent, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
Name: "Beach party",
// Create a response to another event Description: "Come on in! The water's fine!",
createEmptyResponse(is, createEvent(is)) Earliest: d,
Latest: d,
is.Equal(getTotalResponses(is, eventID), 1)
}) })
} is.NoErr(err)
func pending(t *testing.T) { _, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{
t.Fatalf("pending") EventID: wrongEvent.EventID,
GuestName: "Matt Hooper",
DateHours: map[date.Date]map[int]struct{}{d: {0: {}}},
})
is.NoErr(err)
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{},
})
})
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) { func TestCreateEventResponse(t *testing.T) {

View File

@ -357,7 +357,7 @@ func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
return return
} }
responses, err := h.store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{ summary, err := h.store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{
EventID: eventID, EventID: eventID,
}) })
if err != nil { if err != nil {
@ -365,6 +365,33 @@ func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
return 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{ body := hm.Terms{
e.Form()( e.Form()(
e.Label(a.For(fieldNameEventName))(hm.Text("Event name")), 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.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) _ = h.writePage(w, "Edit your event", body)
} }

View File

@ -1,14 +1,12 @@
Essential: Essential:
------------ ------------
Show results on admin page Show response after submission
Show respondent names on admin page
Make earliest and latest dates required for creation Make earliest and latest dates required for creation
Prevent blank event names and guest names Prevent blank event names and guest names
Consider some front-end, regex-based field validation Consider some front-end, regex-based field validation
Authenticate admin page with admin code Authenticate admin page with admin code
It might be sufficient to require that when getting the response summary. 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 Consider redirecting from /create/do to admin
Cleanup: Cleanup: