summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Dittberner <jandd@cacert.org>2017-04-20 20:58:22 +0200
committerJan Dittberner <jan@dittberner.info>2017-04-22 00:12:38 +0200
commitb6ad5d8ad327066b21bdb690f5a5017f6bed9740 (patch)
tree6bd3420f4389a684c575aee792271bad64ef0d52
parentdcdd5f715f4800d02b04841054342ea2e44d950e (diff)
downloadcacert-boardvoting-b6ad5d8ad327066b21bdb690f5a5017f6bed9740.tar.gz
cacert-boardvoting-b6ad5d8ad327066b21bdb690f5a5017f6bed9740.tar.xz
cacert-boardvoting-b6ad5d8ad327066b21bdb690f5a5017f6bed9740.zip
Implement reminder job
-rw-r--r--boardvoting.go19
-rw-r--r--config.yaml.example1
-rw-r--r--jobs.go63
-rw-r--r--models.go141
-rw-r--r--notifications.go64
-rwxr-xr-xremind.php43
-rw-r--r--templates/remind_voter_mail.txt15
7 files changed, 220 insertions, 126 deletions
diff --git a/boardvoting.go b/boardvoting.go
index 3b03f22..152d969 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -468,15 +468,16 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
type Config struct {
- BoardMailAddress string `yaml:"board_mail_address"`
- NoticeSenderAddress string `yaml:"notice_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"`
+ 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 {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"mail_server"`
diff --git a/config.yaml.example b/config.yaml.example
index f971540..d2c24e3 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -1,6 +1,7 @@
---
board_mail_address: cacert-board@lists.cacert.org
notice_sender_address: cacert-board-votes@lists.cacert.org
+reminder_sender_address: returns@cacert.org
database_file: database.sqlite
client_ca_certificates: cacert_class3.pem
server_certificate: server.crt
diff --git a/jobs.go b/jobs.go
index 02683c8..3f15e2d 100644
--- a/jobs.go
+++ b/jobs.go
@@ -12,13 +12,15 @@ type jobIdentifier int
const (
JobIdCloseDecisions jobIdentifier = iota
+ JobIdRemindVotersJob
)
var rescheduleChannel = make(chan jobIdentifier, 1)
func JobScheduler(quitChannel chan int) {
var jobs = map[jobIdentifier]Job{
- JobIdCloseDecisions: NewCloseDecisionsJob(),
+ JobIdCloseDecisions: NewCloseDecisionsJob(),
+ JobIdRemindVotersJob: NewRemindVotersJob(),
}
logger.Println("INFO started job scheduler")
@@ -60,11 +62,10 @@ func (j *CloseDecisionsJob) Schedule() {
return
}
if nextDue == nil {
- if j.timer != nil {
- j.timer.Stop()
- j.timer = nil
- }
+ logger.Println("INFO no next planned execution of CloseDecisionsJob")
+ j.Stop()
} else {
+ nextDue := nextDue.Add(time.Minute)
logger.Println("INFO scheduling CloseDecisionsJob for", nextDue)
when := nextDue.Sub(time.Now())
if j.timer != nil {
@@ -78,6 +79,7 @@ func (j *CloseDecisionsJob) Schedule() {
func (j *CloseDecisionsJob) Stop() {
if j.timer != nil {
j.timer.Stop()
+ j.timer = nil
}
}
@@ -93,3 +95,54 @@ func (j *CloseDecisionsJob) Run() {
func (j *CloseDecisionsJob) String() string {
return "CloseDecisionsJob"
}
+
+type RemindVotersJob struct {
+ timer *time.Timer
+}
+
+func NewRemindVotersJob() *RemindVotersJob {
+ job := &RemindVotersJob{}
+ job.Schedule()
+ return job
+}
+
+func (j *RemindVotersJob) Schedule() {
+ year, month, day := time.Now().UTC().Date()
+ nextExecution := time.Date(year, month, day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 3)
+ logger.Println("INFO scheduling RemindVotersJob for", nextExecution)
+ when := nextExecution.Sub(time.Now())
+ if j.timer != nil {
+ j.timer.Reset(when)
+ } else {
+ j.timer = time.AfterFunc(when, j.Run)
+ }
+}
+
+func (j *RemindVotersJob) Stop() {
+ if j.timer != nil {
+ j.timer.Stop()
+ j.timer = nil
+ }
+}
+
+func (j *RemindVotersJob) Run() {
+ logger.Println("INFO running RemindVotersJob")
+ defer func() { rescheduleChannel <- JobIdRemindVotersJob }()
+
+ voters, err := GetReminderVoters()
+ if err != nil {
+ logger.Println("ERROR problem getting voters", err)
+ return
+ }
+
+ for _, voter := range *voters {
+ decisions, err := FindUnvotedDecisionsForVoter(&voter)
+ if err != nil {
+ logger.Println("ERROR problem getting unvoted decisions")
+ return
+ }
+ if len(*decisions) > 0 {
+ voterMail <- &RemindVoterNotification{voter: voter, decisions: *decisions}
+ }
+ }
+}
diff --git a/models.go b/models.go
index d27610f..5297cf2 100644
--- a/models.go
+++ b/models.go
@@ -24,96 +24,85 @@ const (
sqlUpdateDecisionStatus
sqlSelectClosableDecisions
sqlGetNextPendingDecisionDue
+ sqlGetReminderVoters
+ sqlFindUnvotedDecisionsForVoter
)
var sqlStatements = map[sqlKey]string{
sqlLoadDecisions: `
-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
+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
ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $1`,
sqlLoadUnvotedDecisions: `
-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 decisions.id NOT IN (
- SELECT votes.decision
- FROM votes
- WHERE votes.voter = $1)
+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 decisions.id NOT IN (SELECT votes.decision FROM votes WHERE votes.voter = $1)
ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $2;`,
sqlLoadDecisionByTag: `
-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.tag=$1;`,
+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.tag=$1;`,
sqlLoadDecisionById: `
-SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed,
- decisions.title, decisions.content, decisions.votetype, decisions.status,
- decisions.due, decisions.modified
-FROM decisions
-WHERE decisions.id=$1;`,
+SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
+ decisions.votetype, decisions.status, decisions.due, decisions.modified
+FROM decisions
+WHERE decisions.id=$1;`,
sqlLoadVoteCountsForDecision: `
-SELECT vote, COUNT(vote)
-FROM votes
-WHERE decision=$1 GROUP BY vote`,
+SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
sqlLoadVotesForDecision: `
SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes
-FROM votes
-JOIN voters ON votes.voter=voters.id
-WHERE decision=$1`,
+FROM votes
+JOIN voters ON votes.voter=voters.id
+WHERE decision=$1`,
sqlLoadEnabledVoterByEmail: `
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`,
sqlCountOlderThanDecision: `
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
sqlCountOlderThanUnvotedDecision: `
-SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1
- AND status=0
- AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
+SELECT COUNT(*) > 0 FROM decisions
+WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
sqlCreateDecision: `
-INSERT INTO decisions (
- proposed, proponent, title, content, votetype, status, due, modified,tag
-) VALUES (
- :proposed, :proponent, :title, :content, :votetype, 0,
- :due,
- :proposed,
+INSERT INTO decisions (proposed, proponent, title, content, votetype, status, due, modified,tag)
+VALUES (
+ :proposed, :proponent, :title, :content, :votetype, 0, :due, :proposed,
'm' || strftime('%Y%m%d', :proposed) || '.' || (
SELECT COUNT(*)+1 AS num
FROM decisions
- WHERE proposed
- BETWEEN date(:proposed) AND date(:proposed, '1 day')
+ WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day')
)
)`,
sqlUpdateDecision: `
UPDATE decisions
-SET proponent=:proponent, title=:title, content=:content,
- votetype=:votetype, due=:due, modified=:modified
-WHERE id=:id`,
+SET proponent=:proponent, title=:title, content=:content, votetype=:votetype, due=:due, modified=:modified
+WHERE id=:id`,
sqlUpdateDecisionStatus: `
-UPDATE decisions
-SET status=:status, modified=:modified WHERE id=:id
-`,
+UPDATE decisions SET status=:status, modified=:modified WHERE id=:id`,
sqlSelectClosableDecisions: `
-SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed,
- decisions.title, decisions.content, decisions.votetype, decisions.status,
- decisions.due, decisions.modified
+SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
+ decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM decisions
WHERE decisions.status=0 AND :now > due`,
sqlGetNextPendingDecisionDue: `
SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 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
+`,
}
var db *sqlx.DB
@@ -644,3 +633,41 @@ func GetNextPendingDecisionDue() (due *time.Time, err error) {
return
}
+
+func GetReminderVoters() (voters *[]Voter, err error) {
+ getReminderVotersStmt, err := db.Preparex(sqlStatements[sqlGetReminderVoters])
+ if err != nil {
+ logger.Println("Error preparing statement:", err)
+ return
+ }
+ defer getReminderVotersStmt.Close()
+
+ voterSlice := make([]Voter, 0)
+
+ if err = getReminderVotersStmt.Select(&voterSlice); err != nil {
+ logger.Println("Error getting voters:", err)
+ return
+ }
+ voters = &voterSlice
+
+ return
+}
+
+func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) {
+ findUnvotedDecisionsForVoterStmt, err := db.Preparex(sqlStatements[sqlFindUnvotedDecisionsForVoter])
+ if err != nil {
+ logger.Println("Error preparing statement:", err)
+ return
+ }
+ defer findUnvotedDecisionsForVoterStmt.Close()
+
+ decisionsSlice := make([]Decision, 0)
+
+ if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil {
+ logger.Println("Error getting unvoted decisions:", err)
+ return
+ }
+ decisions = &decisionsSlice
+
+ return
+}
diff --git a/notifications.go b/notifications.go
index beb9b3f..d4b9715 100644
--- a/notifications.go
+++ b/notifications.go
@@ -15,7 +15,15 @@ type NotificationMail interface {
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)
func CloseMailNotifier() {
@@ -46,6 +54,24 @@ 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
@@ -78,9 +104,7 @@ func (n *NotificationClosedDecision) GetData() interface{} {
}{&n.decision, &n.voteSums}
}
-func (n *NotificationClosedDecision) GetTemplate() string {
- return "closed_motion_mail.txt"
-}
+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)
@@ -106,9 +130,7 @@ func (n *NotificationCreateMotion) GetData() interface{} {
}{&n.decision, n.voter.Name, voteURL, unvotedURL}
}
-func (n *NotificationCreateMotion) GetTemplate() string {
- return "create_motion_mail.txt"
-}
+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)
@@ -134,9 +156,7 @@ func (n *NotificationUpdateMotion) GetData() interface{} {
}{&n.decision, n.voter.Name, voteURL, unvotedURL}
}
-func (n *NotificationUpdateMotion) GetTemplate() string {
- return "update_motion_mail.txt"
-}
+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)
@@ -158,9 +178,7 @@ func (n *NotificationWithDrawMotion) GetData() interface{} {
}{&n.decision, n.voter.Name}
}
-func (n *NotificationWithDrawMotion) GetTemplate() string {
- return "withdraw_motion_mail.txt"
-}
+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)
@@ -169,3 +187,25 @@ func (n *NotificationWithDrawMotion) GetSubject() string {
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
+}
+
+func (n *RemindVoterNotification) GetData() interface{} {
+ return struct {
+ Decisions []Decision
+ Name string
+ BaseURL string
+ }{n.decisions, n.voter.Name, config.BaseURL}
+}
+
+func (n *RemindVoterNotification) GetTemplate() string { return "remind_voter_mail.txt" }
+
+func (n *RemindVoterNotification) GetSubject() string { return "Outstanding CAcert board votes" }
+
+func (n *RemindVoterNotification) GetRecipient() (address string, name string) {
+ address, name = n.voter.Reminder, n.voter.Name
+ return
+}
diff --git a/remind.php b/remind.php
deleted file mode 100755
index b3ce74c..0000000
--- a/remind.php
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/php
-<?
-require_once("database.php");
-$db = new DB();
-
-$id = 0;
-$page = 1;
-
-$voters = $db->getStatement('get reminder voters');
-$voters->execute();
-
-$outstanding = $db->getStatement('list my unvoted decisions');
-$outstanding->bindParam(':id',$id);
-$outstanding->bindParam(':page',$page);
-
-while ($v = $voters->fetch()) {
- $id = $v['id'];
- $outstanding->execute();
- $msg ='';
- while ($row=$outstanding->fetch()) {
- $msg .= ($row['votetype'] ? 'vote ' : 'motion ') . $row['tag'] . ' ' . $row['title'] . "\nDue: " . $row['due'] . "\nhttps://community.cacert.org/board/motions.php?motion=" . $row['tag'] . "\n\n";
- }
- if ($msg) {
- // form email
- $name = $v['name'];
- $body = <<<BODY
-Dear $name,
-
-You have not voted in the following CAcert Board vote(s)/motion(s):
-
-$msg
-
-
-To view all your outstanding motions: https://community.cacert.org/board/motions.php?unvoted=1
-
-Kind regards,
-the vote system
-
-BODY;
- $db->remind_notify($v['email'],"Outstanding CAcert board votes",$body);
- }
-}
-?>
diff --git a/templates/remind_voter_mail.txt b/templates/remind_voter_mail.txt
new file mode 100644
index 0000000..ddf5366
--- /dev/null
+++ b/templates/remind_voter_mail.txt
@@ -0,0 +1,15 @@
+{{ $baseurl := .BaseURL }}
+Dear {{ .Name }},
+
+You have not voted in the following CAcert Board vote(s)/motion(s):
+
+{{ range .Decisions -}}
+{{ .VoteType }} {{ .Tag }} {{ .Title }}
+Due: {{ .Due }}
+{{ $baseurl }}/motions/{{ .Tag }}
+{{ end }}
+
+To view all your outstanding motions: {{ $baseurl }}/motions/?unvoted=1
+
+Kind regards,
+the vote system \ No newline at end of file