Implement proxy voting
authorJan Dittberner <jan@dittberner.info>
Fri, 21 Apr 2017 00:25:49 +0000 (02:25 +0200)
committerJan Dittberner <jan@dittberner.info>
Fri, 21 Apr 2017 22:14:08 +0000 (00:14 +0200)
boardvoting.go
forms.go
jobs.go
models.go
notifications.go
proxy.php [deleted file]
templates/proxy_vote_form.html [new file with mode: 0644]
templates/proxy_vote_mail.txt [new file with mode: 0644]

index 152d969..d62b80e 100644 (file)
@@ -1,10 +1,12 @@
 package main
 
 import (
 package main
 
 import (
+       "bytes"
        "context"
        "crypto/tls"
        "crypto/x509"
        "encoding/base64"
        "context"
        "crypto/tls"
        "crypto/x509"
        "encoding/base64"
+       "encoding/pem"
        "fmt"
        "github.com/Masterminds/sprig"
        "github.com/gorilla/sessions"
        "fmt"
        "github.com/Masterminds/sprig"
        "github.com/gorilla/sessions"
@@ -50,6 +52,7 @@ const (
        ctxNeedsAuth contextKey = iota
        ctxVoter
        ctxDecision
        ctxNeedsAuth contextKey = iota
        ctxVoter
        ctxDecision
+       ctxVote
 )
 
 func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
 )
 
 func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
@@ -227,6 +230,11 @@ func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok b
        return
 }
 
        return
 }
 
+func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
+       vote, ok = r.Context().Value(ctxVote).(VoteChoice)
+       return
+}
+
 type FlashMessageAction struct{}
 
 func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) (err error) {
 type FlashMessageAction struct{}
 
 func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) (err error) {
@@ -277,7 +285,7 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                        return
                }
 
                        return
                }
 
-               notifyMail <- &NotificationWithDrawMotion{decision: decision.Decision, voter: *voter}
+               NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)
 
                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)
 
                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)
@@ -330,7 +338,7 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
                                return
                        }
 
                                return
                        }
 
-                       notifyMail <- &NotificationCreateMotion{decision: *data, voter: *voter}
+                       NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}
 
                        if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 
                        if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -395,7 +403,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                                return
                        }
 
                                return
                        }
 
-                       notifyMail <- &NotificationUpdateMotion{decision: *data, voter: *voter}
+                       NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)
 
                        if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 
                        if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -467,17 +475,164 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        }
 }
 
        }
 }
 
