Implement reminder job

debian
Jan Dittberner 7 years ago committed by Jan Dittberner
parent dcdd5f715f
commit b6ad5d8ad3

@ -470,6 +470,7 @@ 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"`
ReminderSenderAddress string `yaml:"reminder_sender_address"`
DatabaseFile string `yaml:"database_file"`
ClientCACertificates string `yaml:"client_ca_certificates"`
ServerCert string `yaml:"server_certificate"`

@ -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

@ -12,6 +12,7 @@ type jobIdentifier int
const (
JobIdCloseDecisions jobIdentifier = iota
JobIdRemindVotersJob
)
var rescheduleChannel = make(chan jobIdentifier, 1)
@ -19,6 +20,7 @@ var rescheduleChannel = make(chan jobIdentifier, 1)
func JobScheduler(quitChannel chan int) {
var jobs = map[jobIdentifier]Job{
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}
}
}
}

@ -24,49 +24,39 @@ 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
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
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)
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
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
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
@ -80,40 +70,39 @@ 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
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
}

@ -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() {
@ -42,6 +50,24 @@ func MailNotifier() {
}
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 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)
@ -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
}

@ -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);
}
}
?>

@ -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
Loading…
Cancel
Save