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"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Masterminds/sprig"
|
"github.com/Masterminds/sprig"
|
||||||
"os"
|
|
||||||
"text/template"
|
|
||||||
"gopkg.in/gomail.v2"
|
"gopkg.in/gomail.v2"
|
||||||
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
type templateBody string
|
type templateBody string
|
||||||
|
@ -89,7 +88,7 @@ func UpdateMotion(decision *Decision, voter *Voter) (err error) {
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", config.NoticeSenderAddress)
|
m.SetHeader("From", config.NoticeSenderAddress)
|
||||||
m.SetHeader("To", config.BoardMailAddress)
|
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.SetHeader("References", fmt.Sprintf("<%s>", decision.Tag))
|
||||||
m.SetBody("text/plain", mailText.String())
|
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) {
|
func WithdrawMotion(decision *Decision, voter *Voter) (err error) {
|
||||||
// load template, fill name, tag, title, content
|
err = decision.UpdateStatus()
|
||||||
|
|
||||||
type mailContext struct {
|
type mailContext struct {
|
||||||
*Decision
|
*Decision
|
||||||
Name string
|
Name string
|
||||||
Sender string
|
|
||||||
Recipient string
|
|
||||||
}
|
}
|
||||||
context := mailContext{decision, voter.Name, config.NoticeSenderAddress, config.BoardMailAddress}
|
context := mailContext{decision, voter.Name}
|
||||||
|
|
||||||
// fill withdraw_mail.txt
|
mailText, err := buildMail("withdraw_motion_mail.txt", context)
|
||||||
t, err := template.New("withdraw_mail.txt").Funcs(
|
|
||||||
sprig.GenericFuncMap()).ParseFiles("templates/withdraw_mail.txt")
|
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
117
boardvoting.go
117
boardvoting.go
|
@ -18,6 +18,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
|
@ -216,10 +217,6 @@ func (authenticationRequiredHandler) NeedsAuth() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type withDrawMotionAction struct {
|
|
||||||
authenticationRequiredHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
|
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
|
||||||
voter, ok = r.Context().Value(ctxVoter).(*Voter)
|
voter, ok = r.Context().Value(ctxVoter).(*Voter)
|
||||||
return
|
return
|
||||||
|
@ -230,37 +227,71 @@ func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok b
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
|
type FlashMessageAction struct{}
|
||||||
voter, voter_ok := getVoterFromRequest(r)
|
|
||||||
decision, decision_ok := getDecisionFromRequest(r)
|
|
||||||
|
|
||||||
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)
|
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||||
return
|
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 {
|
switch r.Method {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil {
|
decision.Status = voteStatusWithdrawn
|
||||||
log.Println("could not parse confirm parameter:", err)
|
decision.Modified = time.Now().UTC()
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
if err := WithdrawMotion(&decision.Decision, voter); err != nil {
|
||||||
} else if confirm {
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
WithdrawMotion(&decision.Decision, voter)
|
return
|
||||||
} else {
|
}
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
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)
|
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
fmt.Fprintln(w, "Withdraw motion", decision.Tag)
|
templateContext.Decision = decision
|
||||||
|
renderTemplate(w, templates, templateContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type editMotionAction struct {
|
type newMotionHandler struct {
|
||||||
authenticationRequiredHandler
|
FlashMessageAction
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMotionHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
voter, ok := getVoterFromRequest(r)
|
voter, ok := getVoterFromRequest(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||||
|
@ -287,17 +318,15 @@ func newMotionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
templateContext.Form = form
|
templateContext.Form = form
|
||||||
renderTemplate(w, templates, templateContext)
|
renderTemplate(w, templates, templateContext)
|
||||||
} else {
|
} else {
|
||||||
|
data.Proposed = time.Now().UTC()
|
||||||
if err := CreateMotion(data, voter); err != nil {
|
if err := CreateMotion(data, voter); err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, err := store.Get(r, sessionCookieName)
|
if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.AddFlash("The motion has been proposed!")
|
|
||||||
session.Save(r, w)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
|
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)
|
decision, ok := getDecisionFromRequest(r)
|
||||||
if !ok || decision.Status != voteStatusPending {
|
if !ok || decision.Status != voteStatusPending {
|
||||||
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
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)
|
voter, ok := getVoterFromRequest(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
templates := []string{"edit_motion_form.html", "header.html", "footer.html"}
|
templates := []string{"edit_motion_form.html", "header.html", "footer.html"}
|
||||||
var templateContext struct {
|
var templateContext struct {
|
||||||
|
@ -344,21 +379,18 @@ func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
templateContext.Form = form
|
templateContext.Form = form
|
||||||
renderTemplate(w, templates, templateContext)
|
renderTemplate(w, templates, templateContext)
|
||||||
} else {
|
} else {
|
||||||
|
data.Modified = time.Now().UTC()
|
||||||
if err := UpdateMotion(data, voter); err != nil {
|
if err := UpdateMotion(data, voter); err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, err := store.Get(r, sessionCookieName)
|
if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.AddFlash("The motion has been modified!")
|
|
||||||
session.Save(r, w)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
|
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
templateContext.Voter = voter
|
templateContext.Voter = voter
|
||||||
|
@ -382,14 +414,20 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
subURL := r.URL.Path
|
subURL := r.URL.Path
|
||||||
|
|
||||||
var motionActionMap = map[string]motionActionHandler{
|
var motionActionMap = map[string]motionActionHandler{
|
||||||
"withdraw": withDrawMotionAction{},
|
"withdraw": &withDrawMotionAction{},
|
||||||
"edit": editMotionAction{},
|
"edit": &editMotionAction{},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case subURL == "":
|
case subURL == "":
|
||||||
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
|
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
|
||||||
return
|
return
|
||||||
|
case subURL == "/newmotion/":
|
||||||
|
handler := &newMotionHandler{}
|
||||||
|
authenticateRequest(
|
||||||
|
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
|
||||||
|
handler.Handle)
|
||||||
|
return
|
||||||
case strings.Count(subURL, "/") == 1:
|
case strings.Count(subURL, "/") == 1:
|
||||||
parts := strings.Split(subURL, "/")
|
parts := strings.Split(subURL, "/")
|
||||||
motionTag := parts[0]
|
motionTag := parts[0]
|
||||||
|
@ -427,7 +465,7 @@ type Config struct {
|
||||||
BaseURL string `yaml:"base_url"`
|
BaseURL string `yaml:"base_url"`
|
||||||
MailServer struct {
|
MailServer struct {
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
} `yaml:"mail_server"`
|
} `yaml:"mail_server"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,9 +503,7 @@ func main() {
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
|
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
|
||||||
http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
|
http.Handle("/newmotion/", motionsHandler{})
|
||||||
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), newMotionHandler)
|
|
||||||
})
|
|
||||||
http.Handle("/static/", http.FileServer(http.Dir(".")))
|
http.Handle("/static/", http.FileServer(http.Dir(".")))
|
||||||
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
|
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
|
||||||
|
|
||||||
|
@ -495,7 +531,18 @@ func main() {
|
||||||
|
|
||||||
logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
|
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 {
|
if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
|
||||||
logger.Fatal("ListenAndServerTLS: ", err)
|
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
|
server_key: server.key
|
||||||
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
|
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
|
||||||
base_url: https://motions.cacert.org
|
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.Due = time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Proposed = time.Now().UTC()
|
|
||||||
|
|
||||||
return len(f.Errors) == 0, data
|
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.Due = time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Modified = time.Now().UTC()
|
|
||||||
|
|
||||||
return len(f.Errors) == 0, data
|
return len(f.Errors) == 0, data
|
||||||
}
|
}
|
||||||
|
|
42
models.go
42
models.go
|
@ -20,6 +20,7 @@ const (
|
||||||
sqlCountOlderThanUnvotedDecision
|
sqlCountOlderThanUnvotedDecision
|
||||||
sqlCreateDecision
|
sqlCreateDecision
|
||||||
sqlUpdateDecision
|
sqlUpdateDecision
|
||||||
|
sqlUpdateDecisionStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
var sqlStatements = map[sqlKey]string{
|
var sqlStatements = map[sqlKey]string{
|
||||||
|
@ -98,6 +99,10 @@ UPDATE decisions
|
||||||
SET proponent=:proponent, title=:title, content=:content,
|
SET proponent=:proponent, title=:title, content=:content,
|
||||||
votetype=:votetype, due=:due, modified=:modified
|
votetype=:votetype, due=:due, modified=:modified
|
||||||
WHERE id=:id`,
|
WHERE id=:id`,
|
||||||
|
sqlUpdateDecisionStatus: `
|
||||||
|
UPDATE decisions
|
||||||
|
SET status=:status, modified=:modified WHERE id=:id
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var db *sqlx.DB
|
var db *sqlx.DB
|
||||||
|
@ -417,6 +422,22 @@ func (d *Decision) Create() (err error) {
|
||||||
return
|
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) {
|
func (d *Decision) Update() (err error) {
|
||||||
updateDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecision])
|
updateDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecision])
|
||||||
if err != nil {
|
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)
|
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 {
|
if err != nil {
|
||||||
logger.Println("Error preparing statement:", err)
|
logger.Println("Error preparing statement:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer getDecisionStmt.Close()
|
defer updateStatusStmt.Close()
|
||||||
|
|
||||||
err = getDecisionStmt.Get(d, d.Id)
|
result, err := updateStatusStmt.Exec(d)
|
||||||
if err != nil {
|
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
|
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,
|
Dear Board,
|
||||||
|
|
||||||
{{ .Name }} has withdrawn the motion {{ .Tag }} that was as follows:
|
{{ .Name }} has withdrawn the motion {{ .Tag }} that was as follows:
|
Loading…
Reference in a new issue