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 {
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
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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: