Implement motion withdraw

main
Jan Dittberner 2 years ago
parent c12aaf4d89
commit 28ddbd2ce6

@ -178,9 +178,7 @@ func (app *application) calculateMotionListOptions(r *http.Request) (*models.Mot
func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) { func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
showVotes := r.URL.Query().Has("showvotes") motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r, params, showVotes)
if motion == nil { if motion == nil {
return 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) { func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params, false) motion := app.motionFromRequestParam(w, r, params)
if motion == nil { if motion == nil {
return 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) { func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params, false) motion := app.motionFromRequestParam(w, r, params)
if motion == nil { if motion == nil {
return return
} }
@ -364,24 +362,74 @@ func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, "/motions/", http.StatusSeeOther) http.Redirect(w, r, "/motions/", http.StatusSeeOther)
} }
func (app *application) withdrawMotionForm(_ http.ResponseWriter, _ *http.Request) { func (app *application) withdrawMotionForm(w http.ResponseWriter, r *http.Request) {
panic("not implemented") 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) { func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Request) {
panic("not implemented") 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) { func (app *application) voteForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params, false) motion := app.motionFromRequestParam(w, r, params)
if motion == nil { if motion == nil {
app.notFound(w)
return return
} }
choice := app.choiceFromRequestParam(w, params) choice := app.choiceFromRequestParam(w, params)
if choice == nil { if choice == nil {
app.notFound(w)
return 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) { func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params, false) motion := app.motionFromRequestParam(w, r, params)
if motion == nil { if motion == nil {
return 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) { func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params, false) motion := app.motionFromRequestParam(w, r, params)
if motion == nil { if motion == nil {
return 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) { func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params, false) motion := app.motionFromRequestParam(w, r, params)
if motion == nil { if motion == nil {
return return
} }

@ -175,10 +175,11 @@ func (app *application) motionFromRequestParam(
w http.ResponseWriter, w http.ResponseWriter,
r *http.Request, r *http.Request,
params httprouter.Params, params httprouter.Params,
withVotes bool,
) *models.Motion { ) *models.Motion {
tag := params.ByName("tag") tag := params.ByName("tag")
withVotes := r.URL.Query().Has("showvotes")
motion, err := app.motions.GetMotionByTag(r.Context(), tag, withVotes) motion, err := app.motions.GetMotionByTag(r.Context(), tag, withVotes)
if err != nil { if err != nil {
app.serverError(w, err) app.serverError(w, err)

@ -305,3 +305,21 @@ func (p ProxyVoteNotification) GetNotificationContent(mc *mailConfig) *Notificat
recipients: []recipientData{voteNoticeRecipient(mc)}, 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)},
}
}

@ -1,10 +1,10 @@
Dear Board, 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, Kind regards,
the voting system the voting system

@ -48,3 +48,11 @@ func parseSqlite3TimeStamp(timeStamp string) (*time.Time, error) {
return nil, fmt.Errorf("could not parse timestamp: %w", err) 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"
)

@ -108,11 +108,11 @@ var (
voteStatusDeclined = &VoteStatus{Label: "declined", ID: -1} voteStatusDeclined = &VoteStatus{Label: "declined", ID: -1}
voteStatusPending = &VoteStatus{Label: "pending", ID: 0} voteStatusPending = &VoteStatus{Label: "pending", ID: 0}
voteStatusApproved = &VoteStatus{Label: "approved", ID: 1} voteStatusApproved = &VoteStatus{Label: "approved", ID: 1}
voteStatusWithdrawn = &VoteStatus{Label: "withdrawn", ID: -2} VoteStatusWithdrawn = &VoteStatus{Label: "withdrawn", ID: -2}
) )
func VoteStatusFromInt(id int64) (*VoteStatus, error) { 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 { if int64(vs.ID) == id {
return vs, nil 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) { func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*Motion, error) {
tx, err := m.DB.BeginTxx(ctx, nil) tx, err := m.DB.BeginTxx(ctx, nil)
if err != 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) { 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() { for rows.Next() {
decision := &Motion{} decision := &Motion{}
if err = rows.StructScan(decision); err != nil { 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 { 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 { 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 return results, nil
@ -373,7 +373,7 @@ func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion)
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("could not execute update query: %w", err) return nil, fmt.Errorf(errCouldNotExecuteQuery, err)
} }
affectedRows, err := result.RowsAffected() affectedRows, err := result.RowsAffected()
@ -393,8 +393,38 @@ func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion)
return d, nil return d, nil
} }
func (m *MotionModel) UnVotedDecisionsForVoter(_ context.Context, _ *User) ([]*Motion, error) { func (m *MotionModel) UnVotedDecisionsForVoter(ctx context.Context, voter *User) ([]*Motion, error) {
panic("not implemented") 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) { 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 var decision Motion
if err = rows.Err(); err != nil { 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 { 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) 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...) rows, err := m.DB.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return fmt.Errorf("could not execute query: %w", err) return fmt.Errorf(errCouldNotExecuteQuery, err)
} }
defer func(rows *sql.Rows) { defer func(rows *sql.Rows) {
@ -609,7 +639,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*Motion) err
for rows.Next() { for rows.Next() {
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return fmt.Errorf("could not fetch row: %w", err) return fmt.Errorf(errCouldNotFetchRow, err)
} }
var ( var (
@ -620,7 +650,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*Motion) err
err = rows.Scan(&decisionID, &vote, &count) err = rows.Scan(&decisionID, &vote, &count)
if err != nil { if err != nil {
return fmt.Errorf("could not scan row: %w", err) return fmt.Errorf(errCouldNotScanResult, err)
} }
switch { switch {
@ -826,7 +856,7 @@ ORDER BY voters.name`,
var vote Vote var vote Vote
if err := rows.StructScan(&vote); err != nil { 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) md.Votes = append(md.Votes, &vote)
@ -858,7 +888,7 @@ func (m *MotionModel) Update(
) error { ) error {
tx, err := m.DB.BeginTxx(ctx, nil) tx, err := m.DB.BeginTxx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("could not start transaction: %w", err) return fmt.Errorf(errCouldNotStartTransaction, err)
} }
defer func(tx *sqlx.Tx) { defer func(tx *sqlx.Tx) {
@ -873,7 +903,7 @@ func (m *MotionModel) Update(
var motion Motion var motion Motion
if err := row.StructScan(&motion); err != nil { if err := row.StructScan(&motion); err != nil {
return fmt.Errorf("could not scan row: %w", err) return fmt.Errorf(errCouldNotScanResult, err)
} }
updateFn(&motion) updateFn(&motion)
@ -887,7 +917,8 @@ SET title=:title,
content=:content, content=:content,
votetype=:votetype, votetype=:votetype,
due=:due, due=:due,
modified=:modified modified=:modified,
status=:status
WHERE id = :id`, WHERE id = :id`,
motion, motion,
) )
@ -896,7 +927,7 @@ WHERE id = :id`,
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return fmt.Errorf("could not commit transaction: %w", err) return fmt.Errorf(errCouldNotCommitTransaction, err)
} }
return nil return nil
@ -914,7 +945,7 @@ type Vote struct {
func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error { func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error {
tx, err := m.DB.BeginTxx(ctx, nil) tx, err := m.DB.BeginTxx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("could not start transaction: %w", err) return fmt.Errorf(errCouldNotStartTransaction, err)
} }
defer func(tx *sqlx.Tx) { 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) row := tx.QueryRowxContext(ctx, `SELECT * FROM votes WHERE voter=? AND decision=?`, userID, motionID)
if err := row.Err(); err != nil { 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} vote := Vote{UserID: userID, MotionID: motionID}
@ -950,7 +981,7 @@ WHERE decision = :decision
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return fmt.Errorf("could not commit transaction: %w", err) return fmt.Errorf(errCouldNotCommitTransaction, err)
} }
return nil return nil

@ -218,7 +218,7 @@ func (m *UserModel) CreateUser(ctx context.Context, name string, reminder string
res, err := tx.ExecContext( res, err := tx.ExecContext(
ctx, ctx,
`INSERT INTO voters (name) VALUES (?)`, `INSERT INTO voters (name, enabled) VALUES (?, 0)`,
name) name)
if err != nil { if err != nil {
return 0, fmt.Errorf("could not insert user: %w", err) return 0, fmt.Errorf("could not insert user: %w", err)

@ -0,0 +1,20 @@
{{ define "title" }}Withdraw Motion {{ .Motion.Tag }}{{ end }}
{{ define "main" }}
<div class="ui form segment">
{{ template "motion_display" .Motion }}
<form action="/motions/{{ .Motion.Tag }}/withdraw" method="post">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<div class="ui negative message">
<div class="header">
Withdraw motion?
</div>
<p>Do you want to withdraw motion <strong>{{ .Motion.Tag }}: {{ .Motion.Title }}</strong>?</p>
</div>
<button class="ui negative labeled icon button" type="submit">
<i class="trash icon"></i> Withdraw
</button>
<a href="/motions/" class="ui button">Cancel</a>
</form>
</div>
{{ end }}
Loading…
Cancel
Save