summaryrefslogtreecommitdiff
path: root/models.go
diff options
context:
space:
mode:
Diffstat (limited to 'models.go')
-rw-r--r--models.go558
1 files changed, 398 insertions, 160 deletions
diff --git a/models.go b/models.go
index d6eee15..75473b4 100644
--- a/models.go
+++ b/models.go
@@ -1,7 +1,9 @@
package main
import (
+ "bitbucket.org/liamstask/goose/lib/goose"
"database/sql"
+ "fmt"
"github.com/jmoiron/sqlx"
"time"
)
@@ -21,110 +23,113 @@ const (
sqlCreateDecision
sqlUpdateDecision
sqlUpdateDecisionStatus
+ sqlSelectClosableDecisions
+ sqlGetNextPendingDecisionDue
+ sqlGetReminderVoters
+ sqlFindUnvotedDecisionsForVoter
+ sqlGetEnabledVoterById
+ sqlCreateVote
+ sqlLoadVote
+ sqlGetVotersForProxy
)
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`,
+ sqlGetEnabledVoterById: `
+SELECT id, name, enabled, reminder
+FROM voters
+WHERE enabled=1 AND id=$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
-`,
-}
-
-var db *sqlx.DB
-
-func init() {
- for _, sqlStatement := range sqlStatements {
- var stmt *sqlx.Stmt
- stmt, err := db.Preparex(sqlStatement)
- if err != nil {
- logger.Fatalf("ERROR parsing statement %s: %s", sqlStatement, err)
- }
- stmt.Close()
- }
+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
+FROM decisions
+WHERE decisions.status=0 AND :now > due`,
+ sqlGetNextPendingDecisionDue: `
+SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
+ sqlGetVotersForProxy: `
+SELECT id, name, reminder
+FROM voters WHERE enabled=1 AND id != $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`,
+ sqlCreateVote: `
+INSERT OR REPLACE INTO votes (decision, voter, vote, voted, notes)
+VALUES (:decision, :voter, :vote, :voted, :notes)`,
+ sqlLoadVote: `
+SELECT decision, voter, vote, voted, notes
+FROM votes
+WHERE decision=$1 AND voter=$2`,
}
type VoteType uint8
type VoteStatus int8
type Decision struct {
- Id int
+ Id int64
Proposed time.Time
- ProponentId int `db:"proponent"`
+ ProponentId int64 `db:"proponent"`
Title string
Content string
Quorum int
@@ -137,12 +142,12 @@ type Decision struct {
}
type Email struct {
- VoterId int `db:"voter"`
+ VoterId int64 `db:"voter"`
Address string
}
type Voter struct {
- Id int
+ Id int64
Name string
Enabled bool
Reminder string // reminder email address
@@ -150,14 +155,6 @@ type Voter struct {
type VoteChoice int
-type Vote struct {
- DecisionId int `db:"decision"`
- VoterId int `db:"voter"`
- Vote VoteChoice
- Voted time.Time
- Notes string
-}
-
const (
voteAye = 1
voteNaye = -1
@@ -202,6 +199,18 @@ func (v VoteChoice) String() string {
}
}
+var VoteValues = map[string]VoteChoice{
+ "aye": voteAye,
+ "naye": voteNaye,
+ "abstain": voteAbstain,
+}
+
+var VoteChoices = map[int64]VoteChoice{
+ 1: voteAye,
+ 0: voteAbstain,
+ -1: voteNaye,
+}
+
const (
voteStatusDeclined = -1
voteStatusPending = 0
@@ -224,6 +233,99 @@ func (v VoteStatus) String() string {
}
}
+type Vote struct {
+ DecisionId int64 `db:"decision"`
+ VoterId int64 `db:"voter"`
+ Vote VoteChoice
+ Voted time.Time
+ Notes string
+}
+
+type dbHandler struct {
+ db *sqlx.DB
+}
+
+var db *dbHandler
+
+func NewDB(database *sqlx.DB) *dbHandler {
+ handler := &dbHandler{db: database}
+ failed_statements := make([]string, 0)
+ for _, sqlStatement := range sqlStatements {
+ var stmt *sqlx.Stmt
+ stmt, err := database.Preparex(sqlStatement)
+ if err != nil {
+ log.Critical("ERROR parsing statement %s: %s", sqlStatement, err)
+ failed_statements = append(failed_statements, sqlStatement)
+ }
+ stmt.Close()
+ }
+ if len(failed_statements) > 0 {
+ log.Panicf("%d statements failed to prepare", len(failed_statements))
+ }
+
+ migrateConf := &goose.DBConf{
+ MigrationsDir: config.MigrationsPath,
+ Env: "production",
+ Driver: goose.DBDriver{
+ Name: "sqlite3",
+ OpenStr: config.DatabaseFile,
+ Import: "github.com/mattn/go-sqlite3",
+ Dialect: &goose.Sqlite3Dialect{},
+ },
+ }
+
+ latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir)
+ if err != nil {
+ log.Panicf("getting the most recent database repository version failed: %v", err)
+ }
+
+ err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, database.DB)
+ if err != nil {
+ log.Panicf("running database migration failed: %v", err)
+ }
+ return handler
+}
+
+func (d *dbHandler) Close() error {
+ return d.db.Close()
+}
+
+func (d *dbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
+ statement, err := d.db.PrepareNamed(sqlStatements[statementKey])
+ if err != nil {
+ log.Panicf("Preparing statement failed: %v", err)
+ }
+ return statement
+}
+
+func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
+ statement, err := d.db.Preparex(sqlStatements[statementKey])
+ if err != nil {
+ log.Panicf("Preparing statement failed: %v", err)
+ }
+ return statement
+}
+
+func (v *Vote) Save() (err error) {
+ insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote)
+ defer insertVoteStmt.Close()
+
+ if _, err = insertVoteStmt.Exec(v); err != nil {
+ log.Errorf("saving vote failed: %v", err)
+ return
+ }
+
+ getVoteStmt := db.getPreparedStatement(sqlLoadVote)
+ defer getVoteStmt.Close()
+
+ if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil {
+ log.Errorf("getting inserted vote failed: %v", err)
+ return
+ }
+
+ return
+}
+
type VoteSums struct {
Ayes int
Nayes int
@@ -234,6 +336,29 @@ 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
+}
+
+func (v *VoteSums) CalculateResult(quorum int, majority int) (status VoteStatus, reasoning string) {
+ if v.VoteCount() < quorum {
+ status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
+ } else if (v.Ayes / v.TotalVotes()) < (majority / 100) {
+ status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed majority of %d%% has not been reached.", majority)
+ } else {
+ status, reasoning = voteStatusApproved, "Quorum and majority have been reached"
+ }
+ return
+}
+
type VoteForDisplay struct {
Vote
Name string
@@ -247,11 +372,7 @@ type DecisionForDisplay struct {
}
func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
- decisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionByTag])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)
defer decisionStmt.Close()
decision = &DecisionForDisplay{}
@@ -260,7 +381,8 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err
decision = nil
err = nil
} else {
- logger.Printf("Error getting motion %s: %v\n", tag, err)
+ log.Errorf("getting motion %s failed: %v", tag, err)
+ return
}
}
decision.VoteSums, err = decision.Decision.VoteSums()
@@ -275,13 +397,9 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err
func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
var decisionsStmt *sqlx.Stmt
if unvoted && voter != nil {
- decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadUnvotedDecisions])
+ decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions)
} else {
- decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadDecisions])
- }
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
+ decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
}
defer decisionsStmt.Close()
@@ -292,7 +410,7 @@ func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (deci
rows, err = decisionsStmt.Queryx(page - 1)
}
if err != nil {
- logger.Printf("Error loading motions for page %d: %v\n", page, err)
+ log.Errorf("loading motions for page %d failed: %v", page, err)
return
}
defer rows.Close()
@@ -300,7 +418,7 @@ func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (deci
for rows.Next() {
var d DecisionForDisplay
if err = rows.StructScan(&d); err != nil {
- logger.Printf("Error loading motions for page %d: %v\n", page, err)
+ log.Errorf("loading motions for page %d failed: %v", page, err)
return
}
d.VoteSums, err = d.Decision.VoteSums()
@@ -313,16 +431,12 @@ func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (deci
}
func (d *Decision) VoteSums() (sums *VoteSums, err error) {
- votesStmt, err := db.Preparex(sqlStatements[sqlLoadVoteCountsForDecision])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision)
defer votesStmt.Close()
voteRows, err := votesStmt.Queryx(d.Id)
if err != nil {
- logger.Printf("Error fetching vote sums for motion %s: %v\n", d.Tag, err)
+ log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
return
}
defer voteRows.Close()
@@ -332,7 +446,7 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
var vote VoteChoice
var count int
if err = voteRows.Scan(&vote, &count); err != nil {
- logger.Printf("Error fetching vote sums for motion %s: %v\n", d.Tag, err)
+ log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
return
}
switch vote {
@@ -348,39 +462,33 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
}
func (d *DecisionForDisplay) LoadVotes() (err error) {
- votesStmt, err := db.Preparex(sqlStatements[sqlLoadVotesForDecision])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)
defer votesStmt.Close()
+
err = votesStmt.Select(&d.Votes, d.Id)
if err != nil {
- logger.Printf("Error selecting votes for motion %s: %v\n", d.Tag, err)
+ log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
+ return
}
return
}
func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
- var olderStmt *sqlx.Stmt
if unvoted && voter != nil {
- olderStmt, err = db.Preparex(sqlStatements[sqlCountOlderThanUnvotedDecision])
- } else {
- olderStmt, err = db.Preparex(sqlStatements[sqlCountOlderThanDecision])
- }
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
- defer olderStmt.Close()
+ olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision)
+ defer olderStmt.Close()
- if unvoted && voter != nil {
if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
- logger.Printf("Error finding older motions than %s: %v\n", d.Tag, err)
+ log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
+ return
}
} else {
+ olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision)
+ defer olderStmt.Close()
+
if err = olderStmt.Get(&result, d.Proposed); err != nil {
- logger.Printf("Error finding older motions than %s: %v\n", d.Tag, err)
+ log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
+ return
}
}
@@ -388,117 +496,103 @@ func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err err
}
func (d *Decision) Create() (err error) {
- insertDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlCreateDecision])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)
defer insertDecisionStmt.Close()
result, err := insertDecisionStmt.Exec(d)
if err != nil {
- logger.Println("Error creating motion:", err)
+ log.Errorf("creating motion failed: %v", err)
return
}
lastInsertId, err := result.LastInsertId()
if err != nil {
- logger.Println("Error getting id of inserted motion:", err)
+ log.Errorf("getting id of inserted motion failed: %v", err)
return
}
+ rescheduleChannel <- JobIdCloseDecisions
- getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
defer getDecisionStmt.Close()
err = getDecisionStmt.Get(d, lastInsertId)
if err != nil {
- logger.Println("Error getting inserted motion:", err)
+ log.Errorf("getting inserted motion failed: %v", err)
+ return
}
return
}
func (d *Decision) LoadWithId() (err error) {
- getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
defer getDecisionStmt.Close()
err = getDecisionStmt.Get(d, d.Id)
if err != nil {
- logger.Println("Error loading updated motion:", err)
+ log.Errorf("loading updated motion failed: %v", err)
+ return
}
return
}
func (d *Decision) Update() (err error) {
- updateDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecision])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)
defer updateDecisionStmt.Close()
result, err := updateDecisionStmt.Exec(d)
if err != nil {
- logger.Println("Error updating motion:", err)
+ log.Errorf("updating motion failed: %v", err)
return
}
affectedRows, err := result.RowsAffected()
if err != nil {
- logger.Print("Problem determining the affected rows")
+ log.Error("Problem determining the affected rows")
return
} else if affectedRows != 1 {
- logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
+ log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
}
+ rescheduleChannel <- JobIdCloseDecisions
err = d.LoadWithId()
return
}
func (d *Decision) UpdateStatus() (err error) {
- updateStatusStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus])
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
defer updateStatusStmt.Close()
result, err := updateStatusStmt.Exec(d)
if err != nil {
- logger.Println("Error setting motion status:", err)
+ log.Errorf("setting motion status failed: %v", err)
return
}
affectedRows, err := result.RowsAffected()
if err != nil {
- logger.Print("Problem determining the affected rows")
+ log.Errorf("determining the affected rows failed: %v", err)
+ return
} else if affectedRows != 1 {
- logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
+ log.Warningf("wrong number of affected rows: %d (1 expected)", 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 {
- logger.Println("Error preparing statement:", err)
- return
- }
+ findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail)
defer findVoterStmt.Close()
voter = &Voter{}
if err = findVoterStmt.Get(voter, emailAddress); err != nil {
if err != sql.ErrNoRows {
- logger.Printf("Error getting voter for address %s: %v\n", emailAddress, err)
+ log.Errorf("getting voter for address %s failed: %v", emailAddress, err)
} else {
err = nil
voter = nil
@@ -506,3 +600,147 @@ func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
}
return
}
+
+func (d *Decision) Close() error {
+ quorum, majority := d.VoteType.QuorumAndMajority()
+
+ var voteSums *VoteSums
+ var err error
+
+ if voteSums, err = d.VoteSums(); err != nil {
+ log.Errorf("getting vote sums failed: %v", err)
+ return err
+ }
+ var reasoning string
+ d.Status, reasoning = voteSums.CalculateResult(quorum, majority)
+
+ closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
+ defer closeDecisionStmt.Close()
+
+ result, err := closeDecisionStmt.Exec(d)
+ if err != nil {
+ log.Errorf("closing vote failed: %v", err)
+ return err
+ }
+ if affectedRows, err := result.RowsAffected(); err != nil {
+ log.Errorf("getting affected rows failed: %v", err)
+ return err
+ } else if affectedRows != 1 {
+ log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
+ }
+
+ NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums, reasoning)
+
+ log.Infof("decision %s closed with result %s: reasoning %s", d.Tag, d.Status, reasoning)
+
+ return nil
+}
+
+func CloseDecisions() (err error) {
+ getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions)
+ defer getClosableDecisionsStmt.Close()
+
+ decisions := make([]*Decision, 0)
+ rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
+ if err != nil {
+ log.Errorf("fetching closable decisions failed: %v", err)
+ return
+ }
+ defer rows.Close()
+ for rows.Next() {
+ decision := &Decision{}
+ if err = rows.StructScan(decision); err != nil {
+ log.Errorf("scanning row failed: %v", err)
+ return
+ }
+ decisions = append(decisions, decision)
+ }
+ rows.Close()
+
+ for _, decision := range decisions {
+ log.Debugf("found closable decision %s", decision.Tag)
+ if err = decision.Close(); err != nil {
+ log.Errorf("closing decision %s failed: %s", decision.Tag, err)
+ return
+ }
+ }
+
+ return
+}
+
+func GetNextPendingDecisionDue() (due *time.Time, err error) {
+ getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue)
+ defer getNextPendingDecisionDueStmt.Close()
+
+ row := getNextPendingDecisionDueStmt.QueryRow()
+
+ due = &time.Time{}
+ if err = row.Scan(due); err != nil {
+ if err == sql.ErrNoRows {
+ log.Debug("No pending decisions")
+ return nil, nil
+ }
+ log.Errorf("parsing result failed: %v", err)
+ return nil, err
+ }
+
+ return
+}
+
+func GetReminderVoters() (voters *[]Voter, err error) {
+ getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters)
+ defer getReminderVotersStmt.Close()
+
+ voterSlice := make([]Voter, 0)
+
+ if err = getReminderVotersStmt.Select(&voterSlice); err != nil {
+ log.Errorf("getting voters failed: %v", err)
+ return
+ }
+ voters = &voterSlice
+
+ return
+}
+
+func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) {
+ findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter)
+ defer findUnvotedDecisionsForVoterStmt.Close()
+
+ decisionsSlice := make([]Decision, 0)
+
+ if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil {
+ log.Errorf("getting unvoted decisions failed: %v", err)
+ return
+ }
+ decisions = &decisionsSlice
+
+ return
+}
+
+func GetVoterById(id int64) (voter *Voter, err error) {
+ getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById)
+ defer getVoterByIdStmt.Close()
+
+ voter = &Voter{}
+ if err = getVoterByIdStmt.Get(voter, id); err != nil {
+ log.Errorf("getting voter failed: %v", err)
+ return
+ }
+
+ return
+}
+
+func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) {
+ getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy)
+ defer getVotersForProxyStmt.Close()
+
+ votersSlice := make([]Voter, 0)
+
+ if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil {
+ log.Errorf("Error getting voters for proxy failed: %v", err)
+ return
+ }
+ voters = &votersSlice
+
+ return
+}