From 28ddbd2ce623d18c8b19eee49be9d7949a66886a Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 29 May 2022 12:01:58 +0200 Subject: [PATCH] Implement motion withdraw --- cmd/boardvoting/handlers.go | 74 +++++++++++++++---- cmd/boardvoting/helpers.go | 3 +- cmd/boardvoting/notifications.go | 18 +++++ .../mailtemplates/withdraw_motion_mail.txt | 6 +- internal/models/models.go | 8 ++ internal/models/motions.go | 73 ++++++++++++------ internal/models/voters.go | 2 +- ui/html/pages/withdraw_motion.html | 20 +++++ 8 files changed, 165 insertions(+), 39 deletions(-) create mode 100644 ui/html/pages/withdraw_motion.html diff --git a/cmd/boardvoting/handlers.go b/cmd/boardvoting/handlers.go index b7c1757..be7881e 100644 --- a/cmd/boardvoting/handlers.go +++ b/cmd/boardvoting/handlers.go @@ -178,9 +178,7 @@ func (app *application) calculateMotionListOptions(r *http.Request) (*models.Mot func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - showVotes := r.URL.Query().Has("showvotes") - - motion := app.motionFromRequestParam(w, r, params, showVotes) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { return } @@ -269,7 +267,7 @@ func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - motion := app.motionFromRequestParam(w, r, params, false) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { return } @@ -290,7 +288,7 @@ func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) { func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - motion := app.motionFromRequestParam(w, r, params, false) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { return } @@ -364,24 +362,74 @@ func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) http.Redirect(w, r, "/motions/", http.StatusSeeOther) } -func (app *application) withdrawMotionForm(_ http.ResponseWriter, _ *http.Request) { - panic("not implemented") +func (app *application) withdrawMotionForm(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + motion := app.motionFromRequestParam(w, r, params) + if motion == nil { + app.notFound(w) + + return + } + + data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) + + data.Motion = motion + + app.render(w, http.StatusOK, "withdraw_motion.html", data) } -func (app *application) withdrawMotionSubmit(_ http.ResponseWriter, _ *http.Request) { - panic("not implemented") +func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + motion := app.motionFromRequestParam(w, r, params) + if motion == nil { + app.notFound(w) + + return + } + + user, err := app.GetUser(r) + if err != nil { + app.clientError(w, http.StatusUnauthorized) + + return + } + + err = app.motions.Update(r.Context(), motion.ID, func(m *models.Motion) { + m.Status = models.VoteStatusWithdrawn + }) + if err != nil { + app.serverError(w, err) + + return + } + + app.mailNotifier.notifyChannel <- &WithDrawMotionNotification{motion, user} + + app.sessionManager.Put( + r.Context(), + "flash", + fmt.Sprintf("Motion %s has been withdrawn!", motion.Tag), + ) + + http.Redirect(w, r, "/motions/", http.StatusSeeOther) } func (app *application) voteForm(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - motion := app.motionFromRequestParam(w, r, params, false) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { + app.notFound(w) + return } choice := app.choiceFromRequestParam(w, params) if choice == nil { + app.notFound(w) + return } @@ -399,7 +447,7 @@ func (app *application) voteForm(w http.ResponseWriter, r *http.Request) { func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - motion := app.motionFromRequestParam(w, r, params, false) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { return } @@ -449,7 +497,7 @@ func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) { func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - motion := app.motionFromRequestParam(w, r, params, false) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { return } @@ -475,7 +523,7 @@ func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) { func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) - motion := app.motionFromRequestParam(w, r, params, false) + motion := app.motionFromRequestParam(w, r, params) if motion == nil { return } diff --git a/cmd/boardvoting/helpers.go b/cmd/boardvoting/helpers.go index cf034dd..6a0a0ce 100644 --- a/cmd/boardvoting/helpers.go +++ b/cmd/boardvoting/helpers.go @@ -175,10 +175,11 @@ func (app *application) motionFromRequestParam( w http.ResponseWriter, r *http.Request, params httprouter.Params, - withVotes bool, ) *models.Motion { tag := params.ByName("tag") + withVotes := r.URL.Query().Has("showvotes") + motion, err := app.motions.GetMotionByTag(r.Context(), tag, withVotes) if err != nil { app.serverError(w, err) diff --git a/cmd/boardvoting/notifications.go b/cmd/boardvoting/notifications.go index ae24522..dbfb2e3 100644 --- a/cmd/boardvoting/notifications.go +++ b/cmd/boardvoting/notifications.go @@ -305,3 +305,21 @@ func (p ProxyVoteNotification) GetNotificationContent(mc *mailConfig) *Notificat recipients: []recipientData{voteNoticeRecipient(mc)}, } } + +type WithDrawMotionNotification struct { + Motion *models.Motion + Voter *models.User +} + +func (w WithDrawMotionNotification) GetNotificationContent(mc *mailConfig) *NotificationContent { + return &NotificationContent{ + template: "withdraw_motion_mail.txt", + data: struct { + *models.Motion + Name string + }{Motion: w.Motion, Name: w.Voter.Name}, + subject: fmt.Sprintf("Re: %s - %s", w.Motion.Tag, w.Motion.Title), + headers: motionReplyHeaders(w.Motion), + recipients: []recipientData{defaultRecipient(mc)}, + } +} diff --git a/internal/mailtemplates/withdraw_motion_mail.txt b/internal/mailtemplates/withdraw_motion_mail.txt index fb3ed87..87fd530 100644 --- a/internal/mailtemplates/withdraw_motion_mail.txt +++ b/internal/mailtemplates/withdraw_motion_mail.txt @@ -1,10 +1,10 @@ Dear Board, -{{ .Name }} has withdrawn the motion {{ .Tag }} that was as follows: +{{ .Data.Name }} has withdrawn the motion {{ .Data.Tag }} that was as follows: -{{ .Title }} +{{ .Data.Title }} -{{ wrap 76 .Content }} +{{ wrap 76 .Data.Content }} Kind regards, the voting system \ No newline at end of file diff --git a/internal/models/models.go b/internal/models/models.go index 5e16392..3ada42a 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -48,3 +48,11 @@ func parseSqlite3TimeStamp(timeStamp string) (*time.Time, error) { return nil, fmt.Errorf("could not parse timestamp: %w", err) } + +const ( + errCouldNotExecuteQuery = "could not execute query: %w" + errCouldNotFetchRow = "could not fetch row: %w" + errCouldNotScanResult = "could not scan result: %w" + errCouldNotStartTransaction = "could not start transaction: %w" + errCouldNotCommitTransaction = "could not commit transaction: %w" +) diff --git a/internal/models/motions.go b/internal/models/motions.go index 104c2b3..cd8ab96 100644 --- a/internal/models/motions.go +++ b/internal/models/motions.go @@ -108,11 +108,11 @@ var ( voteStatusDeclined = &VoteStatus{Label: "declined", ID: -1} voteStatusPending = &VoteStatus{Label: "pending", ID: 0} voteStatusApproved = &VoteStatus{Label: "approved", ID: 1} - voteStatusWithdrawn = &VoteStatus{Label: "withdrawn", ID: -2} + VoteStatusWithdrawn = &VoteStatus{Label: "withdrawn", ID: -2} ) func VoteStatusFromInt(id int64) (*VoteStatus, error) { - for _, vs := range []*VoteStatus{voteStatusPending, voteStatusApproved, voteStatusWithdrawn, voteStatusDeclined} { + for _, vs := range []*VoteStatus{voteStatusPending, voteStatusApproved, VoteStatusWithdrawn, voteStatusDeclined} { if int64(vs.ID) == id { return vs, nil } @@ -295,7 +295,7 @@ VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :prop func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*Motion, error) { tx, err := m.DB.BeginTxx(ctx, nil) if err != nil { - return nil, fmt.Errorf("could not start transaction: %w", err) + return nil, fmt.Errorf(errCouldNotStartTransaction, err) } defer func(tx *sqlx.Tx) { @@ -320,7 +320,7 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now for rows.Next() { decision := &Motion{} if err = rows.StructScan(decision); err != nil { - return nil, fmt.Errorf("scanning row failed: %w", err) + return nil, fmt.Errorf(errCouldNotScanResult, err) } if rows.Err() != nil { @@ -345,7 +345,7 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now } if err = tx.Commit(); err != nil { - return nil, fmt.Errorf("could not commit transaction: %w", err) + return nil, fmt.Errorf(errCouldNotCommitTransaction, err) } return results, nil @@ -373,7 +373,7 @@ func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) ) if err != nil { - return nil, fmt.Errorf("could not execute update query: %w", err) + return nil, fmt.Errorf(errCouldNotExecuteQuery, err) } affectedRows, err := result.RowsAffected() @@ -393,8 +393,38 @@ func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) return d, nil } -func (m *MotionModel) UnVotedDecisionsForVoter(_ context.Context, _ *User) ([]*Motion, error) { - panic("not implemented") +func (m *MotionModel) UnVotedDecisionsForVoter(ctx context.Context, voter *User) ([]*Motion, error) { + rows, err := m.DB.QueryxContext( + ctx, + `SELECT decisions.* +FROM decisions +WHERE NOT EXISTS(SELECT * FROM votes WHERE decision = decisions.id AND voter = ?)`, + voter.ID) + if err != nil { + return nil, fmt.Errorf(errCouldNotExecuteQuery, err) + } + + defer func(rows *sqlx.Rows) { + _ = rows.Close() + }(rows) + + result := make([]*Motion, 0) + + for rows.Next() { + if err := rows.Err(); err != nil { + return nil, fmt.Errorf(errCouldNotFetchRow, err) + } + + var motion Motion + + if err := rows.StructScan(&motion); err != nil { + return nil, fmt.Errorf(errCouldNotScanResult, err) + } + + result = append(result, &motion) + } + + return result, nil } func (m *MotionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*VoteSums, error) { @@ -560,11 +590,11 @@ func (m *MotionModel) GetMotions(ctx context.Context, options *MotionListOptions var decision Motion if err = rows.Err(); err != nil { - return nil, fmt.Errorf("could not fetch row: %w", err) + return nil, fmt.Errorf(errCouldNotFetchRow, err) } if err = rows.StructScan(&decision); err != nil { - return nil, fmt.Errorf("could not scan result: %w", err) + return nil, fmt.Errorf(errCouldNotScanResult, err) } motions = append(motions, &decision) @@ -600,7 +630,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*Motion) err rows, err := m.DB.QueryContext(ctx, query, args...) if err != nil { - return fmt.Errorf("could not execute query: %w", err) + return fmt.Errorf(errCouldNotExecuteQuery, err) } defer func(rows *sql.Rows) { @@ -609,7 +639,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*Motion) err for rows.Next() { if err = rows.Err(); err != nil { - return fmt.Errorf("could not fetch row: %w", err) + return fmt.Errorf(errCouldNotFetchRow, err) } var ( @@ -620,7 +650,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*Motion) err err = rows.Scan(&decisionID, &vote, &count) if err != nil { - return fmt.Errorf("could not scan row: %w", err) + return fmt.Errorf(errCouldNotScanResult, err) } switch { @@ -826,7 +856,7 @@ ORDER BY voters.name`, var vote Vote if err := rows.StructScan(&vote); err != nil { - return fmt.Errorf("could not scan row: %w", err) + return fmt.Errorf(errCouldNotScanResult, err) } md.Votes = append(md.Votes, &vote) @@ -858,7 +888,7 @@ func (m *MotionModel) Update( ) error { tx, err := m.DB.BeginTxx(ctx, nil) if err != nil { - return fmt.Errorf("could not start transaction: %w", err) + return fmt.Errorf(errCouldNotStartTransaction, err) } defer func(tx *sqlx.Tx) { @@ -873,7 +903,7 @@ func (m *MotionModel) Update( var motion Motion if err := row.StructScan(&motion); err != nil { - return fmt.Errorf("could not scan row: %w", err) + return fmt.Errorf(errCouldNotScanResult, err) } updateFn(&motion) @@ -887,7 +917,8 @@ SET title=:title, content=:content, votetype=:votetype, due=:due, - modified=:modified + modified=:modified, + status=:status WHERE id = :id`, motion, ) @@ -896,7 +927,7 @@ WHERE id = :id`, } if err := tx.Commit(); err != nil { - return fmt.Errorf("could not commit transaction: %w", err) + return fmt.Errorf(errCouldNotCommitTransaction, err) } return nil @@ -914,7 +945,7 @@ type Vote struct { func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error { tx, err := m.DB.BeginTxx(ctx, nil) if err != nil { - return fmt.Errorf("could not start transaction: %w", err) + return fmt.Errorf(errCouldNotStartTransaction, err) } defer func(tx *sqlx.Tx) { @@ -923,7 +954,7 @@ func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, pe row := tx.QueryRowxContext(ctx, `SELECT * FROM votes WHERE voter=? AND decision=?`, userID, motionID) if err := row.Err(); err != nil { - return fmt.Errorf("could not execute query: %w", err) + return fmt.Errorf(errCouldNotExecuteQuery, err) } vote := Vote{UserID: userID, MotionID: motionID} @@ -950,7 +981,7 @@ WHERE decision = :decision } if err := tx.Commit(); err != nil { - return fmt.Errorf("could not commit transaction: %w", err) + return fmt.Errorf(errCouldNotCommitTransaction, err) } return nil diff --git a/internal/models/voters.go b/internal/models/voters.go index 234193f..e56c992 100644 --- a/internal/models/voters.go +++ b/internal/models/voters.go @@ -218,7 +218,7 @@ func (m *UserModel) CreateUser(ctx context.Context, name string, reminder string res, err := tx.ExecContext( ctx, - `INSERT INTO voters (name) VALUES (?)`, + `INSERT INTO voters (name, enabled) VALUES (?, 0)`, name) if err != nil { return 0, fmt.Errorf("could not insert user: %w", err) diff --git a/ui/html/pages/withdraw_motion.html b/ui/html/pages/withdraw_motion.html new file mode 100644 index 0000000..7d59a96 --- /dev/null +++ b/ui/html/pages/withdraw_motion.html @@ -0,0 +1,20 @@ +{{ define "title" }}Withdraw Motion {{ .Motion.Tag }}{{ end }} + +{{ define "main" }} +
+ {{ template "motion_display" .Motion }} +
+ +
+
+ Withdraw motion? +
+

Do you want to withdraw motion {{ .Motion.Tag }}: {{ .Motion.Title }}?

+
+ + Cancel +
+
+{{ end }} \ No newline at end of file