diff --git a/cmd/boardvoting/handlers.go b/cmd/boardvoting/handlers.go index 45de331..0442364 100644 --- a/cmd/boardvoting/handlers.go +++ b/cmd/boardvoting/handlers.go @@ -19,6 +19,8 @@ package main import ( "bytes" + "database/sql" + "errors" "fmt" "html/template" "io/fs" @@ -27,11 +29,11 @@ import ( "strings" "time" - "github.com/Masterminds/sprig/v3" - "github.com/gorilla/csrf" - "git.cacert.org/cacert-boardvoting/internal/models" "git.cacert.org/cacert-boardvoting/ui" + "github.com/Masterminds/sprig/v3" + "github.com/gorilla/csrf" + "github.com/julienschmidt/httprouter" ) func newTemplateCache() (map[string]*template.Template, error) { @@ -81,6 +83,11 @@ type templateData struct { } PrevPage, NextPage string Motions []*models.MotionForDisplay + Motion *models.MotionForDisplay +} + +func (app *application) newTemplateData(r *http.Request) *templateData { + return &templateData{} } func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) { @@ -164,7 +171,9 @@ func (app *application) motionList(w http.ResponseWriter, r *http.Request) { return } - templateData := &templateData{Motions: motions} + templateData := app.newTemplateData(r) + + templateData.Motions = motions err = templateData.setPaginationParameters(first, last) if err != nil { @@ -208,19 +217,53 @@ func calculateMotionListOptions(r *http.Request) (*models.MotionListOptions, err return listOptions, nil } -func (app *application) home(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) +func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + tag := params.ByName("tag") + + showVotes := r.URL.Query().Has("showvotes") + + motion, err := app.motions.GetMotionByTag(r.Context(), tag, showVotes) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + app.serverError(w, err) return } - if r.Method != "GET" && r.Method != "HEAD" { - w.Header().Set("Allow", "GET,HEAD") - app.clientError(w, http.StatusMethodNotAllowed) + if motion == nil { + app.notFound(w) return } - http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) + data := app.newTemplateData(r) + + data.Motion = motion + + app.render(w, http.StatusOK, "motion.html", data) +} + +func (app *application) newMotionForm(writer http.ResponseWriter, request *http.Request) { + panic("not implemented") +} + +func (app *application) newMotionSubmit(writer http.ResponseWriter, request *http.Request) { + panic("not implemented") +} + +func (app *application) voteForm(writer http.ResponseWriter, request *http.Request) { + panic("not implemented") +} + +func (app *application) voteSubmit(writer http.ResponseWriter, request *http.Request) { + panic("not implemented") +} + +func (app *application) proxyVoteForm(writer http.ResponseWriter, request *http.Request) { + panic("not implemented") +} + +func (app *application) proxyVoteSubmit(writer http.ResponseWriter, request *http.Request) { + panic("not implemented") } diff --git a/cmd/boardvoting/routes.go b/cmd/boardvoting/routes.go index 2fffec0..be9449d 100644 --- a/cmd/boardvoting/routes.go +++ b/cmd/boardvoting/routes.go @@ -21,6 +21,7 @@ import ( "io/fs" "net/http" + "github.com/julienschmidt/httprouter" "github.com/justinas/alice" "github.com/vearutop/statigz" "github.com/vearutop/statigz/brotli" @@ -29,7 +30,7 @@ import ( ) func (app *application) routes() http.Handler { - mux := http.NewServeMux() + router := httprouter.New() staticDir, _ := fs.Sub(ui.Files, "static") @@ -40,13 +41,23 @@ func (app *application) routes() http.Handler { fileServer := statigz.FileServer(staticData, brotli.AddEncoding, statigz.EncodeOnInit) - mux.Handle("/favicon.ico", http.RedirectHandler("/static/images/favicon.ico", http.StatusMovedPermanently)) - mux.Handle("/static/", http.StripPrefix("/static", fileServer)) - - mux.HandleFunc("/", app.home) - mux.HandleFunc("/motions/", app.motionList) + router.Handler(http.MethodGet, "/", http.RedirectHandler("/motions/", http.StatusMovedPermanently)) + router.Handler( + http.MethodGet, + "/favicon.ico", + http.RedirectHandler("/static/images/favicon.ico", http.StatusMovedPermanently), + ) + router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer)) + router.HandlerFunc(http.MethodGet, "/motions/", app.motionList) + router.HandlerFunc(http.MethodGet, "/motions/:tag", app.motionDetails) + router.HandlerFunc(http.MethodGet, "/newmotion/", app.newMotionForm) + router.HandlerFunc(http.MethodPost, "/newmotion/", app.newMotionSubmit) + router.HandlerFunc(http.MethodGet, "/vote/", app.voteForm) + router.HandlerFunc(http.MethodPost, "/vote/", app.voteSubmit) + router.HandlerFunc(http.MethodGet, "/proxy/", app.proxyVoteForm) + router.HandlerFunc(http.MethodPost, "/proxy/", app.proxyVoteSubmit) standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders) - return standard.Then(mux) + return standard.Then(router) } diff --git a/go.mod b/go.mod index 6c06a61..febdd5b 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index d528e53..6afcfed 100644 --- a/go.sum +++ b/go.sum @@ -749,6 +749,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= diff --git a/internal/models/motions.go b/internal/models/motions.go index 65e207d..019ea71 100644 --- a/internal/models/motions.go +++ b/internal/models/motions.go @@ -390,7 +390,7 @@ type MotionForDisplay struct { Due time.Time Modified time.Time Sums VoteSums - Votes []VoteForDisplay + Votes []*VoteForDisplay } type VoteForDisplay struct { @@ -626,3 +626,82 @@ LIMIT $1`, return rows, nil } + +func (m *MotionModel) GetMotionByTag(ctx context.Context, tag string, withVotes bool) (*MotionForDisplay, error) { + row := m.DB.QueryRowxContext( + ctx, + `SELECT decisions.id, + decisions.tag, + decisions.proponent, + voters.name AS proposer, + decisions.proposed, + decisions.title, + decisions.content, + decisions.votetype, + decisions.status, + decisions.due, + decisions.modified +FROM decisions + JOIN voters ON decisions.proponent = voters.id +WHERE decisions.tag = ?`, + tag, + ) + if err := row.Err(); err != nil { + return nil, fmt.Errorf("could not query motion: %w", err) + } + + var result MotionForDisplay + + if err := row.StructScan(&result); err != nil { + return nil, fmt.Errorf("could not fill motion from query result: %w", err) + } + + if err := m.FillVoteSums(ctx, []*MotionForDisplay{&result}); err != nil { + return nil, fmt.Errorf("could not get vote sums: %w", err) + } + + if withVotes { + if err := m.FillVotes(ctx, &result); err != nil { + return nil, fmt.Errorf("could not get votes for %s: %w", result.Tag, err) + } + } + + return &result, nil +} + +func (m *MotionModel) FillVotes(ctx context.Context, md *MotionForDisplay) error { + rows, err := m.DB.QueryxContext(ctx, + `SELECT voters.name, votes.vote +FROM voters + JOIN votes ON votes.voter = voters.id +WHERE votes.decision = ? +ORDER BY voters.name`, + md.ID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil + } + + return fmt.Errorf("could not fetch rows: %w", err) + } + + defer func(rows *sqlx.Rows) { + _ = rows.Close() + }(rows) + + for rows.Next() { + if err := rows.Err(); err != nil { + return fmt.Errorf("could not get row: %w", err) + } + + var voteDisplay VoteForDisplay + + if err := rows.StructScan(&voteDisplay); err != nil { + return fmt.Errorf("could not scan row: %w", err) + } + + md.Votes = append(md.Votes, &voteDisplay) + } + + return nil +} diff --git a/ui/html/base.html b/ui/html/base.html index 84a6b08..7a6baa4 100644 --- a/ui/html/base.html +++ b/ui/html/base.html @@ -4,6 +4,7 @@