2017-04-17 14:24:37 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2017-04-21 19:41:25 +00:00
|
|
|
"bitbucket.org/liamstask/goose/lib/goose"
|
2017-04-17 14:24:37 +00:00
|
|
|
"database/sql"
|
2017-04-20 09:35:33 +00:00
|
|
|
"fmt"
|
2017-04-17 14:24:37 +00:00
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2017-04-18 18:30:08 +00:00
|
|
|
type sqlKey int
|
|
|
|
|
2017-04-17 14:24:37 +00:00
|
|
|
const (
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlLoadDecisions sqlKey = iota
|
|
|
|
sqlLoadUnvotedDecisions
|
|
|
|
sqlLoadDecisionByTag
|
|
|
|
sqlLoadDecisionById
|
|
|
|
sqlLoadVoteCountsForDecision
|
|
|
|
sqlLoadVotesForDecision
|
|
|
|
sqlLoadEnabledVoterByEmail
|
|
|
|
sqlCountOlderThanDecision
|
|
|
|
sqlCountOlderThanUnvotedDecision
|
|
|
|
sqlCreateDecision
|
2017-04-18 22:05:42 +00:00
|
|
|
sqlUpdateDecision
|
2017-04-19 19:35:08 +00:00
|
|
|
sqlUpdateDecisionStatus
|
2017-04-19 21:32:12 +00:00
|
|
|
sqlSelectClosableDecisions
|
2017-04-20 09:35:33 +00:00
|
|
|
sqlGetNextPendingDecisionDue
|
2017-04-20 18:58:22 +00:00
|
|
|
sqlGetReminderVoters
|
|
|
|
sqlFindUnvotedDecisionsForVoter
|
2017-04-21 00:25:49 +00:00
|
|
|
sqlGetEnabledVoterById
|
|
|
|
sqlCreateVote
|
|
|
|
sqlLoadVote
|
|
|
|
sqlGetVotersForProxy
|
2017-04-18 18:30:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var sqlStatements = map[sqlKey]string{
|
|
|
|
sqlLoadDecisions: `
|
2017-04-20 18:58:22 +00:00
|
|
|
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
|
2017-04-17 14:24:37 +00:00
|
|
|
ORDER BY proposed DESC
|
2017-04-18 18:30:08 +00:00
|
|
|
LIMIT 10 OFFSET 10 * $1`,
|
|
|
|
sqlLoadUnvotedDecisions: `
|
2017-04-20 18:58:22 +00:00
|
|
|
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)
|
2017-04-18 18:30:08 +00:00
|
|
|
ORDER BY proposed DESC
|
|
|
|
LIMIT 10 OFFSET 10 * $2;`,
|
|
|
|
sqlLoadDecisionByTag: `
|
2017-04-20 18:58:22 +00:00
|
|
|
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;`,
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlLoadDecisionById: `
|
2017-04-20 18:58:22 +00:00
|
|
|
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;`,
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlLoadVoteCountsForDecision: `
|
2017-04-20 18:58:22 +00:00
|
|
|
SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlLoadVotesForDecision: `
|
2017-04-17 14:24:37 +00:00
|
|
|
SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes
|
2017-04-20 18:58:22 +00:00
|
|
|
FROM votes
|
|
|
|
JOIN voters ON votes.voter=voters.id
|
|
|
|
WHERE decision=$1`,
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlLoadEnabledVoterByEmail: `
|
|
|
|
SELECT voters.id, voters.name, voters.enabled, voters.reminder
|
2017-04-20 18:58:22 +00:00
|
|
|
FROM voters
|
|
|
|
JOIN emails ON voters.id=emails.voter
|
|
|
|
WHERE emails.address=$1 AND voters.enabled=1`,
|
2017-04-21 00:25:49 +00:00
|
|
|
sqlGetEnabledVoterById: `
|
|
|
|
SELECT id, name, enabled, reminder
|
|
|
|
FROM voters
|
|
|
|
WHERE enabled=1 AND id=$1`,
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlCountOlderThanDecision: `
|
|
|
|
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
|
|
|
|
sqlCountOlderThanUnvotedDecision: `
|
2017-04-20 18:58:22 +00:00
|
|
|
SELECT COUNT(*) > 0 FROM decisions
|
|
|
|
WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
|
2017-04-18 18:30:08 +00:00
|
|
|
sqlCreateDecision: `
|
2017-04-20 18:58:22 +00:00
|
|
|
INSERT INTO decisions (proposed, proponent, title, content, votetype, status, due, modified,tag)
|
|
|
|
VALUES (
|
|
|
|
:proposed, :proponent, :title, :content, :votetype, 0, :due, :proposed,
|
2017-04-18 18:30:08 +00:00
|
|
|
'm' || strftime('%Y%m%d', :proposed) || '.' || (
|
|
|
|
SELECT COUNT(*)+1 AS num
|
|
|
|
FROM decisions
|
2017-04-20 18:58:22 +00:00
|
|
|
WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day')
|
2017-04-18 00:34:21 +00:00
|
|
|
)
|
2017-04-18 18:30:08 +00:00
|
|
|
)`,
|
2017-04-18 22:05:42 +00:00
|
|
|
sqlUpdateDecision: `
|
|
|
|
UPDATE decisions
|
2017-04-20 18:58:22 +00:00
|
|
|
SET proponent=:proponent, title=:title, content=:content, votetype=:votetype, due=:due, modified=:modified
|
|
|
|
WHERE id=:id`,
|
2017-04-19 19:35:08 +00:00
|
|
|
sqlUpdateDecisionStatus: `
|
2017-04-20 18:58:22 +00:00
|
|
|
UPDATE decisions SET status=:status, modified=:modified WHERE id=:id`,
|
2017-04-19 21:32:12 +00:00
|
|
|
sqlSelectClosableDecisions: `
|
2017-04-20 18:58:22 +00:00
|
|
|
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
|
|
|
|
decisions.votetype, decisions.status, decisions.due, decisions.modified
|
2017-04-19 21:32:12 +00:00
|
|
|
FROM decisions
|
|
|
|
WHERE decisions.status=0 AND :now > due`,
|
2017-04-20 09:35:33 +00:00
|
|
|
sqlGetNextPendingDecisionDue: `
|
|
|
|
SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
|
2017-04-21 00:25:49 +00:00
|
|
|
sqlGetVotersForProxy: `
|
|
|
|
SELECT id, name, reminder
|
|
|
|
FROM voters WHERE enabled=1 AND id != $1 AND id NOT IN (SELECT voter FROM votes WHERE decision=$2)`,
|
2017-04-20 18:58:22 +00:00
|
|
|
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)
|
2017-04-21 00:25:49 +00:00
|
|
|
ORDER BY due ASC`,
|
|
|
|
sqlCreateVote: `
|
2017-04-21 11:19:05 +00:00
|
|
|
INSERT OR REPLACE INTO votes (decision, voter, vote, voted, notes)
|
2017-04-21 00:25:49 +00:00
|
|
|
VALUES (:decision, :voter, :vote, :voted, :notes)`,
|
|
|
|
sqlLoadVote: `
|
|
|
|
SELECT decision, voter, vote, voted, notes
|
|
|
|
FROM votes
|
|
|
|
WHERE decision=$1 AND voter=$2`,
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
2017-04-17 14:24:37 +00:00
|
|
|
|
|
|
|
var db *sqlx.DB
|
|
|
|
|
2017-04-18 18:30:08 +00:00
|
|
|
func init() {
|
2017-04-21 22:06:16 +00:00
|
|
|
failed_statements := make([]string, 0)
|
2017-04-18 18:30:08 +00:00
|
|
|
for _, sqlStatement := range sqlStatements {
|
|
|
|
var stmt *sqlx.Stmt
|
|
|
|
stmt, err := db.Preparex(sqlStatement)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Criticalf("ERROR parsing statement %s: %s", sqlStatement, err)
|
|
|
|
failed_statements = append(failed_statements, sqlStatement)
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
|
|
|
stmt.Close()
|
|
|
|
}
|
2017-04-21 22:06:16 +00:00
|
|
|
if len(failed_statements) > 0 {
|
|
|
|
panic(fmt.Sprintf("%d statements failed", len(failed_statements)))
|
|
|
|
}
|
2017-04-21 19:41:25 +00:00
|
|
|
|
|
|
|
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 {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Criticalf("getting the most recent database repository version failed: %s", err)
|
|
|
|
panic(err)
|
2017-04-21 19:41:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, db.DB)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Criticalf("running database migration failed: %s", err)
|
|
|
|
panic(err)
|
2017-04-21 19:41:25 +00:00
|
|
|
}
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
|
|
|
|
2017-04-18 00:34:21 +00:00
|
|
|
type VoteType uint8
|
|
|
|
type VoteStatus int8
|
2017-04-17 14:24:37 +00:00
|
|
|
|
|
|
|
type Decision struct {
|
2017-04-21 00:25:49 +00:00
|
|
|
Id int64
|
2017-04-17 14:24:37 +00:00
|
|
|
Proposed time.Time
|
2017-04-21 00:25:49 +00:00
|
|
|
ProponentId int64 `db:"proponent"`
|
2017-04-17 14:24:37 +00:00
|
|
|
Title string
|
|
|
|
Content string
|
|
|
|
Quorum int
|
|
|
|
Majority int
|
|
|
|
Status VoteStatus
|
|
|
|
Due time.Time
|
|
|
|
Modified time.Time
|
|
|
|
Tag string
|
|
|
|
VoteType VoteType
|
|
|
|
}
|
|
|
|
|
|
|
|
type Email struct {
|
2017-04-21 00:25:49 +00:00
|
|
|
VoterId int64 `db:"voter"`
|
2017-04-17 14:24:37 +00:00
|
|
|
Address string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Voter struct {
|
2017-04-21 00:25:49 +00:00
|
|
|
Id int64
|
2017-04-17 14:24:37 +00:00
|
|
|
Name string
|
|
|
|
Enabled bool
|
|
|
|
Reminder string // reminder email address
|
|
|
|
}
|
|
|
|
|
|
|
|
type VoteChoice int
|
|
|
|
|
|
|
|
const (
|
|
|
|
voteAye = 1
|
|
|
|
voteNaye = -1
|
|
|
|
voteAbstain = 0
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
voteTypeMotion = 0
|
|
|
|
voteTypeVeto = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
func (v VoteType) String() string {
|
|
|
|
switch v {
|
|
|
|
case voteTypeMotion:
|
|
|
|
return "motion"
|
|
|
|
case voteTypeVeto:
|
|
|
|
return "veto"
|
|
|
|
default:
|
|
|
|
return "unknown"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v VoteType) QuorumAndMajority() (int, int) {
|
|
|
|
switch v {
|
|
|
|
case voteTypeMotion:
|
|
|
|
return 3, 50
|
|
|
|
default:
|
|
|
|
return 1, 99
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v VoteChoice) String() string {
|
|
|
|
switch v {
|
|
|
|
case voteAye:
|
|
|
|
return "aye"
|
|
|
|
case voteNaye:
|
|
|
|
return "naye"
|
|
|
|
case voteAbstain:
|
|
|
|
return "abstain"
|
|
|
|
default:
|
|
|
|
return "unknown"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-21 00:25:49 +00:00
|
|
|
var VoteValues = map[string]VoteChoice{
|
|
|
|
"aye": voteAye,
|
|
|
|
"naye": voteNaye,
|
|
|
|
"abstain": voteAbstain,
|
|
|
|
}
|
|
|
|
|
|
|
|
var VoteChoices = map[int64]VoteChoice{
|
|
|
|
1: voteAye,
|
|
|
|
0: voteAbstain,
|
|
|
|
-1: voteNaye,
|
|
|
|
}
|
|
|
|
|
2017-04-17 20:56:20 +00:00
|
|
|
const (
|
|
|
|
voteStatusDeclined = -1
|
|
|
|
voteStatusPending = 0
|
|
|
|
voteStatusApproved = 1
|
|
|
|
voteStatusWithdrawn = -2
|
|
|
|
)
|
|
|
|
|
2017-04-17 14:24:37 +00:00
|
|
|
func (v VoteStatus) String() string {
|
|
|
|
switch v {
|
2017-04-17 20:56:20 +00:00
|
|
|
case voteStatusDeclined:
|
2017-04-17 14:24:37 +00:00
|
|
|
return "declined"
|
2017-04-17 20:56:20 +00:00
|
|
|
case voteStatusPending:
|
2017-04-17 14:24:37 +00:00
|
|
|
return "pending"
|
2017-04-17 20:56:20 +00:00
|
|
|
case voteStatusApproved:
|
2017-04-17 14:24:37 +00:00
|
|
|
return "approved"
|
2017-04-17 20:56:20 +00:00
|
|
|
case voteStatusWithdrawn:
|
2017-04-17 14:24:37 +00:00
|
|
|
return "withdrawn"
|
|
|
|
default:
|
|
|
|
return "unknown"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-21 00:25:49 +00:00
|
|
|
type Vote struct {
|
|
|
|
DecisionId int64 `db:"decision"`
|
|
|
|
VoterId int64 `db:"voter"`
|
|
|
|
Vote VoteChoice
|
|
|
|
Voted time.Time
|
|
|
|
Notes string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vote) Save() (err error) {
|
|
|
|
insertVoteStmt, err := db.PrepareNamed(sqlStatements[sqlCreateVote])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer insertVoteStmt.Close()
|
|
|
|
|
|
|
|
_, err = insertVoteStmt.Exec(v)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("saving vote failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
getVoteStmt, err := db.Preparex(sqlStatements[sqlLoadVote])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getVoteStmt.Close()
|
|
|
|
|
|
|
|
err = getVoteStmt.Get(v, v.DecisionId, v.VoterId)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting inserted vote failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-17 14:24:37 +00:00
|
|
|
type VoteSums struct {
|
|
|
|
Ayes int
|
|
|
|
Nayes int
|
|
|
|
Abstains int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteSums) VoteCount() int {
|
|
|
|
return v.Ayes + v.Nayes + v.Abstains
|
|
|
|
}
|
|
|
|
|
2017-04-20 09:35:33 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-04-17 14:24:37 +00:00
|
|
|
type VoteForDisplay struct {
|
|
|
|
Vote
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
|
|
|
type DecisionForDisplay struct {
|
|
|
|
Decision
|
|
|
|
Proposer string `db:"proposer"`
|
|
|
|
*VoteSums
|
|
|
|
Votes []VoteForDisplay
|
|
|
|
}
|
|
|
|
|
|
|
|
func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
|
2017-04-18 18:30:08 +00:00
|
|
|
decisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionByTag])
|
2017-04-17 14:24:37 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer decisionStmt.Close()
|
|
|
|
|
|
|
|
decision = &DecisionForDisplay{}
|
|
|
|
if err = decisionStmt.Get(decision, tag); err != nil {
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
decision = nil
|
|
|
|
err = nil
|
|
|
|
} else {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting motion %s failed: %v", tag, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
decision.VoteSums, err = decision.Decision.VoteSums()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindDecisionsForDisplayOnPage loads a set of decisions from the database.
|
|
|
|
//
|
|
|
|
// This function uses OFFSET for pagination which is not a good idea for larger data sets.
|
|
|
|
//
|
|
|
|
// TODO: migrate to timestamp base pagination
|
2017-04-18 18:30:08 +00:00
|
|
|
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])
|
|
|
|
} else {
|
|
|
|
decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadDecisions])
|
2017-04-17 14:24:37 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer decisionsStmt.Close()
|
|
|
|
|
2017-04-18 18:30:08 +00:00
|
|
|
var rows *sqlx.Rows
|
|
|
|
if unvoted && voter != nil {
|
|
|
|
rows, err = decisionsStmt.Queryx(voter.Id, page-1)
|
|
|
|
} else {
|
|
|
|
rows, err = decisionsStmt.Queryx(page - 1)
|
|
|
|
}
|
2017-04-17 14:24:37 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("loading motions for page %d failed: %v", page, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
var d DecisionForDisplay
|
|
|
|
if err = rows.StructScan(&d); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("loading motions for page %d failed: %v", page, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
d.VoteSums, err = d.Decision.VoteSums()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
decisions = append(decisions, &d)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Decision) VoteSums() (sums *VoteSums, err error) {
|
2017-04-18 18:30:08 +00:00
|
|
|
votesStmt, err := db.Preparex(sqlStatements[sqlLoadVoteCountsForDecision])
|
2017-04-17 14:24:37 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer votesStmt.Close()
|
|
|
|
|
|
|
|
voteRows, err := votesStmt.Queryx(d.Id)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer voteRows.Close()
|
|
|
|
|
|
|
|
sums = &VoteSums{}
|
|
|
|
for voteRows.Next() {
|
|
|
|
var vote VoteChoice
|
|
|
|
var count int
|
|
|
|
if err = voteRows.Scan(&vote, &count); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
switch vote {
|
|
|
|
case voteAye:
|
|
|
|
sums.Ayes = count
|
|
|
|
case voteNaye:
|
|
|
|
sums.Nayes = count
|
|
|
|
case voteAbstain:
|
|
|
|
sums.Abstains = count
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DecisionForDisplay) LoadVotes() (err error) {
|
2017-04-18 18:30:08 +00:00
|
|
|
votesStmt, err := db.Preparex(sqlStatements[sqlLoadVotesForDecision])
|
2017-04-17 14:24:37 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer votesStmt.Close()
|
|
|
|
err = votesStmt.Select(&d.Votes, d.Id)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-18 18:30:08 +00:00
|
|
|
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])
|
|
|
|
}
|
2017-04-17 14:24:37 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer olderStmt.Close()
|
|
|
|
|
2017-04-18 18:30:08 +00:00
|
|
|
if unvoted && voter != nil {
|
|
|
|
if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("finding older motions than %s failed: %v", d.Tag, err)
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err = olderStmt.Get(&result, d.Proposed); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("finding older motions than %s failed: %v", d.Tag, err)
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
2017-04-17 14:24:37 +00:00
|
|
|
}
|
2017-04-18 18:30:08 +00:00
|
|
|
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-18 22:05:42 +00:00
|
|
|
func (d *Decision) Create() (err error) {
|
2017-04-18 18:30:08 +00:00
|
|
|
insertDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlCreateDecision])
|
2017-04-18 00:34:21 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-18 00:34:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer insertDecisionStmt.Close()
|
|
|
|
|
|
|
|
result, err := insertDecisionStmt.Exec(d)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("creating motion failed: %s", err)
|
2017-04-18 00:34:21 +00:00
|
|
|
return
|
|
|
|
}
|
2017-04-18 18:30:08 +00:00
|
|
|
|
|
|
|
lastInsertId, err := result.LastInsertId()
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting id of inserted motion failed: %s", err)
|
2017-04-18 22:05:42 +00:00
|
|
|
return
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
2017-04-20 09:35:33 +00:00
|
|
|
rescheduleChannel <- JobIdCloseDecisions
|
2017-04-18 18:30:08 +00:00
|
|
|
|
|
|
|
getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-18 18:30:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getDecisionStmt.Close()
|
|
|
|
|
|
|
|
err = getDecisionStmt.Get(d, lastInsertId)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting inserted motion failed: %s", err)
|
2017-04-18 18:30:08 +00:00
|
|
|
}
|
|
|
|
|
2017-04-18 00:34:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-19 19:35:08 +00:00
|
|
|
func (d *Decision) LoadWithId() (err error) {
|
|
|
|
getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-19 19:35:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getDecisionStmt.Close()
|
|
|
|
|
|
|
|
err = getDecisionStmt.Get(d, d.Id)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("loading updated motion failed: %s", err)
|
2017-04-19 19:35:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-18 22:05:42 +00:00
|
|
|
func (d *Decision) Update() (err error) {
|
|
|
|
updateDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecision])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-18 22:05:42 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer updateDecisionStmt.Close()
|
|
|
|
|
|
|
|
result, err := updateDecisionStmt.Exec(d)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("updating motion failed: %s", err)
|
2017-04-18 22:05:42 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
affectedRows, err := result.RowsAffected()
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("Problem determining the affected rows")
|
2017-04-18 22:05:42 +00:00
|
|
|
return
|
|
|
|
} else if affectedRows != 1 {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
|
2017-04-18 22:05:42 +00:00
|
|
|
}
|
2017-04-20 09:35:33 +00:00
|
|
|
rescheduleChannel <- JobIdCloseDecisions
|
2017-04-18 22:05:42 +00:00
|
|
|
|
2017-04-19 19:35:08 +00:00
|
|
|
err = d.LoadWithId()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Decision) UpdateStatus() (err error) {
|
|
|
|
updateStatusStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus])
|
2017-04-18 22:05:42 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-18 22:05:42 +00:00
|
|
|
return
|
|
|
|
}
|
2017-04-19 19:35:08 +00:00
|
|
|
defer updateStatusStmt.Close()
|
2017-04-18 22:05:42 +00:00
|
|
|
|
2017-04-19 19:35:08 +00:00
|
|
|
result, err := updateStatusStmt.Exec(d)
|
2017-04-18 22:05:42 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("setting motion status failed: %s", err)
|
2017-04-19 19:35:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
affectedRows, err := result.RowsAffected()
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("determining the affected rows failed: %s", err)
|
2017-04-20 09:35:33 +00:00
|
|
|
return
|
2017-04-19 19:35:08 +00:00
|
|
|
} else if affectedRows != 1 {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
|
2017-04-18 22:05:42 +00:00
|
|
|
}
|
2017-04-20 09:35:33 +00:00
|
|
|
rescheduleChannel <- JobIdCloseDecisions
|
2017-04-18 22:05:42 +00:00
|
|
|
|
2017-04-19 19:35:08 +00:00
|
|
|
err = d.LoadWithId()
|
2017-04-18 22:05:42 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-20 09:35:33 +00:00
|
|
|
func (d *Decision) String() string {
|
|
|
|
return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id)
|
|
|
|
}
|
|
|
|
|
2017-04-17 14:24:37 +00:00
|
|
|
func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
|
2017-04-18 18:30:08 +00:00
|
|
|
findVoterStmt, err := db.Preparex(sqlStatements[sqlLoadEnabledVoterByEmail])
|
2017-04-17 14:24:37 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-17 14:24:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer findVoterStmt.Close()
|
|
|
|
|
|
|
|
voter = &Voter{}
|
|
|
|
if err = findVoterStmt.Get(voter, emailAddress); err != nil {
|
|
|
|
if err != sql.ErrNoRows {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting voter for address %s failed: %v", emailAddress, err)
|
2017-04-17 14:24:37 +00:00
|
|
|
} else {
|
|
|
|
err = nil
|
|
|
|
voter = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2017-04-19 21:32:12 +00:00
|
|
|
|
|
|
|
func (d *Decision) Close() (err error) {
|
|
|
|
quorum, majority := d.VoteType.QuorumAndMajority()
|
|
|
|
|
|
|
|
voteSums, err := d.VoteSums()
|
|
|
|
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting vote sums failed: %s", err)
|
2017-04-19 21:32:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
votes := voteSums.VoteCount()
|
|
|
|
|
|
|
|
if votes < quorum {
|
|
|
|
d.Status = voteStatusDeclined
|
|
|
|
} else {
|
2017-04-20 09:35:33 +00:00
|
|
|
votes = voteSums.TotalVotes()
|
2017-04-19 21:32:12 +00:00
|
|
|
if (voteSums.Ayes / votes) > (majority / 100) {
|
|
|
|
d.Status = voteStatusApproved
|
|
|
|
} else {
|
|
|
|
d.Status = voteStatusDeclined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-20 09:35:33 +00:00
|
|
|
closeDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-20 09:35:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer closeDecisionStmt.Close()
|
|
|
|
|
2017-04-19 21:32:12 +00:00
|
|
|
result, err := closeDecisionStmt.Exec(d)
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("closing vote failed: %s", err)
|
2017-04-19 21:32:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
affectedRows, err := result.RowsAffected()
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting affected rows failed: %s", err)
|
2017-04-19 21:32:12 +00:00
|
|
|
}
|
|
|
|
if affectedRows != 1 {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
|
2017-04-19 21:32:12 +00:00
|
|
|
}
|
|
|
|
|
2017-04-21 00:25:49 +00:00
|
|
|
NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums)
|
2017-04-19 21:32:12 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func CloseDecisions() (err error) {
|
2017-04-20 09:35:33 +00:00
|
|
|
getClosableDecisionsStmt, err := db.PrepareNamed(sqlStatements[sqlSelectClosableDecisions])
|
2017-04-19 21:32:12 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-19 21:32:12 +00:00
|
|
|
return
|
|
|
|
}
|
2017-04-20 09:35:33 +00:00
|
|
|
defer getClosableDecisionsStmt.Close()
|
2017-04-19 21:32:12 +00:00
|
|
|
|
2017-04-20 09:35:33 +00:00
|
|
|
decisions := make([]*Decision, 0)
|
|
|
|
rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
|
2017-04-19 21:32:12 +00:00
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("fetching closable decisions failed: %s", err)
|
2017-04-19 21:32:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
2017-04-20 09:35:33 +00:00
|
|
|
decision := &Decision{}
|
|
|
|
if err = rows.StructScan(decision); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("scanning row failed: %s", err)
|
2017-04-19 21:32:12 +00:00
|
|
|
return
|
|
|
|
}
|
2017-04-20 09:35:33 +00:00
|
|
|
decisions = append(decisions, decision)
|
|
|
|
}
|
|
|
|
rows.Close()
|
|
|
|
|
|
|
|
for _, decision := range decisions {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Debugf("found closable decision %s", decision.Tag)
|
2017-04-20 09:35:33 +00:00
|
|
|
if err = decision.Close(); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("closing decision %s failed: %s", decision.Tag, err)
|
2017-04-19 21:32:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2017-04-20 09:35:33 +00:00
|
|
|
|
|
|
|
func GetNextPendingDecisionDue() (due *time.Time, err error) {
|
|
|
|
getNextPendingDecisionDueStmt, err := db.Preparex(sqlStatements[sqlGetNextPendingDecisionDue])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-20 09:35:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getNextPendingDecisionDueStmt.Close()
|
|
|
|
|
|
|
|
row := getNextPendingDecisionDueStmt.QueryRow()
|
|
|
|
|
|
|
|
var dueTimestamp time.Time
|
|
|
|
if err = row.Scan(&dueTimestamp); err != nil {
|
|
|
|
if err == sql.ErrNoRows {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Debugf("No pending decisions")
|
2017-04-20 09:35:33 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("parsing result failed: %s", err)
|
2017-04-20 09:35:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
due = &dueTimestamp
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2017-04-20 18:58:22 +00:00
|
|
|
|
|
|
|
func GetReminderVoters() (voters *[]Voter, err error) {
|
|
|
|
getReminderVotersStmt, err := db.Preparex(sqlStatements[sqlGetReminderVoters])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-20 18:58:22 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getReminderVotersStmt.Close()
|
|
|
|
|
|
|
|
voterSlice := make([]Voter, 0)
|
|
|
|
|
|
|
|
if err = getReminderVotersStmt.Select(&voterSlice); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting voters failed: %s", err)
|
2017-04-20 18:58:22 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
voters = &voterSlice
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) {
|
|
|
|
findUnvotedDecisionsForVoterStmt, err := db.Preparex(sqlStatements[sqlFindUnvotedDecisionsForVoter])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-20 18:58:22 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer findUnvotedDecisionsForVoterStmt.Close()
|
|
|
|
|
|
|
|
decisionsSlice := make([]Decision, 0)
|
|
|
|
|
|
|
|
if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting unvoted decisions failed: %s", err)
|
2017-04-20 18:58:22 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
decisions = &decisionsSlice
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2017-04-21 00:25:49 +00:00
|
|
|
|
|
|
|
func GetVoterById(id int64) (voter *Voter, err error) {
|
|
|
|
getVoterByIdStmt, err := db.Preparex(sqlStatements[sqlGetEnabledVoterById])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getVoterByIdStmt.Close()
|
|
|
|
|
|
|
|
voter = &Voter{}
|
|
|
|
if err = getVoterByIdStmt.Get(voter, id); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("getting voter failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetVotersForProxy(proxy *Voter, decision *Decision) (voters *[]Voter, err error) {
|
|
|
|
getVotersForProxyStmt, err := db.Preparex(sqlStatements[sqlGetVotersForProxy])
|
|
|
|
if err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("preparing statement failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer getVotersForProxyStmt.Close()
|
|
|
|
|
|
|
|
votersSlice := make([]Voter, 0)
|
|
|
|
|
|
|
|
if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id, decision.Id); err != nil {
|
2017-04-21 22:06:16 +00:00
|
|
|
logger.Errorf("Error getting voters for proxy failed: %s", err)
|
2017-04-21 00:25:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
voters = &votersSlice
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|