+type voteHandler struct {
+       FlashMessageAction
+       authenticationRequiredHandler
+}
+
+func (h *voteHandler) Handle(w http.ResponseWriter, r *http.Request) {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       voter, ok := getVoterFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       vote, ok := getVoteFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       fmt.Fprintln(w, "to be implemented")
+       fmt.Fprintln(w, "Decision:", decision)
+       fmt.Fprintln(w, "Voter:", voter)
+       fmt.Fprintln(w, "Vote:", vote)
+}
+
+type proxyVoteHandler struct {
+       FlashMessageAction
+       authenticationRequiredHandler
+}
+
+func getPEMClientCert(r *http.Request) string {
+       clientCertPEM := bytes.NewBufferString("")
+       pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: r.TLS.PeerCertificates[0].Raw})
+       return clientCertPEM.String()
+}
+
+func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       proxy, ok := getVoterFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+               return
+       }
+       templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
+       var templateContext struct {
+               Form      ProxyVoteForm
+               Decision  *DecisionForDisplay
+               Voters    *[]Voter
+               PageTitle string
+               Flashes   interface{}
+       }
+       switch r.Method {
+       case http.MethodPost:
+               form := ProxyVoteForm{
+                       Voter:         r.FormValue("Voter"),
+                       Vote:          r.FormValue("Vote"),
+                       Justification: r.FormValue("Justification"),
+               }
+
+               if valid, voter, data, justification := form.Validate(); !valid {
+                       templateContext.Form = form
+                       templateContext.Decision = decision
+                       if voters, err := GetVotersForProxy(proxy, &decision.Decision); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       } else {
+                               templateContext.Voters = voters
+                       }
+                       renderTemplate(w, templates, templateContext)
+               } else {
+                       data.DecisionId = decision.Id
+                       data.Voted = time.Now().UTC()
+                       data.Notes = fmt.Sprintf(
+                               "Proxy-Vote by %s\n\n%s\n\n%s",
+                               proxy.Name, justification, getPEMClientCert(r))
+
+                       if err := data.Save(); err != nil {
+                               logger.Println("Error saving vote:", err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+
+                       NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
+
+                       if err := h.AddFlash(w, r, "The vote has been registered."); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+
+                       http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
+               }
+               return
+       default:
+               templateContext.Form = ProxyVoteForm{}
+               templateContext.Decision = decision
+               if voters, err := GetVotersForProxy(proxy, &decision.Decision); err != nil {
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               } else {
+                       templateContext.Voters = voters
+               }
+               renderTemplate(w, templates, templateContext)
+       }
+}
+
+type decisionVoteHandler struct{}
+
+func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       if err := db.Ping(); err != nil {
+               logger.Fatal(err)
+       }
+
+       switch {
+       case strings.HasPrefix(r.URL.Path, "/proxy/"):
+               motionTag := r.URL.Path[len("/proxy/"):]
+               handler := &proxyVoteHandler{}
+               authenticateRequest(
+                       w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
+                       func(w http.ResponseWriter, r *http.Request) {
+                               singleDecisionHandler(w, r, motionTag, handler.Handle)
+                       })
+       case strings.HasPrefix(r.URL.Path, "/vote/"):
+               parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
+               motionTag := parts[0]
+               voteValue, ok := VoteValues[parts[1]]
+               if !ok {
+                       http.NotFound(w, r)
+                       return
+               }
+               handler := &voteHandler{}
+               authenticateRequest(
+                       w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
+                       func(w http.ResponseWriter, r *http.Request) {
+                               singleDecisionHandler(
+                                       w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
+                                       motionTag, handler.Handle)
+                       })
+               return
+       }
+}
+
 type Config struct {
 type Config struct {
-       BoardMailAddress      string `yaml:"board_mail_address"`
-       NoticeSenderAddress   string `yaml:"notice_sender_address"`
-       ReminderSenderAddress string `yaml:"reminder_sender_address"`
-       DatabaseFile          string `yaml:"database_file"`
-       ClientCACertificates  string `yaml:"client_ca_certificates"`
-       ServerCert            string `yaml:"server_certificate"`
-       ServerKey             string `yaml:"server_key"`
-       CookieSecret          string `yaml:"cookie_secret"`
-       BaseURL               string `yaml:"base_url"`
-       MailServer            struct {
+       BoardMailAddress          string `yaml:"board_mail_address"`
+       VoteNoticeAddress         string `yaml:"notice_sender_address"`
+       NotificationSenderAddress string `yaml:"reminder_sender_address"`
+       DatabaseFile              string `yaml:"database_file"`
+       ClientCACertificates      string `yaml:"client_ca_certificates"`
+       ServerCert                string `yaml:"server_certificate"`
+       ServerKey                 string `yaml:"server_key"`
+       CookieSecret              string `yaml:"cookie_secret"`
+       BaseURL                   string `yaml:"base_url"`
+       MailServer                struct {
                Host string `yaml:"host"`
                Port int    `yaml:"port"`
        } `yaml:"mail_server"`
                Host string `yaml:"host"`
                Port int    `yaml:"port"`
        } `yaml:"mail_server"`
@@ -516,8 +671,9 @@ func main() {
 
        defer db.Close()
 
 
        defer db.Close()
 
-       go MailNotifier()
-       defer CloseMailNotifier()
+       quitMailChannel := make(chan int)
+       go MailNotifier(quitMailChannel)
+       defer func() { quitMailChannel <- 1 }()
 
        quitChannel := make(chan int)
        go JobScheduler(quitChannel)
 
        quitChannel := make(chan int)
        go JobScheduler(quitChannel)
@@ -525,6 +681,8 @@ func main() {
 
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
        http.Handle("/newmotion/", motionsHandler{})
 
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
        http.Handle("/newmotion/", motionsHandler{})
+       http.Handle("/proxy/", &decisionVoteHandler{})
+       http.Handle("/vote/", &decisionVoteHandler{})
        http.Handle("/static/", http.FileServer(http.Dir(".")))
        http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
 
        http.Handle("/static/", http.FileServer(http.Dir(".")))
        http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
 
index b7b7225..e0d430a 100644 (file)
--- a/forms.go
+++ b/forms.go
@@ -92,3 +92,40 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
 
        return len(f.Errors) == 0, data
 }
 
        return len(f.Errors) == 0, data
 }
+
+type ProxyVoteForm struct {
+       Voter         string
+       Vote          string
+       Justification string
+       Errors        map[string]string
+}
+
+func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) {
+       f.Errors = make(map[string]string)
+
+       data := &Vote{}
+
+       var voter *Voter
+       if voterId, err := strconv.ParseInt(f.Voter, 10, 64); err != nil {
+               f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter", err)
+       } else if voter, err = GetVoterById(voterId); err != nil {
+               f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter", err)
+       } else {
+               data.VoterId = voter.Id
+       }
+
+       if vote, err := strconv.ParseInt(f.Vote, 10, 8); err != nil {
+               f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote", err)
+       } else if voteChoice, ok := VoteChoices[vote]; !ok {
+               f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote")
+       } else {
+               data.Vote = voteChoice
+       }
+
+       justification := strings.TrimSpace(f.Justification)
+       if len(justification) < 3 {
+               f.Errors["Justification"] = "Please enter at least 3 characters."
+       }
+
+       return len(f.Errors) == 0, voter, data, justification
+}
diff --git a/jobs.go b/jobs.go
index 3f15e2d..2535ef1 100644 (file)
--- a/jobs.go
+++ b/jobs.go
@@ -142,7 +142,7 @@ func (j *RemindVotersJob) Run() {
                        return
                }
                if len(*decisions) > 0 {
                        return
                }
                if len(*decisions) > 0 {
-                       voterMail <- &RemindVoterNotification{voter: voter, decisions: *decisions}
+                       NotifyMailChannel <- &RemindVoterNotification{voter: voter, decisions: *decisions}
                }
        }
 }
                }
        }
 }
