summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Dittberner <jandd@cacert.org>2017-04-19 21:35:08 +0200
committerJan Dittberner <jan@dittberner.info>2017-04-22 00:12:38 +0200
commit0ce9ad6dcc01375cb3f2d57f7bbdf701e605bea1 (patch)
tree8de15dd825d80e025764614f6324343771b8231a
parentbc194e8943bdec58ef462e393ef2a8a59d928718 (diff)
downloadcacert-boardvoting-0ce9ad6dcc01375cb3f2d57f7bbdf701e605bea1.tar.gz
cacert-boardvoting-0ce9ad6dcc01375cb3f2d57f7bbdf701e605bea1.tar.xz
cacert-boardvoting-0ce9ad6dcc01375cb3f2d57f7bbdf701e605bea1.zip
Implement withdraw motion
-rw-r--r--actions.go36
-rw-r--r--boardvoting.go117
-rw-r--r--config.yaml.example4
-rw-r--r--forms.go4
-rw-r--r--models.go42
-rw-r--r--templates/withdraw_motion_form.html22
-rw-r--r--templates/withdraw_motion_mail.txt (renamed from templates/withdraw_mail.txt)4
7 files changed, 166 insertions, 63 deletions
diff --git a/actions.go b/actions.go
index a8e78fb..19e9bf3 100644
--- a/actions.go
+++ b/actions.go
@@ -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
}
diff --git a/boardvoting.go b/boardvoting.go
index efa9ecc..187a867 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -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)
+ }
}
diff --git a/config.yaml.example b/config.yaml.example
index f5da83d..f971540 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -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 \ No newline at end of file
+mail_server:
+ host: localhost
+ port: 25 \ No newline at end of file
diff --git a/forms.go b/forms.go
index 503e7e5..b7b7225 100644
--- a/forms.go
+++ b/forms.go
@@ -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
}
diff --git a/models.go b/models.go
index 8194dc9..d6eee15 100644
--- a/models.go
+++ b/models.go
@@ -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
}
diff --git a/templates/withdraw_motion_form.html b/templates/withdraw_motion_form.html
new file mode 100644
index 0000000..809ce28
--- /dev/null
+++ b/templates/withdraw_motion_form.html
@@ -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" . }}
diff --git a/templates/withdraw_mail.txt b/templates/withdraw_motion_mail.txt
index 4ccf4de..fb3ed87 100644
--- a/templates/withdraw_mail.txt
+++ b/templates/withdraw_motion_mail.txt
@@ -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: