Implement decision closing job
This commit is contained in:
parent
2de96dc13d
commit
dcdd5f715f
6 changed files with 192 additions and 32 deletions
|
@ -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(".")))
|
||||
|
|
|
@ -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
|
95
jobs.go
Normal file
95
jobs.go
Normal 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"
|
||||
}
|
97
models.go
97
models.go
|
@ -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
|
||||
}
|
||||
|
|
18
templates/closed_motion_mail.txt
Normal file
18
templates/closed_motion_mail.txt
Normal 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.
|
Loading…
Reference in a new issue