index 5297cf2..dfc054d 100644 (file)
--- a/models.go
+++ b/models.go
@@ -26,6 +26,10 @@ const (
        sqlGetNextPendingDecisionDue
        sqlGetReminderVoters
        sqlFindUnvotedDecisionsForVoter
        sqlGetNextPendingDecisionDue
        sqlGetReminderVoters
        sqlFindUnvotedDecisionsForVoter
+       sqlGetEnabledVoterById
+       sqlCreateVote
+       sqlLoadVote
+       sqlGetVotersForProxy
 )
 
 var sqlStatements = map[sqlKey]string{
 )
 
 var sqlStatements = map[sqlKey]string{
@@ -67,6 +71,10 @@ SELECT voters.id, voters.name, voters.enabled, voters.reminder
 FROM   voters
 JOIN   emails ON voters.id=emails.voter
 WHERE  emails.address=$1 AND voters.enabled=1`,
 FROM   voters
 JOIN   emails ON voters.id=emails.voter
 WHERE  emails.address=$1 AND voters.enabled=1`,
+       sqlGetEnabledVoterById: `
+SELECT id, name, enabled, reminder
+FROM   voters
+WHERE  enabled=1 AND id=$1`,
        sqlCountOlderThanDecision: `
 SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
        sqlCountOlderThanUnvotedDecision: `
        sqlCountOlderThanDecision: `
 SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
        sqlCountOlderThanUnvotedDecision: `
@@ -95,14 +103,23 @@ FROM   decisions
 WHERE  decisions.status=0 AND :now > due`,
        sqlGetNextPendingDecisionDue: `
 SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
 WHERE  decisions.status=0 AND :now > due`,
        sqlGetNextPendingDecisionDue: `
 SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
+       sqlGetVotersForProxy: `
+SELECT id, name, reminder
+FROM   voters WHERE enabled=1 AND id != $1 AND id NOT IN (SELECT voter FROM votes WHERE decision=$2)`,
        sqlGetReminderVoters: `
 SELECT id, name, reminder FROM voters WHERE enabled=1 AND reminder!='' AND reminder IS NOT NULL`,
        sqlFindUnvotedDecisionsForVoter: `
 SELECT tag, title, votetype, due
 FROM   decisions
 WHERE  status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
        sqlGetReminderVoters: `
 SELECT id, name, reminder FROM voters WHERE enabled=1 AND reminder!='' AND reminder IS NOT NULL`,
        sqlFindUnvotedDecisionsForVoter: `
 SELECT tag, title, votetype, due
 FROM   decisions
 WHERE  status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
-ORDER BY due ASC
-`,
+ORDER BY due ASC`,
+       sqlCreateVote: `
+INSERT INTO votes (decision, voter, vote, voted, notes)
+VALUES (:decision, :voter, :vote, :voted, :notes)`,
+       sqlLoadVote: `
+SELECT decision, voter, vote, voted, notes
+FROM   votes
+WHERE  decision=$1 AND voter=$2`,
 }
 
 var db *sqlx.DB
 }
 
 var db *sqlx.DB
