diff options
Diffstat (limited to 'models.go')
-rw-r--r-- | models.go | 378 |
1 files changed, 164 insertions, 214 deletions
@@ -106,7 +106,7 @@ WHERE decisions.status=0 AND :now > due`, 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 AND id NOT IN (SELECT voter FROM votes WHERE decision=$2)`, +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: ` @@ -123,47 +123,6 @@ FROM votes WHERE decision=$1 AND voter=$2`, } -var db *sqlx.DB - -func init() { - failed_statements := make([]string, 0) - for _, sqlStatement := range sqlStatements { - var stmt *sqlx.Stmt - stmt, err := db.Preparex(sqlStatement) - if err != nil { - logger.Criticalf("ERROR parsing statement %s: %s", sqlStatement, err) - failed_statements = append(failed_statements, sqlStatement) - } - stmt.Close() - } - if len(failed_statements) > 0 { - panic(fmt.Sprintf("%d statements failed", 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 { - logger.Criticalf("getting the most recent database repository version failed: %s", err) - panic(err) - } - - err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, db.DB) - if err != nil { - logger.Criticalf("running database migration failed: %s", err) - panic(err) - } -} - type VoteType uint8 type VoteStatus int8 @@ -282,30 +241,86 @@ type Vote struct { Notes string } -func (v *Vote) Save() (err error) { - insertVoteStmt, err := db.PrepareNamed(sqlStatements[sqlCreateVote]) +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.Criticalf("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 { - logger.Errorf("preparing statement failed: %s", err) - return + log.Panicf("getting the most recent database repository version failed: %v", err) } - defer insertVoteStmt.Close() - _, err = insertVoteStmt.Exec(v) + err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, database.DB) if err != nil { - logger.Errorf("saving vote failed: %s", err) - return + 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 +} - getVoteStmt, err := db.Preparex(sqlStatements[sqlLoadVote]) +func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt { + statement, err := d.db.Preparex(sqlStatements[statementKey]) if err != nil { - logger.Errorf("preparing statement failed: %s", err) + 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() - err = getVoteStmt.Get(v, v.DecisionId, v.VoterId) - if err != nil { - logger.Errorf("getting inserted vote failed: %s", err) + if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil { + log.Errorf("getting inserted vote failed: %v", err) + return } return @@ -333,6 +348,17 @@ func (v *VoteSums) Percent() int { 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 @@ -346,11 +372,7 @@ type DecisionForDisplay struct { } func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) { - decisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionByTag]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag) defer decisionStmt.Close() decision = &DecisionForDisplay{} @@ -359,7 +381,8 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err decision = nil err = nil } else { - logger.Errorf("getting motion %s failed: %v", tag, err) + log.Errorf("getting motion %s failed: %v", tag, err) + return } } decision.VoteSums, err = decision.Decision.VoteSums() @@ -374,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.Errorf("preparing statement failed: %s", err) - return + decisionsStmt = db.getPreparedStatement(sqlLoadDecisions) } defer decisionsStmt.Close() @@ -391,7 +410,7 @@ func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (deci rows, err = decisionsStmt.Queryx(page - 1) } if err != nil { - logger.Errorf("loading motions for page %d failed: %v", page, err) + log.Errorf("loading motions for page %d failed: %v", page, err) return } defer rows.Close() @@ -399,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.Errorf("loading motions for page %d failed: %v", page, err) + log.Errorf("loading motions for page %d failed: %v", page, err) return } d.VoteSums, err = d.Decision.VoteSums() @@ -412,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.Errorf("preparing statement failed: %s", err) - return - } + votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision) defer votesStmt.Close() voteRows, err := votesStmt.Queryx(d.Id) if err != nil { - logger.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) + log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) return } defer voteRows.Close() @@ -431,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.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) + log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) return } switch vote { @@ -447,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.Errorf("preparing statement failed: %s", err) - return - } + votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision) defer votesStmt.Close() + err = votesStmt.Select(&d.Votes, d.Id) if err != nil { - logger.Errorf("selecting votes for motion %s failed: %v", 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.Errorf("preparing statement failed: %s", 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.Errorf("finding older motions than %s failed: %v", 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.Errorf("finding older motions than %s failed: %v", d.Tag, err) + log.Errorf("finding older motions than %s failed: %v", d.Tag, err) + return } } @@ -487,76 +496,62 @@ 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.Errorf("preparing statement failed: %s", err) - return - } + insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision) defer insertDecisionStmt.Close() result, err := insertDecisionStmt.Exec(d) if err != nil { - logger.Errorf("creating motion failed: %s", err) + log.Errorf("creating motion failed: %v", err) return } lastInsertId, err := result.LastInsertId() if err != nil { - logger.Errorf("getting id of inserted motion failed: %s", err) + log.Errorf("getting id of inserted motion failed: %v", err) return } rescheduleChannel <- JobIdCloseDecisions - getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById) defer getDecisionStmt.Close() err = getDecisionStmt.Get(d, lastInsertId) if err != nil { - logger.Errorf("getting inserted motion failed: %s", 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.Errorf("preparing statement failed: %s", err) - return - } + getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById) defer getDecisionStmt.Close() err = getDecisionStmt.Get(d, d.Id) if err != nil { - logger.Errorf("loading updated motion failed: %s", 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.Errorf("preparing statement failed: %s", err) - return - } + updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision) defer updateDecisionStmt.Close() result, err := updateDecisionStmt.Exec(d) if err != nil { - logger.Errorf("updating motion failed: %s", err) + log.Errorf("updating motion failed: %v", err) return } affectedRows, err := result.RowsAffected() if err != nil { - logger.Errorf("Problem determining the affected rows") + log.Error("Problem determining the affected rows") return } else if affectedRows != 1 { - logger.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) + log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) } rescheduleChannel <- JobIdCloseDecisions @@ -565,24 +560,20 @@ func (d *Decision) Update() (err error) { } func (d *Decision) UpdateStatus() (err error) { - updateStatusStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus) defer updateStatusStmt.Close() result, err := updateStatusStmt.Exec(d) if err != nil { - logger.Errorf("setting motion status failed: %s", err) + log.Errorf("setting motion status failed: %v", err) return } affectedRows, err := result.RowsAffected() if err != nil { - logger.Errorf("determining the affected rows failed: %s", err) + log.Errorf("determining the affected rows failed: %v", err) return } else if affectedRows != 1 { - logger.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) + log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) } rescheduleChannel <- JobIdCloseDecisions @@ -595,17 +586,13 @@ func (d *Decision) String() string { } func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { - findVoterStmt, err := db.Preparex(sqlStatements[sqlLoadEnabledVoterByEmail]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail) defer findVoterStmt.Close() voter = &Voter{} if err = findVoterStmt.Get(voter, emailAddress); err != nil { if err != sql.ErrNoRows { - logger.Errorf("getting voter for address %s failed: %v", emailAddress, err) + log.Errorf("getting voter for address %s failed: %v", emailAddress, err) } else { err = nil voter = nil @@ -614,72 +601,56 @@ func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { return } -func (d *Decision) Close() (err error) { +func (d *Decision) Close() error { quorum, majority := d.VoteType.QuorumAndMajority() - voteSums, err := d.VoteSums() - - if err != nil { - logger.Errorf("getting vote sums failed: %s", err) - return - } - votes := voteSums.VoteCount() + var voteSums *VoteSums + var err error - if votes < quorum { - d.Status = voteStatusDeclined - } else { - votes = voteSums.TotalVotes() - if (voteSums.Ayes / votes) > (majority / 100) { - d.Status = voteStatusApproved - } else { - d.Status = voteStatusDeclined - } + 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, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus) defer closeDecisionStmt.Close() result, err := closeDecisionStmt.Exec(d) if err != nil { - logger.Errorf("closing vote failed: %s", err) - return - } - affectedRows, err := result.RowsAffected() - if err != nil { - logger.Errorf("getting affected rows failed: %s", err) + log.Errorf("closing vote failed: %v", err) + return err } - if affectedRows != 1 { - logger.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) + 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) + NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums, reasoning) - return + log.Infof("decision %s closed with result %s: reasoning %s", d.Tag, d.Status, reasoning) + + return nil } func CloseDecisions() (err error) { - getClosableDecisionsStmt, err := db.PrepareNamed(sqlStatements[sqlSelectClosableDecisions]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + 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 { - logger.Errorf("fetching closable decisions failed: %s", err) + log.Errorf("fetching closable decisions failed: %v", err) return } defer rows.Close() for rows.Next() { decision := &Decision{} if err = rows.StructScan(decision); err != nil { - logger.Errorf("scanning row failed: %s", err) + log.Errorf("scanning row failed: %v", err) return } decisions = append(decisions, decision) @@ -687,9 +658,9 @@ func CloseDecisions() (err error) { rows.Close() for _, decision := range decisions { - logger.Debugf("found closable decision %s", decision.Tag) + log.Debugf("found closable decision %s", decision.Tag) if err = decision.Close(); err != nil { - logger.Errorf("closing decision %s failed: %s", decision.Tag, err) + log.Errorf("closing decision %s failed: %s", decision.Tag, err) return } } @@ -698,41 +669,32 @@ func CloseDecisions() (err error) { } func GetNextPendingDecisionDue() (due *time.Time, err error) { - getNextPendingDecisionDueStmt, err := db.Preparex(sqlStatements[sqlGetNextPendingDecisionDue]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue) defer getNextPendingDecisionDueStmt.Close() row := getNextPendingDecisionDueStmt.QueryRow() - var dueTimestamp time.Time - if err = row.Scan(&dueTimestamp); err != nil { + due = &time.Time{} + if err = row.Scan(due); err != nil { if err == sql.ErrNoRows { - logger.Debugf("No pending decisions") + log.Debug("No pending decisions") return nil, nil } - logger.Errorf("parsing result failed: %s", err) - return + log.Errorf("parsing result failed: %v", err) + return nil, err } - due = &dueTimestamp return } func GetReminderVoters() (voters *[]Voter, err error) { - getReminderVotersStmt, err := db.Preparex(sqlStatements[sqlGetReminderVoters]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters) defer getReminderVotersStmt.Close() voterSlice := make([]Voter, 0) if err = getReminderVotersStmt.Select(&voterSlice); err != nil { - logger.Errorf("getting voters failed: %s", err) + log.Errorf("getting voters failed: %v", err) return } voters = &voterSlice @@ -741,17 +703,13 @@ func GetReminderVoters() (voters *[]Voter, err error) { } func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) { - findUnvotedDecisionsForVoterStmt, err := db.Preparex(sqlStatements[sqlFindUnvotedDecisionsForVoter]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter) defer findUnvotedDecisionsForVoterStmt.Close() decisionsSlice := make([]Decision, 0) if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil { - logger.Errorf("getting unvoted decisions failed: %s", err) + log.Errorf("getting unvoted decisions failed: %v", err) return } decisions = &decisionsSlice @@ -760,34 +718,26 @@ func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err erro } func GetVoterById(id int64) (voter *Voter, err error) { - getVoterByIdStmt, err := db.Preparex(sqlStatements[sqlGetEnabledVoterById]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - return - } + getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById) defer getVoterByIdStmt.Close() voter = &Voter{} if err = getVoterByIdStmt.Get(voter, id); err != nil { - logger.Errorf("getting voter failed: %s", err) + log.Errorf("getting voter failed: %v", err) return } return } -func GetVotersForProxy(proxy *Voter, decision *Decision) (voters *[]Voter, err error) { - getVotersForProxyStmt, err := db.Preparex(sqlStatements[sqlGetVotersForProxy]) - if err != nil { - logger.Errorf("preparing statement failed: %s", err) - 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, decision.Id); err != nil { - logger.Errorf("Error getting voters for proxy failed: %s", err) + if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil { + log.Errorf("Error getting voters for proxy failed: %v", err) return } voters = &votersSlice |