|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|
|
|
package models
|
|
|
|
package models
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
import (
|
|
|
|
|
|
|
|
"database/sql"
|
|
|
|
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"log"
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
@ -108,6 +110,30 @@ func (v VoteChoice) String() string {
|
|
|
|
return unknownVariant
|
|
|
|
return unknownVariant
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type VoteSums struct {
|
|
|
|
|
|
|
|
Ayes, Nayes, Abstains int
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) CalculateResult(quorum int, majority float32) (VoteStatus, string) {
|
|
|
|
|
|
|
|
if v.VoteCount() < quorum {
|
|
|
|
|
|
|
|
return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (float32(v.Ayes) / float32(v.TotalVotes())) < majority {
|
|
|
|
|
|
|
|
return voteStatusDeclined, fmt.Sprintf("Needed majority of %0.2f%% has not been reached.", majority)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return voteStatusApproved, "Quorum and majority have been reached"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Decision struct {
|
|
|
|
type Decision struct {
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
Proposed time.Time
|
|
|
|
Proposed time.Time
|
|
|
@ -123,6 +149,12 @@ type Decision struct {
|
|
|
|
VoteType VoteType
|
|
|
|
VoteType VoteType
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type ClosedDecision struct {
|
|
|
|
|
|
|
|
Decision *Decision
|
|
|
|
|
|
|
|
VoteSums *VoteSums
|
|
|
|
|
|
|
|
Reasoning string
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type DecisionModel struct {
|
|
|
|
type DecisionModel struct {
|
|
|
|
DB *sqlx.DB
|
|
|
|
DB *sqlx.DB
|
|
|
|
InfoLog *log.Logger
|
|
|
|
InfoLog *log.Logger
|
|
|
@ -164,14 +196,23 @@ VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :prop
|
|
|
|
return id, nil
|
|
|
|
return id, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) CloseDecisions() error {
|
|
|
|
func (m *DecisionModel) CloseDecisions() ([]*ClosedDecision, error) {
|
|
|
|
rows, err := m.DB.NamedQuery(`
|
|
|
|
tx, err := m.DB.Beginx()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not start transaction: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defer func(tx *sqlx.Tx) {
|
|
|
|
|
|
|
|
_ = tx.Rollback()
|
|
|
|
|
|
|
|
}(tx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rows, err := tx.NamedQuery(`
|
|
|
|
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
|
|
|
|
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
|
|
|
|
decisions.votetype, decisions.status, decisions.due, decisions.modified
|
|
|
|
decisions.votetype, decisions.status, decisions.due, decisions.modified
|
|
|
|
FROM decisions
|
|
|
|
FROM decisions
|
|
|
|
WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now().UTC()})
|
|
|
|
WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now().UTC()})
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("fetching closable decisions failed: %w", err)
|
|
|
|
return nil, fmt.Errorf("fetching closable decisions failed: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
@ -183,31 +224,143 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
|
|
|
|
for rows.Next() {
|
|
|
|
for rows.Next() {
|
|
|
|
decision := &Decision{}
|
|
|
|
decision := &Decision{}
|
|
|
|
if err = rows.StructScan(decision); err != nil {
|
|
|
|
if err = rows.StructScan(decision); err != nil {
|
|
|
|
return fmt.Errorf("scanning row failed: %w", err)
|
|
|
|
return nil, fmt.Errorf("scanning row failed: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if rows.Err() != nil {
|
|
|
|
if rows.Err() != nil {
|
|
|
|
return fmt.Errorf("row error: %w", err)
|
|
|
|
return nil, fmt.Errorf("row error: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
decisions = append(decisions, decision)
|
|
|
|
decisions = append(decisions, decision)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
results := make([]*ClosedDecision, 0, len(decisions))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var decisionResult *ClosedDecision
|
|
|
|
|
|
|
|
|
|
|
|
for _, decision := range decisions {
|
|
|
|
for _, decision := range decisions {
|
|
|
|
m.InfoLog.Printf("found closable decision %s", decision.Tag)
|
|
|
|
m.InfoLog.Printf("found closable decision %s", decision.Tag)
|
|
|
|
|
|
|
|
|
|
|
|
if err = m.Close(decision.Tag); err != nil {
|
|
|
|
if decisionResult, err = m.CloseDecision(tx, decision); err != nil {
|
|
|
|
return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err)
|
|
|
|
return nil, fmt.Errorf("closing decision %s failed: %w", decision.Tag, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
results = append(results, decisionResult)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not commit transaction: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) Close(tag string) error {
|
|
|
|
func (m *DecisionModel) CloseDecision(tx *sqlx.Tx, d *Decision) (*ClosedDecision, error) {
|
|
|
|
panic("not implemented")
|
|
|
|
quorum, majority := d.VoteType.QuorumAndMajority()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
voteSums *VoteSums
|
|
|
|
|
|
|
|
err error
|
|
|
|
|
|
|
|
reasoning string
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if voteSums, err = m.GetVoteSums(tx, d); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("getting vote sums failed: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
d.Status, reasoning = voteSums.CalculateResult(quorum, majority)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result, err := m.DB.NamedExec(
|
|
|
|
|
|
|
|
`UPDATE decisions SET status=:status, modified=CURRENT_TIMESTAMP WHERE id=:id`,
|
|
|
|
|
|
|
|
d,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not execute update query: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
affectedRows, err := result.RowsAffected()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not get affected rows count: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if affectedRows != 1 {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unexpected number of rows %d instead of 1", affectedRows)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
m.InfoLog.Printf("decision %s closed with result %s: reasoning '%s'", d.Tag, d.Status, reasoning)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return &ClosedDecision{d, voteSums, reasoning}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) FindUnVotedDecisionsForVoter(v *Voter) ([]Decision, error) {
|
|
|
|
func (m *DecisionModel) FindUnVotedDecisionsForVoter(_ *Voter) ([]Decision, error) {
|
|
|
|
panic("not implemented")
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) GetVoteSums(tx *sqlx.Tx, d *Decision) (*VoteSums, error) {
|
|
|
|
|
|
|
|
voteRows, err := tx.NamedQuery(
|
|
|
|
|
|
|
|
`SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
|
|
|
|
|
|
|
|
d.ID,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defer func(voteRows *sqlx.Rows) {
|
|
|
|
|
|
|
|
_ = voteRows.Close()
|
|
|
|
|
|
|
|
}(voteRows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sums := &VoteSums{}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for voteRows.Next() {
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
vote VoteChoice
|
|
|
|
|
|
|
|
count int
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err = voteRows.Err(); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not fetch vote sums for motion %s: %w", d.Tag, err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err = voteRows.Scan(&vote, &count); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not parse row for vote sums of motion %s: %w", d.Tag, err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch vote {
|
|
|
|
|
|
|
|
case voteAye:
|
|
|
|
|
|
|
|
sums.Ayes = count
|
|
|
|
|
|
|
|
case voteNaye:
|
|
|
|
|
|
|
|
sums.Nayes = count
|
|
|
|
|
|
|
|
case voteAbstain:
|
|
|
|
|
|
|
|
sums.Abstains = count
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return sums, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) GetNextPendingDecisionDue() (*time.Time, error) {
|
|
|
|
|
|
|
|
row := m.DB.QueryRow(`SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`, nil)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if row == nil {
|
|
|
|
|
|
|
|
return nil, errors.New("no row returned")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err := row.Err(); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not retrieve row for next pending decision: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var due time.Time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err := row.Scan(&due); err != nil {
|
|
|
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
|
|
|
m.InfoLog.Print("no pending decisions")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("parsing result failed: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return &due, nil
|
|
|
|
|
|
|
|
}
|
|
|
|