@@ -122,9 +139,9 @@ type VoteType uint8
 type VoteStatus int8
 
 type Decision struct {
 type VoteStatus int8
 
 type Decision struct {
-       Id          int
+       Id          int64
        Proposed    time.Time
        Proposed    time.Time
-       ProponentId int `db:"proponent"`
+       ProponentId int64 `db:"proponent"`
        Title       string
        Content     string
        Quorum      int
        Title       string
        Content     string
        Quorum      int
@@ -137,12 +154,12 @@ type Decision struct {
 }
 
 type Email struct {
 }
 
 type Email struct {
-       VoterId int `db:"voter"`
+       VoterId int64 `db:"voter"`
        Address string
 }
 
 type Voter struct {
        Address string
 }
 
 type Voter struct {
-       Id       int
+       Id       int64
        Name     string
        Enabled  bool
        Reminder string // reminder email address
        Name     string
        Enabled  bool
        Reminder string // reminder email address
@@ -150,14 +167,6 @@ type Voter struct {
 
 type VoteChoice int
 
 
 type VoteChoice int
 
-type Vote struct {
-       DecisionId int `db:"decision"`
-       VoterId    int `db:"voter"`
-       Vote       VoteChoice
-       Voted      time.Time
-       Notes      string
-}
-
 const (
        voteAye     = 1
        voteNaye    = -1
 const (
        voteAye     = 1
        voteNaye    = -1
@@ -202,6 +211,18 @@ func (v VoteChoice) String() string {
        }
 }
 
        }
 }
 
+var VoteValues = map[string]VoteChoice{
+       "aye":     voteAye,
+       "naye":    voteNaye,
+       "abstain": voteAbstain,
+}
+
+var VoteChoices = map[int64]VoteChoice{
+       1:  voteAye,
+       0:  voteAbstain,
+       -1: voteNaye,
+}
+
 const (
        voteStatusDeclined  = -1
        voteStatusPending   = 0
 const (
        voteStatusDeclined  = -1
        voteStatusPending   = 0
@@ -224,6 +245,43 @@ func (v VoteStatus) String() string {
        }
 }
 
        }
 }
 
+type Vote struct {
+       DecisionId int64 `db:"decision"`
+       VoterId    int64 `db:"voter"`
+       Vote       VoteChoice
+       Voted      time.Time
+       Notes      string
+}
+
+func (v *Vote) Save() (err error) {
+       insertVoteStmt, err := db.PrepareNamed(sqlStatements[sqlCreateVote])
+       if err != nil {
+               logger.Println("Error preparing statement:", err)
+               return
+       }
+       defer insertVoteStmt.Close()
+
+       _, err = insertVoteStmt.Exec(v)
+       if err != nil {
+               logger.Println("Error saving vote:", err)
+               return
+       }
+
+       getVoteStmt, err := db.Preparex(sqlStatements[sqlLoadVote])
+       if err != nil {
+               logger.Println("Error preparing statement:", err)
+               return
+       }
+       defer getVoteStmt.Close()
+
+       err = getVoteStmt.Get(v, v.DecisionId, v.VoterId)
+       if err != nil {
+               logger.Println("Error getting inserted vote:", err)
+       }
+
+       return
+}
+
 type VoteSums struct {
        Ayes     int
        Nayes    int
 type VoteSums struct {
        Ayes     int
        Nayes    int
@@ -569,7 +627,7 @@ func (d *Decision) Close() (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)
        }
 
-       notifyMail <- &NotificationClosedDecision{decision: *d, voteSums: *voteSums}
+       NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums)
 
        return
 }
 
        return
 }
@@ -671,3 +729,39 @@ func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err erro
 
        return
 }
 
        return
 }
