Implement withdraw motion

This commit is contained in:
Jan Dittberner 2017-04-19 21:35:08 +02:00 committed by Jan Dittberner
parent bc194e8943
commit 0ce9ad6dcc
7 changed files with 166 additions and 63 deletions

View file

@ -4,9 +4,8 @@ import (
"bytes"
"fmt"
"github.com/Masterminds/sprig"
"os"
"text/template"
"gopkg.in/gomail.v2"
"text/template"
)
type templateBody string
@ -89,7 +88,7 @@ func UpdateMotion(decision *Decision, voter *Voter) (err error) {
m := gomail.NewMessage()
m.SetHeader("From", config.NoticeSenderAddress)
m.SetHeader("To", config.BoardMailAddress)
m.SetHeader("Subject", fmt.Sprintf("%s - %s", decision.Tag, decision.Title))
m.SetHeader("Subject", fmt.Sprintf("Re: %s - %s", decision.Tag, decision.Title))
m.SetHeader("References", fmt.Sprintf("<%s>", decision.Tag))
m.SetBody("text/plain", mailText.String())
@ -102,24 +101,31 @@ func UpdateMotion(decision *Decision, voter *Voter) (err error) {
}
func WithdrawMotion(decision *Decision, voter *Voter) (err error) {
// load template, fill name, tag, title, content
err = decision.UpdateStatus()
type mailContext struct {
*Decision
Name string
Sender string
Recipient string
Name string
}
context := mailContext{decision, voter.Name, config.NoticeSenderAddress, config.BoardMailAddress}
context := mailContext{decision, voter.Name}
// fill withdraw_mail.txt
t, err := template.New("withdraw_mail.txt").Funcs(
sprig.GenericFuncMap()).ParseFiles("templates/withdraw_mail.txt")
mailText, err := buildMail("withdraw_motion_mail.txt", context)
if err != nil {
logger.Fatal(err)
logger.Println("Error", err)
return
}
m := gomail.NewMessage()
m.SetHeader("From", config.NoticeSenderAddress)
m.SetHeader("To", config.BoardMailAddress)
m.SetHeader("Subject", fmt.Sprintf("Re: %s - %s - withdrawn", decision.Tag, decision.Title))
m.SetHeader("References", fmt.Sprintf("<%s>", decision.Tag))
m.SetBody("text/plain", mailText.String())
d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
if err := d.DialAndSend(m); err != nil {
logger.Println("Error sending mail", err)
}
// TODO: send mail
t.Execute(os.Stdout, context)
// TODO: implement call decision.Close()
return
}

View file

@ -18,6 +18,7 @@ import (
"os"
"strconv"
"strings"
"time"
)
var logger *log.Logger
@ -216,10 +217,6 @@ func (authenticationRequiredHandler) NeedsAuth() bool {
return true
}
type withDrawMotionAction struct {
authenticationRequiredHandler
}
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
voter, ok = r.Context().Value(ctxVoter).(*Voter)
return
@ -230,37 +227,71 @@ func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok b
return
}
func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
voter, voter_ok := getVoterFromRequest(r)
decision, decision_ok := getDecisionFromRequest(r)
type FlashMessageAction struct{}
if !voter_ok || !decision_ok || decision.Status != voteStatusPending {
func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) (err error) {
session, err := store.Get(r, sessionCookieName)
if err != nil {
logger.Println("ERROR getting session:", err)
return
}
session.AddFlash(message, tags...)
session.Save(r, w)
if err != nil {
logger.Println("ERROR saving session:", err)
return
}
return
}
type withDrawMotionAction struct {
FlashMessageAction
authenticationRequiredHandler
}
func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html"}
var templateContext struct {
PageTitle string
Decision *DecisionForDisplay
Flashes interface{}
}
switch r.Method {
case http.MethodPost:
if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil {
log.Println("could not parse confirm parameter:", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
} else if confirm {
WithdrawMotion(&decision.Decision, voter)
} else {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
decision.Status = voteStatusWithdrawn
decision.Modified = time.Now().UTC()
if err := WithdrawMotion(&decision.Decision, voter); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if err := a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag)); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
return
default:
fmt.Fprintln(w, "Withdraw motion", decision.Tag)
templateContext.Decision = decision
renderTemplate(w, templates, templateContext)
}
}
type editMotionAction struct {
authenticationRequiredHandler
type newMotionHandler struct {
FlashMessageAction
}
func newMotionHandler(w http.ResponseWriter, r *http.Request) {
func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
@ -287,17 +318,15 @@ func newMotionHandler(w http.ResponseWriter, r *http.Request) {
templateContext.Form = form
renderTemplate(w, templates, templateContext)
} else {
data.Proposed = time.Now().UTC()
if err := CreateMotion(data, voter); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := store.Get(r, sessionCookieName)
if err != nil {
if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session.AddFlash("The motion has been proposed!")
session.Save(r, w)
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
@ -312,7 +341,12 @@ func newMotionHandler(w http.ResponseWriter, r *http.Request) {
}
}
func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
type editMotionAction struct {
FlashMessageAction
authenticationRequiredHandler
}
func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
@ -321,6 +355,7 @@ func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return
}
templates := []string{"edit_motion_form.html", "header.html", "footer.html"}
var templateContext struct {
@ -344,21 +379,18 @@ func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
templateContext.Form = form
renderTemplate(w, templates, templateContext)
} else {
data.Modified = time.Now().UTC()
if err := UpdateMotion(data, voter); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := store.Get(r, sessionCookieName)
if err != nil {
if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session.AddFlash("The motion has been modified!")
session.Save(r, w)
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
return
default:
templateContext.Voter = voter
@ -382,14 +414,20 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
subURL := r.URL.Path
var motionActionMap = map[string]motionActionHandler{
"withdraw": withDrawMotionAction{},
"edit": editMotionAction{},
"withdraw": &withDrawMotionAction{},
"edit": &editMotionAction{},
}
switch {
case subURL == "":
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
return
case subURL == "/newmotion/":
handler := &newMotionHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
handler.Handle)
return
case strings.Count(subURL, "/") == 1:
parts := strings.Split(subURL, "/")
motionTag := parts[0]
@ -427,7 +465,7 @@ type Config struct {
BaseURL string `yaml:"base_url"`
MailServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Port int `yaml:"port"`
} `yaml:"mail_server"`
}
@ -465,9 +503,7 @@ func main() {
defer db.Close()
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), newMotionHandler)
})
http.Handle("/newmotion/", motionsHandler{})
http.Handle("/static/", http.FileServer(http.Dir(".")))
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
@ -495,7 +531,18 @@ func main() {
logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
errs := make(chan error, 1)
go func() {
if err := http.ListenAndServe(":8080", http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
errs <- err
}
close(errs)
}()
if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
logger.Fatal("ListenAndServerTLS: ", err)
}
if err := <-errs; err != nil {
logger.Fatal("ListenAndServe: ", err)
}
}

