summaryrefslogtreecommitdiff
path: root/models.go
diff options
context:
space:
mode:
Diffstat (limited to 'models.go')
-rw-r--r--models.go656
1 files changed, 528 insertions, 128 deletions
diff --git a/models.go b/models.go
index fe6cd23..75473b4 100644
--- a/models.go
+++ b/models.go
@@ -1,67 +1,135 @@
package main
import (
+ "bitbucket.org/liamstask/goose/lib/goose"
"database/sql"
+ "fmt"
"github.com/jmoiron/sqlx"
"time"
)
+type sqlKey int
+
const (
- sqlGetDecisions = `
-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`
- sqlGetDecision = `
-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;`
- sqlGetVoter = `
-SELECT voters.id, voters.name
-FROM voters
-JOIN emails ON voters.id=emails.voter
-WHERE emails.address=$1 AND voters.enabled=1`
- sqlVoteCount = `
-SELECT vote, COUNT(vote)
-FROM votes
-WHERE decision=$1 GROUP BY vote`
- sqlCountOlderThanDecision = `
-SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`
- sqlGetVotesForDecision = `
-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`
- sqlListUnvotedDecisions = `
-SELECT decisions.id, decisions.tag, decisions.proponent,
- voters.name AS proposer, decisions.proposed, decisions.title,
- decisions.content AS 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 decision FROM votes WHERE votes.voter=$2)
-ORDER BY proposed DESC
-LIMIT 10 OFFSET 10 * $1`
+ sqlLoadDecisions sqlKey = iota
+ sqlLoadUnvotedDecisions
+ sqlLoadDecisionByTag
+ sqlLoadDecisionById
+ sqlLoadVoteCountsForDecision
+ sqlLoadVotesForDecision
+ sqlLoadEnabledVoterByEmail
+ sqlCountOlderThanDecision
+ sqlCountOlderThanUnvotedDecision
+ sqlCreateDecision
+ sqlUpdateDecision
+ sqlUpdateDecisionStatus
+ sqlSelectClosableDecisions
+ sqlGetNextPendingDecisionDue
+ sqlGetReminderVoters
+ sqlFindUnvotedDecisionsForVoter
+ sqlGetEnabledVoterById
+ sqlCreateVote
+ sqlLoadVote
+ sqlGetVotersForProxy
)
-var db *sqlx.DB
+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
+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)
+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;`,
+ 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;`,
+ sqlLoadVoteCountsForDecision: `
+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`,
+ 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`,
+ 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)`,
+ sqlCreateDecision: `
+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')
+ )
+)`,
+ sqlUpdateDecision: `
+UPDATE decisions
+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`,
+ 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 int
-type VoteStatus int
+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
@@ -74,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
@@ -87,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
@@ -139,21 +199,133 @@ 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
+ voteStatusApproved = 1
+ voteStatusWithdrawn = -2
+)
+
func (v VoteStatus) String() string {
switch v {
- case -1:
+ case voteStatusDeclined:
return "declined"
- case 0:
+ case voteStatusPending:
return "pending"
- case 1:
+ case voteStatusApproved:
return "approved"
- case -2:
+ case voteStatusWithdrawn:
return "withdrawn"
default:
return "unknown"
}
}
+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
@@ -164,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
@@ -177,11 +372,7 @@ type DecisionForDisplay struct {
}
func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
- decisionStmt, err := db.Preparex(sqlGetDecision)
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
- }
+ decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)
defer decisionStmt.Close()
decision = &DecisionForDisplay{}
@@ -190,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()
@@ -202,47 +394,23 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err
// This function uses OFFSET for pagination which is not a good idea for larger data sets.
//
// TODO: migrate to timestamp base pagination
-func FindDecisionsForDisplayOnPage(page int64) (decisions []*DecisionForDisplay, err error) {
- decisionsStmt, err := db.Preparex(sqlGetDecisions)
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
+func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
+ var decisionsStmt *sqlx.Stmt
+ if unvoted && voter != nil {
+ decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions)
+ } else {
+ decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
}
defer decisionsStmt.Close()
- rows, err := decisionsStmt.Queryx(page - 1)
- if err != nil {
- logger.Printf("Error loading motions for page %d: %v\n", page, err)
- return
- }
- defer rows.Close()
-
- 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)
- return
- }
- d.VoteSums, err = d.Decision.VoteSums()
- if err != nil {
- return
- }
- decisions = append(decisions, &d)
- }
- return
-}
-
-func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decisions []*DecisionForDisplay, err error) {
- decisionsStmt, err := db.Preparex(sqlListUnvotedDecisions)
- if err != nil {
- logger.Println("Error preparing statement:", err)
- return
+ var rows *sqlx.Rows
+ if unvoted && voter != nil {
+ rows, err = decisionsStmt.Queryx(voter.Id, page-1)
+ } else {
+ rows, err = decisionsStmt.Queryx(page - 1)
}
- defer decisionsStmt.Close()
-
- rows, err := decisionsStmt.Queryx(page - 1, voter.Id)
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()
@@ -250,7 +418,7 @@ func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decis
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()
@@ -263,16 +431,12 @@ func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decis
}
func (d *Decision) VoteSums() (sums *VoteSums, err error) {
- votesStmt, err := db.Preparex(sqlVoteCount)
- 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()
@@ -282,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 {
@@ -298,45 +462,137 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
}
func (d *DecisionForDisplay) LoadVotes() (err error) {
- votesStmt, err := db.Preparex(sqlGetVotesForDecision)
+ votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)
+ defer votesStmt.Close()
+
+ err = votesStmt.Select(&d.Votes, d.Id)
if err != nil {
- logger.Println("Error preparing statement:", err)
+ log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
return
}
- defer votesStmt.Close()
- err = votesStmt.Select(&d.Votes, d.Id)
+ return
+}
+
+func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
+ if unvoted && voter != nil {
+ olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision)
+ defer olderStmt.Close()
+
+ if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
+ 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 {
+ log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
+ return
+ }
+ }
+
+ return
+}
+
+func (d *Decision) Create() (err error) {
+ insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)
+ defer insertDecisionStmt.Close()
+
+ result, err := insertDecisionStmt.Exec(d)
if err != nil {
- logger.Printf("Error selecting votes for motion %s: %v\n", d.Tag, err)
+ log.Errorf("creating motion failed: %v", err)
+ return
+ }
+
+ lastInsertId, err := result.LastInsertId()
+ if err != nil {
+ log.Errorf("getting id of inserted motion failed: %v", err)
+ return
}
+ rescheduleChannel <- JobIdCloseDecisions
+
+ getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
+ defer getDecisionStmt.Close()
+
+ err = getDecisionStmt.Get(d, lastInsertId)
+ if err != nil {
+ log.Errorf("getting inserted motion failed: %v", err)
+ return
+ }
+
return
}
-func (d *Decision) OlderExists() (result bool, err error) {
- olderStmt, err := db.Preparex(sqlCountOlderThanDecision)
+func (d *Decision) LoadWithId() (err error) {
+ getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
+ defer getDecisionStmt.Close()
+
+ err = getDecisionStmt.Get(d, d.Id)
if err != nil {
- logger.Println("Error preparing statement:", err)
+ log.Errorf("loading updated motion failed: %v", err)
return
}
- 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)
+ return
+}
+
+func (d *Decision) Update() (err error) {
+ updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)
+ defer updateDecisionStmt.Close()
+
+ result, err := updateDecisionStmt.Exec(d)
+ if err != nil {
+ log.Errorf("updating motion failed: %v", err)
+ return
+ }
+ affectedRows, err := result.RowsAffected()
+ if err != nil {
+ log.Error("Problem determining the affected rows")
+ return
+ } else if affectedRows != 1 {
+ log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
}
+ rescheduleChannel <- JobIdCloseDecisions
+
+ err = d.LoadWithId()
return
}
-func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
- findVoterStmt, err := db.Preparex(sqlGetVoter)
+func (d *Decision) UpdateStatus() (err error) {
+ updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
+ defer updateStatusStmt.Close()
+
+ result, err := updateStatusStmt.Exec(d)
+ if err != nil {
+ log.Errorf("setting motion status failed: %v", err)
+ return
+ }
+ affectedRows, err := result.RowsAffected()
if err != nil {
- logger.Println("Error preparing statement:", err)
+ log.Errorf("determining the affected rows failed: %v", err)
return
+ } else if affectedRows != 1 {
+ 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 := 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
@@ -344,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
+}