+
+func GetVoterById(id int64) (voter *Voter, err error) {
+       getVoterByIdStmt, err := db.Preparex(sqlStatements[sqlGetEnabledVoterById])
+       if err != nil {
+               logger.Println("Error preparing statement:", err)
+               return
+       }
+       defer getVoterByIdStmt.Close()
+
+       voter = &Voter{}
+       if err = getVoterByIdStmt.Get(voter, id); err != nil {
+               logger.Println("Error getting voter:", err)
+               return
+       }
+
+       return
+}
+
+func GetVotersForProxy(proxy *Voter, decision *Decision) (voters *[]Voter, err error) {
+       getVotersForProxyStmt, err := db.Preparex(sqlStatements[sqlGetVotersForProxy])
+       if err != nil {
+               logger.Println("Error preparing statement:", err)
+               return
+       }
+       defer getVotersForProxyStmt.Close()
+
+       votersSlice := make([]Voter, 0)
+
+       if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id, decision.Id); err != nil {
+               logger.Println("Error getting voters for proxy:", err)
+               return
+       }
+       voters = &votersSlice
+
+       return
+}
index d4b9715..499a689 100644 (file)
@@ -13,28 +13,16 @@ type NotificationMail interface {
        GetTemplate() string
        GetSubject() string
        GetHeaders() map[string]string
        GetTemplate() string
        GetSubject() string
        GetHeaders() map[string]string
-}
-
-type VoterMail interface {
-       GetData() interface{}
-       GetTemplate() string
-       GetSubject() string
        GetRecipient() (string, string)
 }
 
        GetRecipient() (string, string)
 }
 