View file

@ -7,4 +7,6 @@ server_certificate: server.crt
server_key: server.key
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
base_url: https://motions.cacert.org
mail_server: localhost:smtp
mail_server:
host: localhost
port: 25

View file

@ -50,8 +50,6 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) {
data.Due = time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
}
data.Proposed = time.Now().UTC()
return len(f.Errors) == 0, data
}
@ -92,7 +90,5 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
data.Due = time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
}
data.Modified = time.Now().UTC()
return len(f.Errors) == 0, data
}

View file

@ -20,6 +20,7 @@ const (
sqlCountOlderThanUnvotedDecision
sqlCreateDecision
sqlUpdateDecision
sqlUpdateDecisionStatus
)
var sqlStatements = map[sqlKey]string{
@ -98,6 +99,10 @@ UPDATE decisions
SET proponent=:proponent, title=:title, content=:content,
votetype=:votetype, due=:due, modified=:modified
WHERE id=:id`,
sqlUpdateDecisionStatus: `
UPDATE decisions
SET status=:status, modified=:modified WHERE id=:id
`,
}
var db *sqlx.DB
@ -417,6 +422,22 @@ func (d *Decision) Create() (err error) {
return
}
func (d *Decision) LoadWithId() (err error) {
getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
if err != nil {
logger.Println("Error preparing statement:", err)
return
}
defer getDecisionStmt.Close()
err = getDecisionStmt.Get(d, d.Id)
if err != nil {
logger.Println("Error loading updated motion:", err)
}
return
}
func (d *Decision) Update() (err error) {
updateDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecision])
if err != nil {
@ -438,18 +459,31 @@ func (d *Decision) Update() (err error) {
logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
}
getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
err = d.LoadWithId()
return
}
func (d *Decision) UpdateStatus() (err error) {
updateStatusStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus])
if err != nil {
logger.Println("Error preparing statement:", err)
return
}
defer getDecisionStmt.Close()
defer updateStatusStmt.Close()
err = getDecisionStmt.Get(d, d.Id)
result, err := updateStatusStmt.Exec(d)
if err != nil {
logger.Println("Error loading updated motion:", err)
logger.Println("Error setting motion status:", err)
return
}
affectedRows, err := result.RowsAffected()
if err != nil {
logger.Print("Problem determining the affected rows")
} else if affectedRows != 1 {
logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
}
err = d.LoadWithId()
return
}

View file

@ -0,0 +1,22 @@
{{ template "header" . }}
<a href="/motions/">Show all votes</a>
<table class="list">
<thead>
<tr>
<th>Status</th>
<th>Motion</th>
</tr>
</thead>
<tbody>
<tr>
{{ with .Decision }}
{{ template "motion_fragment" .}}
{{ end}}
</tr>
</tbody>
</table>
<form action="/motions/{{ .Decision.Tag }}/withdraw" method="post">
<input type="submit" value="Withdraw" />
</form>
{{ template "footer" . }}

View file

@ -1,7 +1,3 @@
From: {{ .Sender }}
To: {{ .Recipient }}
Subject: Re: {{ .Tag }} - {{ .Title }} - withdrawn
Dear Board,
{{ .Name }} has withdrawn the motion {{ .Tag }} that was as follows: