Show responses on admin page
This commit is contained in:
parent
0da89f0e6c
commit
235b826dbb
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
Description: "Come on in! The water's fine!",
|
||||||
|
Earliest: d,
|
||||||
|
Latest: d,
|
||||||
|
})
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
// Create a response to another event
|
_, err = store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{
|
||||||
createEmptyResponse(is, createEvent(is))
|
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.Run("PerHourCounts counts votes per hour", func(t *testing.T) {
|
||||||
t.Fatalf("pending")
|
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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
6
todo.txt
6
todo.txt
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user