-var notifyMail = make(chan NotificationMail, 1)
-var voterMail = make(chan VoterMail, 1)
-var quitMailNotifier = make(chan int)
+var NotifyMailChannel = make(chan NotificationMail, 1)
 
 
-func CloseMailNotifier() {
-       quitMailNotifier <- 1
-}
-
-func MailNotifier() {
+func MailNotifier(quitMailNotifier chan int) {
        logger.Println("Launched mail notifier")
        for {
                select {
        logger.Println("Launched mail notifier")
        for {
                select {
-               case notification := <-notifyMail:
+               case notification := <-NotifyMailChannel:
                        mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
                        if err != nil {
                                logger.Println("ERROR building mail:", err)
                        mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
                        if err != nil {
                                logger.Println("ERROR building mail:", err)
@@ -42,8 +30,9 @@ func MailNotifier() {
                        }
 
                        m := gomail.NewMessage()
                        }
 
                        m := gomail.NewMessage()
-                       m.SetHeader("From", config.NoticeSenderAddress)
-                       m.SetHeader("To", config.BoardMailAddress)
+                       m.SetHeader("From", config.NotificationSenderAddress)
+                       address, name := notification.GetRecipient()
+                       m.SetAddressHeader("To", address, name)
                        m.SetHeader("Subject", notification.GetSubject())
                        for header, value := range notification.GetHeaders() {
                                m.SetHeader(header, value)
                        m.SetHeader("Subject", notification.GetSubject())
                        for header, value := range notification.GetHeaders() {
                                m.SetHeader(header, value)
@@ -54,24 +43,6 @@ func MailNotifier() {
                        if err := d.DialAndSend(m); err != nil {
                                logger.Println("ERROR sending mail:", err)
                        }
                        if err := d.DialAndSend(m); err != nil {
                                logger.Println("ERROR sending mail:", err)
                        }
-               case notification := <-voterMail:
-                       mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
-                       if err != nil {
-                               logger.Println("ERROR building mail:", err)
-                               continue
-                       }
-
-                       m := gomail.NewMessage()
-                       m.SetHeader("From", config.ReminderSenderAddress)
-                       address, name := notification.GetRecipient()
-                       m.SetAddressHeader("To", address, name)
-                       m.SetHeader("Subject", notification.GetSubject())
-                       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)
-                       }
                case <-quitMailNotifier:
                        fmt.Println("Ending mail notifier")
                        return
                case <-quitMailNotifier:
                        fmt.Println("Ending mail notifier")
                        return
@@ -92,11 +63,36 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
        return
 }
 
        return
 }
 
-type NotificationClosedDecision struct {
+type notificationBase struct{}
+
+func (n *notificationBase) GetRecipient() (address string, name string) {
+       address, name = config.BoardMailAddress, "CAcert board mailing list"
+       return
+}
+
+type decisionReplyBase struct {
        decision Decision
        decision Decision
+}
+
+func (n *decisionReplyBase) GetHeaders() map[string]string {
+       return map[string]string{
+               "References":  fmt.Sprintf("<%s>", n.decision.Tag),
+               "In-Reply-To": fmt.Sprintf("<%s>", n.decision.Tag),
+       }
+}
+
+type NotificationClosedDecision struct {
+       notificationBase
+       decisionReplyBase
        voteSums VoteSums
 }
 
        voteSums VoteSums
 }
 
+func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums) *NotificationClosedDecision {
+       notification := &NotificationClosedDecision{voteSums: *voteSums}
+       notification.decision = *decision
+       return notification
+}
+
 func (n *NotificationClosedDecision) GetData() interface{} {
        return struct {
                *Decision
 func (n *NotificationClosedDecision) GetData() interface{} {
        return struct {
                *Decision
@@ -110,11 +106,8 @@ func (n *NotificationClosedDecision) GetSubject() string {
        return fmt.Sprintf("Re: %s - %s - finalised", n.decision.Tag, n.decision.Title)
 }
 
        return fmt.Sprintf("Re: %s - %s - finalised", n.decision.Tag, n.decision.Title)
 }
 
-func (n *NotificationClosedDecision) GetHeaders() map[string]string {
-       return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
-}
-
 type NotificationCreateMotion struct {
 type NotificationCreateMotion struct {
+       notificationBase
        decision Decision
        voter    Voter
 }
        decision Decision
        voter    Voter
 }
@@ -141,8 +134,15 @@ func (n *NotificationCreateMotion) GetHeaders() map[string]string {
 }
 
 type NotificationUpdateMotion struct {
 }
 
 type NotificationUpdateMotion struct {
-       decision Decision
-       voter    Voter
+       notificationBase
+       decisionReplyBase
+       voter Voter
+}
+
+func NewNotificationUpdateMotion(decision Decision, voter Voter) *NotificationUpdateMotion {
+       notification := NotificationUpdateMotion{voter: voter}
+       notification.decision = decision
+       return &notification
 }
 
 func (n *NotificationUpdateMotion) GetData() interface{} {
 }
 
 func (n *NotificationUpdateMotion) GetData() interface{} {
@@ -162,13 +162,16 @@ func (n *NotificationUpdateMotion) GetSubject() string {
        return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
 }
 
        return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
 }
 
-func (n *NotificationUpdateMotion) GetHeaders() map[string]string {
-       return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
+type NotificationWithDrawMotion struct {
+       notificationBase
+       decisionReplyBase
+       voter Voter
 }
 
 }
 
-type NotificationWithDrawMotion struct {
-       decision Decision
-       voter    Voter
+func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) *NotificationWithDrawMotion {
+       notification := &NotificationWithDrawMotion{voter: *voter}
+       notification.decision = *decision
+       return notification
 }
 
 func (n *NotificationWithDrawMotion) GetData() interface{} {
 }
 
 func (n *NotificationWithDrawMotion) GetData() interface{} {
@@ -184,10 +187,6 @@ func (n *NotificationWithDrawMotion) GetSubject() string {
        return fmt.Sprintf("Re: %s - %s - withdrawn", n.decision.Tag, n.decision.Title)
 }
 
        return fmt.Sprintf("Re: %s - %s - withdrawn", n.decision.Tag, n.decision.Title)
 }
 
-func (n *NotificationWithDrawMotion) GetHeaders() map[string]string {
-       return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
-}
-
 type RemindVoterNotification struct {
        voter     Voter
        decisions []Decision
 type RemindVoterNotification struct {
        voter     Voter
        decisions []Decision
@@ -205,7 +204,49 @@ func (n *RemindVoterNotification) GetTemplate() string { return "remind_voter_ma
 
 func (n *RemindVoterNotification) GetSubject() string { return "Outstanding CAcert board votes" }
 
 
 func (n *RemindVoterNotification) GetSubject() string { return "Outstanding CAcert board votes" }
 
+func (n *RemindVoterNotification) GetHeaders() map[string]string {
+       return map[string]string{}
+}
+
 func (n *RemindVoterNotification) GetRecipient() (address string, name string) {
        address, name = n.voter.Reminder, n.voter.Name
        return
 }
 func (n *RemindVoterNotification) GetRecipient() (address string, name string) {
        address, name = n.voter.Reminder, n.voter.Name
        return
 }
+
+type voteNotificationBase struct{}
+
+func (n *voteNotificationBase) GetRecipient() (address string, name string) {
+       address, name = config.VoteNoticeAddress, "CAcert board votes mailing list"
+       return
+}
+
+type NotificationProxyVote struct {
+       voteNotificationBase
+       decisionReplyBase
+       proxy         Voter
+       voter         Voter
+       vote          Vote
+       justification string
+}
+
+func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) *NotificationProxyVote {
+       notification := &NotificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification}
+       notification.decision = *decision
+       return notification
+}
+
+func (n *NotificationProxyVote) GetData() interface{} {
+       return struct {
+               Proxy         string
+               Vote          VoteChoice
+               Voter         string
+               Decision      *Decision
+               Justification string
+       }{n.proxy.Name, n.vote.Vote, n.voter.Name, &n.decision, n.justification}
+}
+
+func (n *NotificationProxyVote) GetTemplate() string { return "proxy_vote_mail.txt" }
+
+func (n *NotificationProxyVote) GetSubject() string {
+       return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
+}
diff --git a/proxy.php b/proxy.php
deleted file mode 100644 (file)
index a860343..0000000
--- a/proxy.php
+++ /dev/null
@@ -1,154 +0,0 @@
-<?php
-       if ($_SERVER['HTTPS'] != 'on') {
-               header("HTTP/1.0 302 Redirect");
-               header("Location: https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
-               exit();
-       }
-       require_once("database.php");
-       $db = new DB();
-       if (!($user = $db->auth())) {
-               header("HTTP/1.0 302 Redirect");
-               header("Location: denied.php");
-               exit();
-       }
-?>
-<html>
-       <head>
-               <title>CAcert Board Decisions</title>
-               <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
-               <link rel="stylesheet" type="text/css" href="styles.css" />
-       </head>
-       <body>
-<?php
-       if (!is_numeric($_REQUEST['motion'])) {
-?>
-               <b>This is not a valid motion!</b><br/>
-               <a href="motions.php">Back to motions</a><br/>
-<?php
-       } else {
-               $stmt = $db->getStatement("get decision");
-               $stmt->bindParam(":decision",$_REQUEST['motion']);
-               if ($stmt->execute() && ($decision=$stmt->fetch()) && ($decision['status'] == 0)) {
-                       if (is_numeric($_POST['voter']) && is_numeric($_POST['vote']) && is_numeric($_REQUEST['motion']) && ($_POST['justification'] != "")) {
-                               $stmt = $db->getStatement("del vote");
-                               $stmt->bindParam(":voter",$_REQUEST['voter']);
-                               $stmt->bindParam(":decision",$_REQUEST['motion']);
-                               if ($stmt->execute()) {
-                                       $stmt = $db->getStatement("do vote");
-                                       $stmt->bindParam(":voter",$_REQUEST['voter']);
-                                       $stmt->bindParam(":decision",$_REQUEST['motion']);
-                                       $stmt->bindParam(":vote",$_REQUEST['vote']);
-                                       $notes = "Proxy-Vote by ".$user['name']."\n\n".$_REQUEST['justification']."\n\n".$_SERVER['SSL_CLIENT_CERT'];
-                                       $stmt->bindParam(":notes",$notes);
-                                       if ($stmt->execute()) {
-                                               ?>
-                                                       <b>The vote has been registered.</b><br/>
-                                                       <a href="motions.php">Back to motions</a>
-                                               <?php
-                                               $stmt = $db->getStatement("get voter by id");
-                                               $stmt->bindParam(":id",$_REQUEST['voter']);
-                                               if ($stmt->execute() && ($voter=$stmt->fetch())) {
-                                                       $voter = $voter['name'];
-                                               } else {
-                                                       $voter = "Voter: ".$_REQUEST['voter'];
-                                               }
-                                               $name = $user['name'];
-                                               $justification = $_REQUEST['justification'];
-                                               $vote = '';
-                                               switch($_REQUEST['vote']) {
-                                                       case 1 : $vote='Aye'; break;
-                                                       case -1: $vote='Naye'; break;
-                                                       default: $vote='Abstain'; break;
-                                               }
-                                               $tag = $decision['tag'];
-                                               $title = $decision['title'];
-                                               $content = $decision['content'];
-                                               $due = $decision['due']." UTC";
-                                               $body = <<<BODY
-Dear Board,
-
-$name has just registered a proxy vote of $vote for $voter on motion $tag.
-
-The justification for this was:
-$justification
-
-Motion:
-$title
-$content
-
-Kind regards,
-the vote system
-
-BODY;
-                                               $db->vote_notify("Re: $tag - $title",$body,$tag);
-                                       } else {
-                                               ?>
-                                               <b>The vote has NOT been registered.</b><br/>
-                                               <a href="motions.php">Back to motions</a>
-                                               <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
-                                               <?php
-                                       }
-                               } else {
-                                       ?>
-                                       <b>The vote has NOT been registered.</b><br/>
-                                       <a href="motions.php">Back to motions</a>
-                                       <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
-                                       <?php
-                               }
-                       } else {
-                       $stmt = $db->getStatement("get voters");
-                       if ($stmt->execute() && ($voters = $stmt->fetchAll())) {
-?>
-                       <form method="POST" action="?motion=<?php echo($_REQUEST['motion']); ?>">
-                       <table>
-                               <tr>
-                                       <th>Voter</th><th>Vote</th>
-                               </tr>
-                               <tr>
-                                       <td><select name="voter"><?php
-                                       foreach ($voters as $voter) {
-?>
-                                               <option value="<?php echo($voter['id']); ?>"<?php if ($voter['id'] == $_POST['voter']) { echo(" selected=\"selected\""); } ?>><?php echo($voter['name']); ?></option>
-<?php
-                                       }
-                                       ?></select></td>
-                                       <td><select name="vote">
-                                               <option value="1"<?php if (1 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Aye</option>
-                                               <option value="0"<?php if (0 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Abstain</option>
-                                               <option value="-1"<?php if (-1 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Naye</option>
-                                       </select></td>
-                               </tr>
-                               <tr>
-                                       <th colspan="2">Justification:</th>
-                               </tr>
-                               <tr>
-                                       <td colspan="2"><textarea name="justification"><?php echo($_POST['justification']); ?></textarea></td>
-                               </tr>
-                               <tr>
-                                       <td colspan="2"><input type="submit" value="Proxy Vote" /></td>
-                               </tr>
-                       </table>
-                       </form>
-<?php
-                       } else {
-?>
-                               <b>Could not retrieve voters!</b><br/>
-                               <a href="motions.php">Back to motions</a><br/>
-                               <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
-<?php
-                       }
-               }
-?>
-                       
-<?php
-               } else {
-?>
-                       <b>This is not a valid motion!</b><br/>
-                       <a href="motions.php">Back to motions</a><br/>
-                       <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
-<?php  
-               }
-       }
-?>
-       </body>
-</html>
diff --git a/templates/proxy_vote_form.html b/templates/proxy_vote_form.html
new file mode 100644 (file)
index 0000000..eb5c421
--- /dev/null
@@ -0,0 +1,61 @@
+{{ template "header" . }}
+{{ $form := .Form }}
+<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="/proxy/{{ .Decision.Tag }}" method="post">
+    <table>
+        <tr>
+            <th>Voter</th>
+            <th>Vote</th>
+        </tr>
+        <tr>
+            <td>
+                <select name="Voter">
+                    {{ range .Voters }}
+                    <option value="{{ .Id }}"
+                            {{ if eq (.Id | print) $form.Voter }}
+                            selected{{ end }}>{{ .Name }}</option>
+                    {{ end }}
+                </select>
+            </td>
+            <td>
+                <select name="Vote">
+                    <option value="1"{{ if eq $form.Vote "1" }}
+                            selected{{ end }}>Aye
+                    </option>
+                    <option value="0"{{ if eq $form.Vote "0" }}
+                            selected{{ end }}>Abstain
+                    </option>
+                    <option value="-1"{{ if eq $form.Vote "-1" }}
+                            selected{{ end }}>Naye
+                    </option>
+                </select>
+            </td>
+        </tr>
+        <tr>
+            <th colspan="2">Justification:</th>
+        </tr>
+        <tr>
+            <td colspan="2"><textarea
+                    name="Justification">{{ $form.Justification }}</textarea>
+            </td>
+        </tr>
+        <tr>
+            <td colspan="2"><input type="submit" value="Proxy Vote"></td>
+        </tr>
+    </table>
+</form>
+{{ template "footer" . }}
\ No newline at end of file
diff --git a/templates/proxy_vote_mail.txt b/templates/proxy_vote_mail.txt
new file mode 100644 (file)
index 0000000..dd7403f
--- /dev/null
@@ -0,0 +1,13 @@
+Dear Board,
+
+{{ .Proxy }} has just registered a proxy vote of {{ .Vote }} for {{ .Voter }} on motion {{ .Decision.Tag }}.
+
+The justification for this was:
+{{ .Justification }}
+
+Motion:
+{{ .Decision.Title }}
+{{ .Decision.Content }}
+
+Kind regards,
+the vote system
\ No newline at end of file