Implement vote closing, refactor notifications
This commit is contained in:
parent
0ce9ad6dcc
commit
2de96dc13d
8 changed files with 276 additions and 517 deletions
131
actions.go
131
actions.go
|
@ -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
|
||||
}
|
|
@ -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(".")))
|
||||
|
|
12
denied.php
12
denied.php
|
@ -1,12 +0,0 @@
|
|||
<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>
|
||||
<b>You are not authorized to act here!</b><br/>
|
||||
<i>If you think this is in error, please contact the administrator</i>
|
||||
<i>If you don't know who that is, it is definitely not an error ;)</i>
|
||||
</body>
|
||||
</html>
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
header("HTTP/1.0 301 Redirect");
|
||||
header("Location: motions.php");
|
||||
exit();
|
||||
?>
|
85
models.go
85
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
|
||||
}
|
||||
|
|
198
motion.php
198
motion.php
|
@ -1,198 +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();
|
||||
}
|
||||
$db->getStatement("stats")->execute();
|
||||
$stats = $db->getStatement("stats")->fetch();
|
||||
?>
|
||||
<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 ($_REQUEST['action'] == "store") {
|
||||
if (is_numeric($_REQUEST['motion'])) {
|
||||
$stmt = $db->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()) {
|
||||
?>
|
||||
<b>The motion has been proposed!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<?php
|
||||
$decision = $db->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 = <<<BODY
|
||||
Dear Board,
|
||||
|
||||
$name has modified motion $tag to the following:
|
||||
|
||||
$title
|
||||
$content
|
||||
|
||||
Vote type: $votetype
|
||||
|
||||
To vote please choose:
|
||||
|
||||
Aye: $voteurl&vote=1
|
||||
Naye: $voteurl&vote=-1
|
||||
Abstain: $voteurl&vote=0
|
||||
|
||||
Please be aware, that if you have voted already your vote is still registered and valid.
|
||||
If this modification has an impact on how you wish to vote, you are responsible for voting
|
||||
again.
|
||||
|
||||
To see all your outstanding votes : $unvoted
|
||||
|
||||
Kind regards,
|
||||
the voting system
|
||||
BODY;
|
||||
$db->notify("Re: $tag - $title - modified",$body,$tag);
|
||||
} else {
|
||||
?>
|
||||
<b>The motion has NOT been proposed!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i><br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
$stmt = $db->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()) {
|
||||
?>
|
||||
<b>The motion has been proposed!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<?php
|
||||
$decision = $db->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 = <<<BODY
|
||||
Dear Board,
|
||||
|
||||
$name has made the following motion:
|
||||
|
||||
$title
|
||||
$content
|
||||
|
||||
Vote type: $votetype
|
||||
|
||||
Voting will close $due.
|
||||
|
||||
To vote please choose:
|
||||
|
||||
Aye: $voteurl&vote=1
|
||||
Naye: $voteurl&vote=-1
|
||||
Abstain: $voteurl&vote=0
|
||||
|
||||
To see all your outstanding votes : $unvoted
|
||||
|
||||
Kind regards,
|
||||
the voting system
|
||||
BODY;
|
||||
$db->notify("$tag - $title",$body,$tag,TRUE);
|
||||
} else {
|
||||
?>
|
||||
<b>The motion has NOT been proposed!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i><br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (is_numeric($_REQUEST['motion'])) {
|
||||
$stmt = $db->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
|
||||
}
|
||||
?>
|
||||
<form <?php if (is_numeric($_REQUEST['motion'])) { echo(" action=\"?\""); } ?> method="POST">
|
||||
<input type="hidden" name="action" value="store" />
|
||||
<?php
|
||||
if (is_numeric($_REQUEST['motion'])) {
|
||||
?><input type="hidden" name="motion" value="<?php echo($_REQUEST["motion"]); ?>" /><?php
|
||||
}
|
||||
?>
|
||||
<table>
|
||||
<tr><td>ID:</td><td><?php echo htmlentities($motion['tag']); ?></td></tr>
|
||||
<tr><td>Proponent:</td><td><?php echo htmlentities($motion['proposer']); ?></td></tr>
|
||||
<tr><td>Proposed date/time:</td><td><?php echo htmlentities($motion['proposed'] ? $motion['proposed']." UTC" : '(auto filled to current date/time)'); ?></td></tr>
|
||||
<tr><td>Title:</td><td><input name="title" value="<?php echo htmlentities($motion['title'])?>"></td></tr>
|
||||
<tr><td>Text:</td><td><textarea name="content"><?php echo htmlspecialchars($motion['content'])?></textarea></td></tr>
|
||||
<tr><td>Vote type:</td><td><select name="votetype">
|
||||
<option value="0" <?php if(!$motion['votetype']) { echo(" selected=\"selected\""); } ?>>Motion</option>
|
||||
<option value="1" <?php if($motion['votetype']) { echo(" selected=\"selected\""); } ?>>Veto</option>
|
||||
</select></td></tr>
|
||||
<tr><td rowspan="2">Due:</td><td><?php echo($motion['due'] ? $motion['due'].' UTC' : '(autofilled from option below)')?></td></tr>
|
||||
<tr><td><select name="due">
|
||||
<option value="+3 days">In 3 Days</option>
|
||||
<option value="+7 days">In 1 Week</option>
|
||||
<option value="+14 days">In 2 Weeks</option>
|
||||
<option value="+28 days">In 4 Weeks</option>
|
||||
</select></td></tr>
|
||||
<tr><td> </td><td><input type="submit" value="Propose" /></td></tr>
|
||||
</table>
|
||||
</form>
|
||||
<br/>
|
||||
<a href="motions.php">Back to motions</a>
|
||||
</body>
|
||||
</html>
|
167
motions.php
167
motions.php
|
@ -1,167 +0,0 @@
|
|||
<?php
|
||||
require_once("database.php");
|
||||
$db = new DB();
|
||||
$page = is_numeric($_REQUEST['page'])?$_REQUEST['page']:1;
|
||||
$user = $db->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 = <<<BODY
|
||||
Dear Board,
|
||||
|
||||
$name has withdrawn the motion $tag that was as follows:
|
||||
|
||||
$title
|
||||
$content
|
||||
|
||||
Kind regards,
|
||||
the voting system
|
||||
BODY;
|
||||
$db->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();
|
||||
}
|
||||
?>
|
||||
<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 ($user) echo '<a href="?unvoted=1">Show my outstanding votes</a><br/>';
|
||||
?>
|
||||
<table class="list">
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Motion</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<?php
|
||||
if ($_REQUEST['motion']) {
|
||||
$stmt = $db->getStatement("list decision");
|
||||
$stmt->execute(array($_REQUEST['motion']));
|
||||
} else {
|
||||
if ($user && $_REQUEST['unvoted']) {
|
||||
$stmt = $db->getStatement("list my unvoted decisions");
|
||||
$stmt->bindParam(":id",$user['id']);
|
||||
} else {
|
||||
$stmt = $db->getStatement("list decisions");
|
||||
}
|
||||
$stmt->bindParam(":page",$page);
|
||||
$stmt->execute();
|
||||
}
|
||||
$items = 0;
|
||||
$id = -1;
|
||||
while ($row = $stmt->fetch()) {
|
||||
$items++;
|
||||
$id = $row['id'];
|
||||
?><tr>
|
||||
<td class="<?php switch($row['status']) { case 0: echo "pending"; break; case 1: echo "approved"; break; case -1: echo "declined"; break; case -2: echo "withdrawn"; break; }?>">
|
||||
<?php
|
||||
switch($row['status']) {
|
||||
case 0: echo "Pending<br/><i>".$row['due']." UTC</i>"; break;
|
||||
case 1: echo "Approved<br/><i>".$row['modified']." UTC</i>"; break;
|
||||
case -1: echo "Declined<br/><i>".$row['modified']." UTC</i>"; break;
|
||||
case -2: echo "Withdrawn<br/><i>".$row['modified']." UTC</i>"; break;
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<i><a href="motions.php?motion=<?php echo $row['tag'].'">'.$row['tag']; ?></a></i><br/>
|
||||
<b><?php echo htmlspecialchars($row['title']); ?></b><br/>
|
||||
<pre><?php echo wordwrap(htmlspecialchars($row['content'])); ?></pre>
|
||||
<br/>
|
||||
<i>Due: <?php echo($row['due']); ?> UTC</i><br/>
|
||||
<i>Proposed: <?php echo($row['proposer']); ?> (<?php echo($row['proposed']); ?> UTC)</i><br/>
|
||||
<i>Vote type: <?php echo(!$row['votetype']?'motion':'veto'); ?></i><br/>
|
||||
<i>Aye|Naye|Abstain: <?php echo($row['ayes']); ?>|<?php echo($row['nayes']); ?>|<?php echo($row['abstains']); ?></i><br/>
|
||||
<?php
|
||||
if ($row['status'] ==0 || $_REQUEST['showvotes']) {
|
||||
$state = array('Naye','Abstain','Aye');
|
||||
$vstmt = $db->getStatement("list votes");
|
||||
$vstmt->execute(array($row['id']));
|
||||
echo "<i>Votes:</i><br/>";
|
||||
while ($vrow = $vstmt->fetch()) {
|
||||
echo "<i>".$vrow['name'].": ".$state[$vrow['vote']+1]."</i><br/>";
|
||||
}
|
||||
} else {
|
||||
echo '<i><a href="motions.php?motion='.$row['tag'].'&showvotes=1">Show Votes</a></i><br/>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<?php
|
||||
if ($row['status'] == 0 && $user ) {
|
||||
?>
|
||||
<ul>
|
||||
<li><a href="vote.php?motion=<?php echo($row['id']); ?>&vote=1">Aye</a></li>
|
||||
<li><a href="vote.php?motion=<?php echo($row['id']); ?>&vote=0">Abstain</a></li>
|
||||
<li><a href="vote.php?motion=<?php echo($row['id']); ?>&vote=-1">Naye</a></li>
|
||||
<li><a href="proxy.php?motion=<?php echo($row['id']); ?>">Proxy Vote</a></li>
|
||||
<li><a href="motion.php?motion=<?php echo($row['id']); ?>">Modify</a></li>
|
||||
<li><a href="motions.php?motion=<?php echo($row['tag']); ?>&withdrawl=1">Withdrawl</a></li>
|
||||
</ul>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr><?php
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td colspan="2" class="navigation">
|
||||
<?php if ($page>1) { ?><a href="?page=<?php echo($page-1); ?>"><</a><?php } else { ?> <?php } ?>
|
||||
|
||||
<?php if ($items>9) { ?><a href="?page=<?php echo($page+1); ?>">></a><?php } else { ?> <?php } ?>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<?php if ($user) echo('<ul><li><a href="motion.php">New Motion</a></li></ul>'); ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
if ($_REQUEST['withdrawl']) {
|
||||
?>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<?php
|
||||
if ($_REQUEST['confirm'] && $_REQUEST['id']) {
|
||||
?>
|
||||
<a href="motions.php">Motion Withdrawn</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<form action="?withdrawl=1&confirm=1&id=<?php echo $id;?>" method="post">
|
||||
<input type="submit" value="Withdraw">
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
171
notifications.go
Normal file
171
notifications.go
Normal file
|
@ -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)}
|
||||
}
|
Loading…
Reference in a new issue