Implement decision closing job

debian
Jan Dittberner 7 years ago
parent 2de96dc13d
commit dcdd5f715f

@ -518,6 +518,10 @@ func main() {
go MailNotifier() go MailNotifier()
defer CloseMailNotifier() defer CloseMailNotifier()
quitChannel := make(chan int)
go JobScheduler(quitChannel)
defer func() { quitChannel <- 1 }()
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{})) http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
http.Handle("/newmotion/", motionsHandler{}) http.Handle("/newmotion/", motionsHandler{})
http.Handle("/static/", http.FileServer(http.Dir("."))) http.Handle("/static/", http.FileServer(http.Dir(".")))

@ -1,8 +0,0 @@
#!/usr/bin/php
<?
require_once("database.php");
$db = new DB();
$db->closeVotes();
?>

@ -1,2 +0,0 @@
# echo "select strftime('%H:%M %m%d%Y',due) from decisions where status=0;" | sqlite3 database.sqlite | xargs -n1 -I^ sudo -u www-data at -f closevotes.php-script ^ +1minute
/var/www/board/closevotes.php

@ -0,0 +1,95 @@
package main
import "time"
type Job interface {
Schedule()
Stop()
Run()
}
type jobIdentifier int
const (
JobIdCloseDecisions jobIdentifier = iota
)
var rescheduleChannel = make(chan jobIdentifier, 1)
func JobScheduler(quitChannel chan int) {
var jobs = map[jobIdentifier]Job{
JobIdCloseDecisions: NewCloseDecisionsJob(),
}
logger.Println("INFO started job scheduler")
for {
select {
case jobId := <-rescheduleChannel:
job := jobs[jobId]
logger.Println("INFO reschedule job", job)
job.Schedule()
case <-quitChannel:
for _, job := range jobs {
job.Stop()
}
logger.Println("INFO stop job scheduler")
return
}
}
}
type CloseDecisionsJob struct {
timer *time.Timer
}
func NewCloseDecisionsJob() *CloseDecisionsJob {
job := &CloseDecisionsJob{}
job.Schedule()
return job
}
func (j *CloseDecisionsJob) Schedule() {
var nextDue *time.Time
nextDue, err := GetNextPendingDecisionDue()
if err != nil {
logger.Fatal("ERROR Could not get next pending due date")
if j.timer != nil {
j.timer.Stop()
j.timer = nil
}
return
}
if nextDue == nil {
if j.timer != nil {
j.timer.Stop()
j.timer = nil
}
} else {
logger.Println("INFO scheduling CloseDecisionsJob for", nextDue)
when := nextDue.Sub(time.Now())
if j.timer != nil {
j.timer.Reset(when)
} else {
j.timer = time.AfterFunc(when, j.Run)
}
}
}
func (j *CloseDecisionsJob) Stop() {
if j.timer != nil {
j.timer.Stop()
}
}
func (j *CloseDecisionsJob) Run() {
logger.Println("INFO running CloseDecisionsJob")
err := CloseDecisions()
if err != nil {
logger.Println("ERROR closing decisions", err)
}
rescheduleChannel <- JobIdCloseDecisions
}
func (j *CloseDecisionsJob) String() string {
return "CloseDecisionsJob"
}

