Show responses on admin page
This commit is contained in:
parent
0da89f0e6c
commit
235b826dbb
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
6
todo.txt
6
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:
|
||||
|
Loading…
Reference in New Issue
Block a user