diff --git a/actions.go b/actions.go
deleted file mode 100644
index 19e9bf3..0000000
--- a/actions.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package main
-
-import (
- "bytes"
- "fmt"
- "github.com/Masterminds/sprig"
- "gopkg.in/gomail.v2"
- "text/template"
-)
-
-type templateBody string
-
-func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer, err error) {
- t, err := template.New(templateName).Funcs(
- sprig.GenericFuncMap()).ParseFiles(fmt.Sprintf("templates/%s", templateName))
- if err != nil {
- return
- }
-
- mailText = bytes.NewBufferString("")
- t.Execute(mailText, context)
-
- return
-}
-
-func CreateMotion(decision *Decision, voter *Voter) (err error) {
- decision.ProponentId = voter.Id
- err = decision.Create()
- if err != nil {
- logger.Println("Error saving motion:", err)
- return
- }
-
- type mailContext struct {
- Decision
- Name string
- VoteURL string
- UnvotedURL string
- }
- voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
- unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
- context := mailContext{*decision, voter.Name, voteURL, unvotedURL}
-
- mailText, err := buildMail("create_motion_mail.txt", context)
- if err != nil {
- logger.Println("Error", err)
- return
- }
-
- m := gomail.NewMessage()
- m.SetHeader("From", config.NoticeSenderAddress)
- m.SetHeader("To", config.BoardMailAddress)
- m.SetHeader("Subject", fmt.Sprintf("%s - %s", decision.Tag, decision.Title))
- m.SetHeader("Message-ID", 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)
- }
-
- return
-}
-
-func UpdateMotion(decision *Decision, voter *Voter) (err error) {
- err = decision.Update()
- if err != nil {
- logger.Println("Error updating motion:", err)
- return
- }
-
- type mailContext struct {
- Decision
- Name string
- VoteURL string
- UnvotedURL string
- }
- voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
- unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
- context := mailContext{*decision, voter.Name, voteURL, unvotedURL}
-
- mailText, err := buildMail("update_motion_mail.txt", context)
- if err != nil {
- 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", 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)
- }
-
- return
-}
-
-func WithdrawMotion(decision *Decision, voter *Voter) (err error) {
- err = decision.UpdateStatus()
-
- type mailContext struct {
- *Decision
- Name string
- }
- context := mailContext{decision, voter.Name}
-
- mailText, err := buildMail("withdraw_motion_mail.txt", context)
- if err != nil {
- 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)
- }
-
- return
-}
diff --git a/boardvoting.go b/boardvoting.go
index 187a867..30de17e 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -271,16 +271,20 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
decision.Status = voteStatusWithdrawn
decision.Modified = time.Now().UTC()
- if err := WithdrawMotion(&decision.Decision, voter); err != nil {
+ if err := decision.UpdateStatus(); err != nil {
+ logger.Println("Error withdrawing motion:", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+
+ notifyMail <- &NotificationWithDrawMotion{decision: decision.Decision, voter: *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)
return
}
+
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
- return
default:
templateContext.Decision = decision
renderTemplate(w, templates, templateContext)
@@ -319,10 +323,15 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, templates, templateContext)
} else {
data.Proposed = time.Now().UTC()
- if err := CreateMotion(data, voter); err != nil {
+ data.ProponentId = voter.Id
+ if err := data.Create(); err != nil {
+ logger.Println("Error saving motion:", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+
+ notifyMail <- &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)
return
@@ -380,10 +389,14 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, templates, templateContext)
} else {
data.Modified = time.Now().UTC()
- if err := UpdateMotion(data, voter); err != nil {
+ if err := data.Update(); err != nil {
+ logger.Println("Error updating motion:", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+
+ notifyMail <- &NotificationUpdateMotion{decision: *data, voter: *voter}
+
if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@@ -502,6 +515,9 @@ func main() {
defer db.Close()
+ go MailNotifier()
+ defer CloseMailNotifier()
+
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
http.Handle("/newmotion/", motionsHandler{})
http.Handle("/static/", http.FileServer(http.Dir(".")))
diff --git a/denied.php b/denied.php
deleted file mode 100644
index 9bb72d6..0000000
--- a/denied.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- CAcert Board Decisions
-
-
-
-
- You are not authorized to act here!
- If you think this is in error, please contact the administrator
- If you don't know who that is, it is definitely not an error ;)
-
-
\ No newline at end of file
diff --git a/index.php b/index.php
deleted file mode 100644
index 3363496..0000000
--- a/index.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
\ No newline at end of file
diff --git a/models.go b/models.go
index d6eee15..5df7d95 100644
--- a/models.go
+++ b/models.go
@@ -21,6 +21,7 @@ const (
sqlCreateDecision
sqlUpdateDecision
sqlUpdateDecisionStatus
+ sqlSelectClosableDecisions
)
var sqlStatements = map[sqlKey]string{
@@ -103,6 +104,12 @@ WHERE id=:id`,
UPDATE decisions
SET status=:status, modified=:modified WHERE id=:id
`,
+ sqlSelectClosableDecisions: `
+SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
+ decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
+FROM decisions
+JOIN voters ON decisions.proponent=voters.id
+WHERE decisions.status=0 AND :now > due`,
}
var db *sqlx.DB
@@ -506,3 +513,81 @@ func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
}
return
}
+
+func (d *Decision) Close() (err error) {
+ closeDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus])
+ if err != nil {
+ logger.Println("Error preparing statement:", err)
+ return
+ }
+ defer closeDecisionStmt.Close()
+
+ quorum, majority := d.VoteType.QuorumAndMajority()
+
+ voteSums, err := d.VoteSums()
+
+ if err != nil {
+ logger.Println("Error getting vote sums")
+ return
+ }
+ votes := voteSums.VoteCount()
+
+ if votes < quorum {
+ d.Status = voteStatusDeclined
+ } else {
+ votes = voteSums.Ayes + voteSums.Nayes
+ if (voteSums.Ayes / votes) > (majority / 100) {
+ d.Status = voteStatusApproved
+ } else {
+ d.Status = voteStatusDeclined
+ }
+ }
+
+ result, err := closeDecisionStmt.Exec(d)
+ if err != nil {
+ logger.Println("Error closing vote:", err)
+ return
+ }
+ affectedRows, err := result.RowsAffected()
+ if err != nil {
+ logger.Println("Error getting affected rows:", err)
+ }
+ if affectedRows != 1 {
+ logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
+ }
+
+ notifyMail <- &NotificationClosedDecision{decision: *d, voteSums: *voteSums}
+
+ return
+}
+
+func CloseDecisions() (err error) {
+ getClosedDecisionsStmt, err := db.PrepareNamed(sqlStatements[sqlSelectClosableDecisions])
+ if err != nil {
+ logger.Println("Error preparing statement:", err)
+ return
+ }
+ defer getClosedDecisionsStmt.Close()
+
+ params := make(map[string]interface{}, 1)
+ params["now"] = time.Now().UTC()
+ rows, err := getClosedDecisionsStmt.Queryx(params)
+ if err != nil {
+ logger.Println("Error fetching closed decisions", err)
+ return
+ }
+ defer rows.Close()
+ for rows.Next() {
+ d := &Decision{}
+ if err = rows.StructScan(d); err != nil {
+ logger.Println("Error filling decision from database row", err)
+ return
+ }
+ if err = d.Close(); err != nil {
+ logger.Printf("Error closing decision %s: %s\n", d.Tag, err)
+ return
+ }
+ }
+
+ return
+}
diff --git a/motion.php b/motion.php
deleted file mode 100644
index 2dec354..0000000
--- a/motion.php
+++ /dev/null
@@ -1,198 +0,0 @@
-auth())) {
- header("HTTP/1.0 302 Redirect");
- header("Location: denied.php");
- exit();
- }
- $db->getStatement("stats")->execute();
- $stats = $db->getStatement("stats")->fetch();
-?>
-
-
- CAcert Board Decisions
-
-
-
-
- getStatement("update decision");
- $stmt->bindParam(":id",$_POST['motion']);
- $stmt->bindParam(":proponent",$user['id']);
- $stmt->bindParam(":title",$_POST['title']);
- $stmt->bindParam(":content",$_POST['content']);
- $stmt->bindParam(":due",$_POST['due']);
- $stmt->bindParam(":votetype",$_POST['votetype']);
- if ($stmt->execute()) {
- ?>
- The motion has been proposed!
- Back to motions
-
-
- getStatement("get decision")->execute(array($_POST['motion']))?$db->getStatement("get decision")->fetch():array();
- $name = $user['name'];
- $tag = $decision['tag'];
- $title = $decision['title'];
- $content =$decision['content'];
- $due = $decision['due']." UTC";
- $votetype = !$decision['votetype'] ? 'motion' : 'veto';
- $baseurl = "https://".$_SERVER['HTTP_HOST'].":".$_SERVER['SERVER_PORT'].preg_replace('/motion\.php/','',$_SERVER['REQUEST_URI']);
- $voteurl = $baseurl."vote.php?motion=".$decision['id'];
- $unvoted = $baseurl."motions.php?unvoted=1";
- $body = <<notify("Re: $tag - $title - modified",$body,$tag);
- } else {
- ?>
- The motion has NOT been proposed!
- Back to motions
- \n",$stmt->errorInfo()); ?>
-
-
- getStatement("create decision");
- $stmt->bindParam(":proponent",$user['id']);
- $stmt->bindParam(":title",$_POST['title']);
- $stmt->bindParam(":content",$_POST['content']);
- $stmt->bindParam(":votetype",$_POST['votetype']);
- $stmt->bindParam(":due",$_POST['due']);
- if ($stmt->execute()) {
- ?>
- The motion has been proposed!
- Back to motions
-
-
- getStatement("get new decision")->execute()?$db->getStatement("get new decision")->fetch():array();
- $name = $user['name'];
- $tag = $decision['tag'];
- $title = $decision['title'];
- $content =$decision['content'];
- $due = $decision['due']." UTC";
- $votetype = !$decision['votetype'] ? 'motion' : 'veto';
- $baseurl = "https://".$_SERVER['HTTP_HOST'].":".$_SERVER['SERVER_PORT'].preg_replace('/motion\.php/','',$_SERVER['REQUEST_URI']);
- $voteurl = $baseurl."vote.php?motion=".$decision['id'];
- $unvoted = $baseurl."motions.php?unvoted=1";
- $body = <<notify("$tag - $title",$body,$tag,TRUE);
- } else {
- ?>
- The motion has NOT been proposed!
- Back to motions
- \n",$stmt->errorInfo()); ?>
-
-
- getStatement("get decision");
- if ($stmt->execute(array($_REQUEST['motion']))) {
- $motion = $stmt->fetch();
- }
- if (!is_numeric($motion['id'])) {
- $motion = array();
- foreach (array("title","content") as $column) {
- $motion[$column] = "";
- }
- $motion["proposer"] = $user['name'];
- $motion["votetype"] = 0; // defaults to motion
- }
- } else {
- $motion = array();
- foreach (array("title","content") as $column) {
- $motion[$column] = "";
- }
- $motion["proposer"] = $user['name'];
- $motion["votetype"] = 0; // defaults to motion
- }
- ?>
-
-
- Back to motions
-
-
diff --git a/motions.php b/motions.php
deleted file mode 100644
index 548731f..0000000
--- a/motions.php
+++ /dev/null
@@ -1,167 +0,0 @@
-auth();
-
- if ($_REQUEST['withdrawl'] && $_REQUEST['confirm'] && $_REQUEST['id']) {
- if (!$user) {
- header("HTTP/1.0 302 Redirect");
- header("Location: denied.php");
- exit();
- }
- $stmt = $db->getStatement("get decision");
- $stmt->bindParam(":decision",$_REQUEST['id']);
- if ($stmt->execute() && ($decision=$stmt->fetch())) {
- $name = $user['name'];
- $tag = $decision['tag'];
- $title = $decision['title'];
- $content = $decision['content'];
- $body = <<notify("Re: $tag - $title - withdrawn",$body,$tag);
- }
- $stmt = $db->getStatement("close decision");
- $status = -2;
- $stmt->bindParam(":status",$status);
- $stmt->bindParam(":decision",$_REQUEST['id']);
- $stmt->execute();
- }
-?>
-
-
- CAcert Board Decisions
-
-
-
-
- Show my outstanding votes
';
- ?>
-
-
-
diff --git a/notifications.go b/notifications.go
new file mode 100644
index 0000000..beb9b3f
--- /dev/null
+++ b/notifications.go
@@ -0,0 +1,171 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/Masterminds/sprig"
+ "gopkg.in/gomail.v2"
+ "text/template"
+)
+
+type NotificationMail interface {
+ GetData() interface{}
+ GetTemplate() string
+ GetSubject() string
+ GetHeaders() map[string]string
+}
+
+var notifyMail = make(chan NotificationMail, 1)
+var quitMailNotifier = make(chan int)
+
+func CloseMailNotifier() {
+ quitMailNotifier <- 1
+}
+
+func MailNotifier() {
+ logger.Println("Launched mail notifier")
+ for {
+ select {
+ case notification := <-notifyMail:
+ mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
+ if err != nil {
+ logger.Println("ERROR building mail:", err)
+ continue
+ }
+
+ m := gomail.NewMessage()
+ m.SetHeader("From", config.NoticeSenderAddress)
+ m.SetHeader("To", config.BoardMailAddress)
+ m.SetHeader("Subject", notification.GetSubject())
+ for header, value := range notification.GetHeaders() {
+ m.SetHeader(header, value)
+ }
+ 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
+ }
+ }
+}
+
+func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer, err error) {
+ t, err := template.New(templateName).Funcs(
+ sprig.GenericFuncMap()).ParseFiles(fmt.Sprintf("templates/%s", templateName))
+ if err != nil {
+ return
+ }
+
+ mailText = bytes.NewBufferString("")
+ t.Execute(mailText, context)
+
+ return
+}
+
+type NotificationClosedDecision struct {
+ decision Decision
+ voteSums VoteSums
+}
+
+func (n *NotificationClosedDecision) GetData() interface{} {
+ return struct {
+ *Decision
+ *VoteSums
+ }{&n.decision, &n.voteSums}
+}
+
+func (n *NotificationClosedDecision) GetTemplate() string {
+ return "closed_motion_mail.txt"
+}
+
+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 {
+ decision Decision
+ voter Voter
+}
+
+func (n *NotificationCreateMotion) GetData() interface{} {
+ voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
+ unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
+ return struct {
+ *Decision
+ Name string
+ VoteURL string
+ UnvotedURL string
+ }{&n.decision, n.voter.Name, voteURL, unvotedURL}
+}
+
+func (n *NotificationCreateMotion) GetTemplate() string {
+ return "create_motion_mail.txt"
+}
+
+func (n *NotificationCreateMotion) GetSubject() string {
+ return fmt.Sprintf("%s - %s", n.decision.Tag, n.decision.Title)
+}
+
+func (n *NotificationCreateMotion) GetHeaders() map[string]string {
+ return map[string]string{"Message-ID": fmt.Sprintf("<%s>", n.decision.Tag)}
+}
+
+type NotificationUpdateMotion struct {
+ decision Decision
+ voter Voter
+}
+
+func (n *NotificationUpdateMotion) GetData() interface{} {
+ voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
+ unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
+ return struct {
+ *Decision
+ Name string
+ VoteURL string
+ UnvotedURL string
+ }{&n.decision, n.voter.Name, voteURL, unvotedURL}
+}
+
+func (n *NotificationUpdateMotion) GetTemplate() string {
+ return "update_motion_mail.txt"
+}
+
+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 {
+ decision Decision
+ voter Voter
+}
+
+func (n *NotificationWithDrawMotion) GetData() interface{} {
+ return struct {
+ *Decision
+ Name string
+ }{&n.decision, n.voter.Name}
+}
+
+func (n *NotificationWithDrawMotion) GetTemplate() string {
+ return "withdraw_motion_mail.txt"
+}
+
+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)}
+}