@ -2,6 +2,7 @@ package main
import ( import (
"database/sql" "database/sql"
"fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"time" "time"
) )
@ -22,6 +23,7 @@ const (
sqlUpdateDecision sqlUpdateDecision
sqlUpdateDecisionStatus sqlUpdateDecisionStatus
sqlSelectClosableDecisions sqlSelectClosableDecisions
sqlGetNextPendingDecisionDue
) )
var sqlStatements = map[sqlKey]string{ var sqlStatements = map[sqlKey]string{
@ -105,11 +107,13 @@ UPDATE decisions
SET status=:status, modified=:modified WHERE id=:id SET status=:status, modified=:modified WHERE id=:id
`, `,
sqlSelectClosableDecisions: ` sqlSelectClosableDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title, SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed,
decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified decisions.title, decisions.content, decisions.votetype, decisions.status,
decisions.due, decisions.modified
FROM decisions FROM decisions
JOIN voters ON decisions.proponent=voters.id
WHERE decisions.status=0 AND :now > due`, WHERE decisions.status=0 AND :now > due`,
sqlGetNextPendingDecisionDue: `
SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
} }
var db *sqlx.DB var db *sqlx.DB
@ -241,6 +245,18 @@ func (v *VoteSums) VoteCount() int {
return v.Ayes + v.Nayes + v.Abstains return v.Ayes + v.Nayes + v.Abstains
} }
func (v *VoteSums) TotalVotes() int {
return v.Ayes + v.Nayes
}
func (v *VoteSums) Percent() int {
totalVotes := v.TotalVotes()
if totalVotes == 0 {
return 0
}
return v.Ayes * 100 / totalVotes
}
type VoteForDisplay struct { type VoteForDisplay struct {
Vote Vote
Name string Name string
@ -413,6 +429,7 @@ func (d *Decision) Create() (err error) {
logger.Println("Error getting id of inserted motion:", err) logger.Println("Error getting id of inserted motion:", err)
return return
} }
rescheduleChannel <- JobIdCloseDecisions
getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById]) getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
if err != nil { if err != nil {
@ -465,6 +482,7 @@ func (d *Decision) Update() (err error) {
} else if affectedRows != 1 { } else if affectedRows != 1 {
logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows) logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
} }
rescheduleChannel <- JobIdCloseDecisions
err = d.LoadWithId() err = d.LoadWithId()
return return
@ -486,14 +504,20 @@ func (d *Decision) UpdateStatus() (err error) {
affectedRows, err := result.RowsAffected() affectedRows, err := result.RowsAffected()
if err != nil { if err != nil {
logger.Print("Problem determining the affected rows") logger.Print("Problem determining the affected rows")
return
} else if affectedRows != 1 { } else if affectedRows != 1 {
logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows) logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
} }
rescheduleChannel <- JobIdCloseDecisions
err = d.LoadWithId() err = d.LoadWithId()
return return
} }
func (d *Decision) String() string {
return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id)
}
func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
findVoterStmt, err := db.Preparex(sqlStatements[sqlLoadEnabledVoterByEmail]) findVoterStmt, err := db.Preparex(sqlStatements[sqlLoadEnabledVoterByEmail])
if err != nil { if err != nil {
@ -515,13 +539,6 @@ func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
} }
func (d *Decision) Close() (err error) { 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() quorum, majority := d.VoteType.QuorumAndMajority()
voteSums, err := d.VoteSums() voteSums, err := d.VoteSums()
@ -535,7 +552,7 @@ func (d *Decision) Close() (err error) {
if votes < quorum { if votes < quorum {
d.Status = voteStatusDeclined d.Status = voteStatusDeclined
} else { } else {
votes = voteSums.Ayes + voteSums.Nayes votes = voteSums.TotalVotes()
if (voteSums.Ayes / votes) > (majority / 100) { if (voteSums.Ayes / votes) > (majority / 100) {
d.Status = voteStatusApproved d.Status = voteStatusApproved
} else { } else {
@ -543,6 +560,13 @@ 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()
result, err := closeDecisionStmt.Exec(d) result, err := closeDecisionStmt.Exec(d)
if err != nil { if err != nil {
logger.Println("Error closing vote:", err) logger.Println("Error closing vote:", err)
@ -562,32 +586,61 @@ func (d *Decision) Close() (err error) {
} }
func CloseDecisions() (err error) { func CloseDecisions() (err error) {
getClosedDecisionsStmt, err := db.PrepareNamed(sqlStatements[sqlSelectClosableDecisions]) getClosableDecisionsStmt, err := db.PrepareNamed(sqlStatements[sqlSelectClosableDecisions])
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return
} }
defer getClosedDecisionsStmt.Close() defer getClosableDecisionsStmt.Close()
params := make(map[string]interface{}, 1) decisions := make([]*Decision, 0)
params["now"] = time.Now().UTC() rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
rows, err := getClosedDecisionsStmt.Queryx(params)
if err != nil { if err != nil {
logger.Println("Error fetching closed decisions", err) logger.Println("Error fetching closable decisions", err)
return return
} }
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
d := &Decision{} decision := &Decision{}
if err = rows.StructScan(d); err != nil { if err = rows.StructScan(decision); err != nil {
logger.Println("Error filling decision from database row", err) logger.Println("Error scanning row", err)
return return
} }
if err = d.Close(); err != nil { decisions = append(decisions, decision)
logger.Printf("Error closing decision %s: %s\n", d.Tag, err) }
rows.Close()
for _, decision := range decisions {
logger.Println("DEBUG found closable decision", decision)
if err = decision.Close(); err != nil {
logger.Printf("Error closing decision %s: %s\n", decision, err)
return return
} }
} }
return return
} }
func GetNextPendingDecisionDue() (due *time.Time, err error) {
getNextPendingDecisionDueStmt, err := db.Preparex(sqlStatements[sqlGetNextPendingDecisionDue])
if err != nil {
logger.Println("Error preparing statement:", err)
return
}
defer getNextPendingDecisionDueStmt.Close()
row := getNextPendingDecisionDueStmt.QueryRow()
var dueTimestamp time.Time
if err = row.Scan(&dueTimestamp); err != nil {
if err == sql.ErrNoRows {
logger.Println("DEBUG No pending decisions")
return nil, nil
}
logger.Println("Error parsing result", err)
return
}
due = &dueTimestamp
return
}

@ -0,0 +1,18 @@
Dear Board,
{{ with .Decision }}The motion with the identifier {{.Tag}} has been {{.Status}}.
Motion:
{{.Title}}
{{.Content}}
Vote type: {{.VoteType}}{{end}}
{{ with .VoteSums }} Ayes: {{ .Ayes }}
Nayes: {{ .Nayes }}
Abstentions: {{ .Abstains }}
Percentage: {{ .Percent }}%{{ end }}
Kind regards,
the voting system.
Loading…
Cancel
Save