Implement reminder job
This commit is contained in:
parent
dcdd5f715f
commit
b6ad5d8ad3
7 changed files with 220 additions and 126 deletions
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
63
jobs.go
63
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
141
models.go
141
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
43
remind.php
43
remind.php
|
@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
15
templates/remind_voter_mail.txt
Normal file
15
templates/remind_voter_mail.txt
Normal file
|
@ -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…
Reference in a new issue