Implement withdraw motion
This commit is contained in:
parent
bc194e8943
commit
0ce9ad6dcc
7 changed files with 166 additions and 63 deletions
36
actions.go
36
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
|
||||
}
|
||||
|
|
117
boardvoting.go
117
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
4
forms.go
4
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
|
||||
}
|
||||
|
|
42
models.go
42
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
|
||||
}
|
||||
|
||||
|
|
22
templates/withdraw_motion_form.html
Normal file
22
templates/withdraw_motion_form.html
Normal 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" . }}
|
|
@ -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:
|
Loading…
Reference in a new issue