2017-04-17 14:24:37 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
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 = `
|
2017-04-17 20:56:20 +00:00
|
|
|
SELECT voters.id, voters.name, voters.enabled, voters.reminder
|
2017-04-17 14:24:37 +00:00
|
|
|
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`
|
|
|
|
)
|
|
|
|
|
|
|
|
var db *sqlx.DB
|
|
|
|
|
|
|
|
type VoteType int
|
|
|
|
type VoteStatus int
|
|
|
|
|
|
|
|
type Decision struct {
|
|
|
|
Id int
|
|
|
|
Proposed time.Time
|
|
|
|
ProponentId int `db:"proponent"`
|
|
|
|
Title string
|
|
|
|
Content string
|
|
|
|
Quorum int
|
|
|
|
Majority int
|
|
|
|
Status VoteStatus
|
|
|
|
Due time.Time
|
|
|
|
Modified time.Time
|
|
|
|
Tag string
|
|
|
|
VoteType VoteType
|
|
|
|
}
|
|
|
|
|
|
|
|
type Email struct {
|
|
|
|
VoterId int `db:"voter"`
|
|
|
|
Address string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Voter struct {
|
|
|
|
Id int
|
|
|
|
Name string
|
|
|
|
Enabled bool
|
|
|
|
Reminder string // reminder email address
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
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-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"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type VoteSums struct {
|
|
|
|
Ayes int
|
|
|
|
Nayes int
|
|
|
|
Abstains int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteSums) VoteCount() int {
|
|
|
|
return v.Ayes + v.Nayes + v.Abstains
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
decisionStmt, err := db.Preparex(sqlGetDecision)
|
|
|
|
if err != nil {
|
|
|
|
logger.Println("Error preparing statement:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer decisionStmt.Close()
|
|
|
|
|
|
|
|
decision = &DecisionForDisplay{}
|
|
|
|
if err = decisionStmt.Get(decision, tag); err != nil {
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
decision = nil
|
|
|
|
err = nil
|
|
|
|
} else {
|
|
|
|
logger.Printf("Error getting motion %s: %v\n", tag, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
|
|
func FindDecisionsForDisplayOnPage(page int64) (decisions []*DecisionForDisplay, err error) {
|
|
|
|
decisionsStmt, err := db.Preparex(sqlGetDecisions)
|
|
|
|
if err != nil {
|
|
|
|
logger.Println("Error preparing statement:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
defer decisionsStmt.Close()
|
|
|
|
|
2017-04-17 20:56:20 +00:00
|
|
|
rows, err := decisionsStmt.Queryx(page-1, voter.Id)
|
2017-04-17 14:24:37 +00:00
|
|
|
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 (d *Decision) VoteSums() (sums *VoteSums, err error) {
|
|
|
|
votesStmt, err := db.Preparex(sqlVoteCount)
|
|
|
|
if err != nil {
|
|
|
|
logger.Println("Error preparing statement:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer voteRows.Close()
|
|
|
|
|
|
|
|
sums = &VoteSums{}
|
|
|
|
for voteRows.Next() {
|
|
|
|
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)
|
|
|
|
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) {
|
|
|
|
votesStmt, err := db.Preparex(sqlGetVotesForDecision)
|
|
|
|
if err != nil {
|
|
|
|
logger.Println("Error preparing statement:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Decision) OlderExists() (result bool, err error) {
|
|
|
|
olderStmt, err := db.Preparex(sqlCountOlderThanDecision)
|
|
|
|
if err != nil {
|
|
|
|
logger.Println("Error preparing statement:", 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 FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
|
|
|
|
findVoterStmt, err := db.Preparex(sqlGetVoter)
|
|
|
|
if err != nil {
|
|
|
|
logger.Println("Error preparing statement:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
} else {
|
|
|
|
err = nil
|
|
|
|
voter = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|