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) {
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
}

@ -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)

@ -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)},
}
}

@ -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

@ -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"
)

@ -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

@ -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)

@ -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