summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--boardvoting.go188
-rw-r--r--forms.go37
-rw-r--r--jobs.go2
-rw-r--r--models.go124
-rw-r--r--notifications.go143
-rw-r--r--proxy.php154
-rw-r--r--templates/proxy_vote_form.html61
-rw-r--r--templates/proxy_vote_mail.txt13
8 files changed, 486 insertions, 236 deletions
diff --git a/boardvoting.go b/boardvoting.go
index 152d969..d62b80e 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -1,10 +1,12 @@
package main
import (
+ "bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
+ "encoding/pem"
"fmt"
"github.com/Masterminds/sprig"
"github.com/gorilla/sessions"
@@ -50,6 +52,7 @@ const (
ctxNeedsAuth contextKey = iota
ctxVoter
ctxDecision
+ ctxVote
)
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
}
+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) {
@@ -277,7 +285,7 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
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)
@@ -330,7 +338,7 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
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)
@@ -395,7 +403,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
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)
@@ -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 {
- 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"`
@@ -516,8 +671,9 @@ func main() {
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)
@@ -525,6 +681,8 @@ func main() {
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))
diff --git a/forms.go b/forms.go
index b7b7225..e0d430a 100644
--- a/forms.go
+++ b/forms.go
@@ -92,3 +92,40 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
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
--- a/jobs.go
+++ b/jobs.go
@@ -142,7 +142,7 @@ func (j *RemindVotersJob) Run() {
return
}
if len(*decisions) > 0 {
- voterMail <- &RemindVoterNotification{voter: voter, decisions: *decisions}
+ NotifyMailChannel <- &RemindVoterNotification{voter: voter, decisions: *decisions}
}
}
}
diff --git a/models.go b/models.go
index 5297cf2..dfc054d 100644
--- a/models.go
+++ b/models.go
@@ -26,6 +26,10 @@ const (
sqlGetNextPendingDecisionDue
sqlGetReminderVoters
sqlFindUnvotedDecisionsForVoter
+ sqlGetEnabledVoterById
+ sqlCreateVote
+ sqlLoadVote
+ sqlGetVotersForProxy
)
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`,
+ sqlGetEnabledVoterById: `
+SELECT id, name, enabled, reminder
+FROM voters
+WHERE enabled=1 AND id=$1`,
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`,
+ 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)
-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
@@ -122,9 +139,9 @@ type VoteType uint8
type VoteStatus int8
type Decision struct {
- Id int
+ Id int64
Proposed time.Time
- ProponentId int `db:"proponent"`
+ ProponentId int64 `db:"proponent"`
Title string
Content string
Quorum int
@@ -137,12 +154,12 @@ type Decision struct {
}
type Email struct {
- VoterId int `db:"voter"`
+ VoterId int64 `db:"voter"`
Address string
}
type Voter struct {
- Id int
+ Id int64
Name string
Enabled bool
Reminder string // reminder email address
@@ -150,14 +167,6 @@ type Voter struct {
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
@@ -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
@@ -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
@@ -569,7 +627,7 @@ func (d *Decision) Close() (err error) {
logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
}
- notifyMail <- &NotificationClosedDecision{decision: *d, voteSums: *voteSums}
+ NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums)
return
}
@@ -671,3 +729,39 @@ func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err erro
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
+}
diff --git a/notifications.go b/notifications.go
index d4b9715..499a689 100644
--- a/notifications.go
+++ b/notifications.go
@@ -13,28 +13,16 @@ type NotificationMail interface {
GetTemplate() string
GetSubject() string
GetHeaders() map[string]string
-}
-
-type VoterMail interface {
- GetData() interface{}
- GetTemplate() string
- GetSubject() 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 {
- case notification := <-notifyMail:
+ case notification := <-NotifyMailChannel:
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.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)
@@ -54,24 +43,6 @@ func MailNotifier() {
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
@@ -92,11 +63,36 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
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
+}
+
+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
}
+func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums) *NotificationClosedDecision {
+ notification := &NotificationClosedDecision{voteSums: *voteSums}
+ notification.decision = *decision
+ return notification
+}
+
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)
}
-func (n *NotificationClosedDecision) GetHeaders() map[string]string {
- return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
-}
-
type NotificationCreateMotion struct {
+ notificationBase
decision Decision
voter Voter
}
@@ -141,8 +134,15 @@ func (n *NotificationCreateMotion) GetHeaders() map[string]string {
}
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{} {
@@ -162,13 +162,16 @@ func (n *NotificationUpdateMotion) GetSubject() string {
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{} {
@@ -184,10 +187,6 @@ func (n *NotificationWithDrawMotion) GetSubject() string {
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
@@ -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) 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
}
+
+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
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
index 0000000..eb5c421
--- /dev/null
+++ b/templates/proxy_vote_form.html
@@ -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
index 0000000..dd7403f
--- /dev/null
+++ b/templates/proxy_vote_mail.txt
@@ -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