Implement decision closing job

This commit is contained in:
Jan Dittberner 2017-04-20 11:35:33 +02:00
parent 2de96dc13d
commit dcdd5f715f
6 changed files with 192 additions and 32 deletions

View file

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

View file

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

View file

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

95
jobs.go Normal file
View file

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

View file

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

View file

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