Save responses in Store

This commit is contained in:
Brandon Dyck 2020-09-27 15:44:04 -06:00
parent cd261000fc
commit bc486fa976
3 changed files with 247 additions and 30 deletions

View File

@ -54,7 +54,7 @@ func NewMemoryStore(genString GenString) (*Store, error) {
pool: pool, pool: pool,
genString: genString, genString: genString,
} }
log.Println("creating schema")
err = store.createSchema() err = store.createSchema()
if err != nil { if err != nil {
defer pool.Close() defer pool.Close()
@ -84,13 +84,14 @@ const schema = `
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
alpha_id TEXT NOT NULL, alpha_id TEXT NOT NULL,
event_id INTEGER NOT NULL, event_id INTEGER NOT NULL,
guest_name TEXT NOT NULL,
UNIQUE (alpha_id), UNIQUE (alpha_id),
FOREIGN KEY (event_id) REFERENCES event(id) FOREIGN KEY (event_id) REFERENCES event(id)
); );
CREATE TABLE response_time ( CREATE TABLE response_time (
response_id INTEGER PRIMARY KEY, response_id INTEGER NOT NULL,
date DATE NOT NULL, date DATE NOT NULL,
time INTEGER NOT NULL, time INTEGER NOT NULL,
@ -103,7 +104,7 @@ const schema = `
BEGIN BEGIN
SELECT SELECT
CASE CASE
WHEN NEW.date BETWEEN event.earliest_date AND event.latest_date WHEN NEW.date NOT BETWEEN event.earliest_date AND event.latest_date
THEN RAISE(ABORT, 'response date is out of range') THEN RAISE(ABORT, 'response date is out of range')
END END
FROM response FROM response
@ -213,36 +214,58 @@ func (s *Store) GetEventMetadata(ctx context.Context, query GetEventMetadataQuer
return result, nil return result, nil
} }
type GetEventResponsesQuery struct { type GetEventResponseSummaryQuery struct {
AlphaID string AlphaID string
} }
type GetEventResponsesResult struct { type GetEventResponseSummaryResult struct {
TotalResponses int TotalResponses int
} }
func (s *Store) GetEventResponses(ctx context.Context, query GetEventResponsesQuery) (GetEventResponsesResult, error) { func (s *Store) GetEventResponseSummary(ctx context.Context, query GetEventResponseSummaryQuery) (GetEventResponseSummaryResult, error) {
conn := s.pool.Get(ctx) conn := s.pool.Get(ctx)
defer s.pool.Put(conn) defer s.pool.Put(conn)
var result GetEventResponsesResult var found bool
const dbQuery = ` var eventID int
const eventIDQuery = `
SELECT id
FROM event
WHERE alpha_id = ?`
var err = sqlitex.Exec(conn, eventIDQuery,
func(stmt *sqlite.Stmt) error {
found = true
eventID = stmt.ColumnInt(0)
return nil
}, query.AlphaID)
if err != nil {
return GetEventResponseSummaryResult{}, err
}
if !found {
return GetEventResponseSummaryResult{}, errors.New("event not found")
}
var result GetEventResponseSummaryResult
const responseCountQuery = `
SELECT COUNT(*) SELECT COUNT(*)
FROM response FROM response
JOIN event ON response.event_id = event.id WHERE event_id = ?;`
WHERE event.alpha_id = ?;` err = sqlitex.Exec(conn, responseCountQuery,
err := sqlitex.Exec(conn, dbQuery,
func(stmt *sqlite.Stmt) error { func(stmt *sqlite.Stmt) error {
result.TotalResponses = stmt.ColumnInt(0) result.TotalResponses = stmt.ColumnInt(0)
return nil return nil
}, query.AlphaID) }, eventID)
// TODO return an error if the event does not exist? if err != nil {
return result, err return GetEventResponseSummaryResult{}, err
}
return result, nil
} }
type CreateEventResponseCommand struct { type CreateEventResponseCommand struct {
EventAlphaID string EventAlphaID string
DateHours map[date.Date]int GuestName string
DateHours map[date.Date]map[int]struct{}
} }
type CreateEventResponseResult struct { type CreateEventResponseResult struct {
@ -258,22 +281,95 @@ func (s *Store) CreateEventResponse(ctx context.Context, cmd CreateEventResponse
defer sqlitex.Save(conn)(&err) defer sqlitex.Save(conn)(&err)
responseAlphaID, err := s.genString(responseAlphaIDLength) responseAlphaID, err := s.genString(responseAlphaIDLength)
if err != nil { if err != nil {
return CreateEventResponseResult{}, err return
} }
// Create response
const responseQuery = ` const responseQuery = `
INSERT INTO response(event_id, alpha_id) INSERT INTO response(event_id, alpha_id, guest_name)
SELECT event.id AS event_id, ? AS alpha_id SELECT event.id AS event_id, ? AS alpha_id, ? as guest_name
FROM event FROM event
WHERE event.alpha_id = ?;` WHERE event.alpha_id = ?;`
err = sqlitex.Exec(conn, responseQuery, nil, responseAlphaID, cmd.EventAlphaID) err = sqlitex.Exec(conn, responseQuery, nil, responseAlphaID, cmd.GuestName, cmd.EventAlphaID)
if err != nil { if err != nil {
return CreateEventResponseResult{}, err return
}
const responseTimeQuery = `
INSERT INTO response_time(response_id, date, time)
SELECT response.id AS response_id, ? AS date, ? AS time
FROM response
WHERE response.alpha_id = ?;`
for d, hs := range cmd.DateHours {
for h := range hs {
err = sqlitex.Exec(conn, responseTimeQuery, nil, d.Format(dbDateLayout), h, responseAlphaID)
if err != nil {
return
}
}
} }
// TODO Create response times
return CreateEventResponseResult{ return CreateEventResponseResult{
ResponseAlphaID: responseAlphaID, ResponseAlphaID: responseAlphaID,
}, nil }, nil
} }
type GetEventResponseQuery struct {
EventAlphaID string
ResponseAlphaID string
}
type GetEventResponseResult struct {
GuestName string
DateHours map[date.Date]map[int]struct{}
}
func (s *Store) GetEventResponse(ctx context.Context, query GetEventResponseQuery) (GetEventResponseResult, error) {
conn := s.pool.Get(ctx)
defer s.pool.Put(conn)
var found bool
var result GetEventResponseResult
var responseID int
const responseQuery = `
SELECT guest_name, response.id
FROM response
JOIN event ON response.event_id = event.id
WHERE response.alpha_id = ? AND event.alpha_id = ?;`
var err = sqlitex.Exec(conn, responseQuery,
func(stmt *sqlite.Stmt) error {
found = true
result.GuestName = stmt.ColumnText(0)
responseID = stmt.ColumnInt(1)
return nil
}, query.ResponseAlphaID, query.EventAlphaID)
if err != nil {
return GetEventResponseResult{}, err
}
if !found {
// TODO return a constant or typed error
return GetEventResponseResult{}, errors.New("not found")
}
result.DateHours = make(map[date.Date]map[int]struct{})
const responseTimeQuery = `
SELECT date, time
FROM response_time
WHERE response_id = ?`
err = sqlitex.Exec(conn, responseTimeQuery,
func(stmt *sqlite.Stmt) error {
d, err := date.Parse(dbDateLayout, stmt.ColumnText(0))
if err != nil {
return err
}
if result.DateHours[d] == nil {
result.DateHours[d] = make(map[int]struct{})
}
result.DateHours[d][stmt.ColumnInt(1)] = struct{}{}
return nil
}, responseID)
if err != nil {
return GetEventResponseResult{}, err
}
return result, nil
}

View File

@ -45,9 +45,10 @@ func TestCreateEvent(t *testing.T) {
is.True(metadataResult.Latest.Equal(latest)) is.True(metadataResult.Latest.Equal(latest))
} }
func TestGetEventResponses(t *testing.T) { func TestGetEventResponseSummary(t *testing.T) {
store, err := back.NewMemoryStore(back.SecureGenString) store, err := back.NewMemoryStore(back.SecureGenString)
is.New(t).NoErr(err) is.New(t).NoErr(err)
defer store.Close()
createEvent := func(is *is.I) (eventID string) { createEvent := func(is *is.I) (eventID string) {
event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
@ -61,7 +62,7 @@ func TestGetEventResponses(t *testing.T) {
} }
getTotalResponses := func(is *is.I, eventID string) int { getTotalResponses := func(is *is.I, eventID string) int {
responses, err := store.GetEventResponses(context.Background(), back.GetEventResponsesQuery{ responses, err := store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{
AlphaID: eventID, AlphaID: eventID,
}) })
is.NoErr(err) is.NoErr(err)
@ -99,3 +100,74 @@ func TestGetEventResponses(t *testing.T) {
is.Equal(getTotalResponses(is, eventID), 1) is.Equal(getTotalResponses(is, eventID), 1)
}) })
} }
func pending(t *testing.T) {
t.Fatalf("pending")
}
func TestCreateEventResponse(t *testing.T) {
store, err := back.NewMemoryStore(back.SecureGenString)
is.New(t).NoErr(err)
createEvent := func(is *is.I) (eventID string) {
event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
Name: "blah",
Description: "stuff happening",
Earliest: date.Today(),
Latest: date.Today().Add(1),
})
is.NoErr(err)
return event.AlphaID
}
t.Run("saves GuestName", func(t *testing.T) {
is := is.New(t)
eventID := createEvent(is)
const guestName = "Etaoin Shrdlu"
createResult, err := store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{
EventAlphaID: eventID,
GuestName: guestName,
})
is.NoErr(err)
getResult, err := store.GetEventResponse(context.Background(), back.GetEventResponseQuery{
EventAlphaID: eventID,
ResponseAlphaID: createResult.ResponseAlphaID,
})
is.NoErr(err)
is.Equal(getResult.GuestName, guestName)
})
t.Run("DateHours saves multiple times on multiple dates", func(t *testing.T) {
is := is.New(t)
earliest := date.Today()
latest := earliest.Add(1)
event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
Name: "blah",
Description: "stuff happening",
Earliest: earliest,
Latest: latest,
})
is.NoErr(err)
var dateHours = map[date.Date]map[int]struct{}{
earliest: {1: {}, 3: {}, 5: {}, 7: {}},
latest: {2: {}, 4: {}, 6: {}},
}
createResponseResult, err := store.CreateEventResponse(context.Background(), back.CreateEventResponseCommand{
EventAlphaID: event.AlphaID,
GuestName: "Etaoin Shrdlu",
DateHours: dateHours,
})
is.NoErr(err)
response, err := store.GetEventResponse(context.Background(), back.GetEventResponseQuery{
EventAlphaID: event.AlphaID,
ResponseAlphaID: createResponseResult.ResponseAlphaID,
})
is.NoErr(err)
is.Equal(dateHours, response.DateHours)
})
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -69,6 +70,7 @@ var timeLabels = []string{
} }
type voteState struct { type voteState struct {
eventAlphaID string
name string name string
earliest, latest date.Date earliest, latest date.Date
} }
@ -90,15 +92,22 @@ func voteForm(disabled bool, st voteState) hm.Term {
var row = hm.Terms{ var row = hm.Terms{
e.Td()(hm.Text(timeLabels[hour])), e.Td()(hm.Text(timeLabels[hour])),
} }
for day := 0; day < len(dates); day++ { for _, day := range dates {
row = append(row, e.Td()(e.Input(a.Type("checkbox"), a.Disabled(disabled)))) value := fmt.Sprintf("%8s-%02d", day.Format("20060102"), hour)
row = append(row, e.Td()(e.Input(
a.Name("slot"),
a.Value(value),
a.Type("checkbox"),
a.Disabled(disabled),
)))
} }
rows = append(rows, e.Tr()(row)) rows = append(rows, e.Tr()(row))
} }
return e.Form(a.Method(http.MethodPost), a.Action(pathDoVote))( return e.Form(a.Method(http.MethodPost), a.Action(pathDoVote))(
e.Label()(hm.Text("What's your name?")), e.Input(a.Type("hidden"), a.Name(keyEventID), a.Value(st.eventAlphaID)),
e.Label(a.For(fieldNameGuestName))(hm.Text("What's your name?")),
e.Br(), e.Br(),
e.Input(a.Size(40), a.Value(st.name), a.Disabled(disabled)), e.Input(a.Name(fieldNameGuestName), a.Size(40), a.Value(st.name), a.Disabled(disabled)),
e.Fieldset()( e.Fieldset()(
e.Legend()(hm.Text("When are you available?")), e.Legend()(hm.Text("When are you available?")),
e.Table()(rows), e.Table()(rows),
@ -130,11 +139,50 @@ func (h *handler) handleVote(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) handleDoVote(w http.ResponseWriter, r *http.Request) { func (h *handler) handleDoVote(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Println(err)
http.Error(w, "invalid form values", http.StatusBadRequest)
return
}
cmd := back.CreateEventResponseCommand{
EventAlphaID: r.URL.Query().Get(keyEventID),
DateHours: make(map[date.Date]map[int]struct{}),
}
for _, slot := range r.PostForm["slot"] {
var dateString string
var hour int
_, err := fmt.Sscanf(slot, "%8s-%02d", &dateString, &hour)
if err != nil {
log.Println(err)
http.Error(w, "invalid form values", http.StatusBadRequest)
return
}
d, err := date.Parse("20060102", dateString)
if err != nil {
log.Println(err)
http.Error(w, "invalid form values", http.StatusBadRequest)
return
}
if cmd.DateHours[d] == nil {
cmd.DateHours[d] = make(map[int]struct{})
}
cmd.DateHours[d][hour] = struct{}{}
}
_, err = h.store.CreateEventResponse(r.Context(), cmd)
if err != nil {
log.Println(err)
// TODO handle not found
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
// TODO Consider redirecting to vote edit page once that exists. // TODO Consider redirecting to vote edit page once that exists.
// TODO Use actual data. // TODO Use actual data.
state := voteState{ state := voteState{
name: "Suzie Q", name: r.Form.Get(fieldNameGuestName),
earliest: date.New(2006, time.May, 3), earliest: date.New(2006, time.May, 3),
latest: date.New(2006, time.May, 8), latest: date.New(2006, time.May, 8),
} }
@ -252,7 +300,7 @@ func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
return return
} }
responses, err := h.store.GetEventResponses(context.Background(), back.GetEventResponsesQuery{ responses, err := h.store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{
AlphaID: eventID, AlphaID: eventID,
}) })
if err != nil { if err != nil {
@ -326,6 +374,7 @@ const (
fieldNameLatest = "latestDate" fieldNameLatest = "latestDate"
fieldNameEventName = "eventName" fieldNameEventName = "eventName"
fieldNameDescription = "eventDescription" fieldNameDescription = "eventDescription"
fieldNameGuestName = "guestName"
) )
const keyEventID = "event_id" const keyEventID = "event_id"