|
|
@ -135,7 +135,7 @@ func (v *VoteSums) CalculateResult(quorum int, majority float32) (VoteStatus, st
|
|
|
|
return voteStatusApproved, "Quorum and majority have been reached"
|
|
|
|
return voteStatusApproved, "Quorum and majority have been reached"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Decision struct {
|
|
|
|
type Motion struct {
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
Proposed time.Time
|
|
|
|
Proposed time.Time
|
|
|
|
Proponent int64 `db:"proponent"`
|
|
|
|
Proponent int64 `db:"proponent"`
|
|
|
@ -150,26 +150,26 @@ type Decision struct {
|
|
|
|
VoteType VoteType
|
|
|
|
VoteType VoteType
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ClosedDecision struct {
|
|
|
|
type ClosedMotion struct {
|
|
|
|
Decision *Decision
|
|
|
|
Decision *Motion
|
|
|
|
VoteSums *VoteSums
|
|
|
|
VoteSums *VoteSums
|
|
|
|
Reasoning string
|
|
|
|
Reasoning string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type DecisionModel struct {
|
|
|
|
type MotionModel struct {
|
|
|
|
DB *sqlx.DB
|
|
|
|
DB *sqlx.DB
|
|
|
|
InfoLog *log.Logger
|
|
|
|
InfoLog *log.Logger
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create a new decision.
|
|
|
|
// Create a new decision.
|
|
|
|
func (m *DecisionModel) Create(
|
|
|
|
func (m *MotionModel) Create(
|
|
|
|
ctx context.Context,
|
|
|
|
ctx context.Context,
|
|
|
|
proponent *Voter,
|
|
|
|
proponent *Voter,
|
|
|
|
voteType VoteType,
|
|
|
|
voteType VoteType,
|
|
|
|
title, content string,
|
|
|
|
title, content string,
|
|
|
|
proposed, due time.Time,
|
|
|
|
proposed, due time.Time,
|
|
|
|
) (int64, error) {
|
|
|
|
) (int64, error) {
|
|
|
|
d := &Decision{
|
|
|
|
d := &Motion{
|
|
|
|
Proposed: proposed.UTC(),
|
|
|
|
Proposed: proposed.UTC(),
|
|
|
|
Proponent: proponent.ID,
|
|
|
|
Proponent: proponent.ID,
|
|
|
|
Title: title,
|
|
|
|
Title: title,
|
|
|
@ -183,10 +183,10 @@ func (m *DecisionModel) Create(
|
|
|
|
`INSERT INTO decisions
|
|
|
|
`INSERT INTO decisions
|
|
|
|
(proposed, proponent, title, content, votetype, status, due, modified, tag)
|
|
|
|
(proposed, proponent, title, content, votetype, status, due, modified, tag)
|
|
|
|
VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :proposed,
|
|
|
|
VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :proposed,
|
|
|
|
'm' || strftime('%Y%m%d', :proposed) || '.' || (
|
|
|
|
'm' || STRFTIME('%Y%m%d', :proposed) || '.' || (
|
|
|
|
SELECT COUNT(*)+1 AS num
|
|
|
|
SELECT COUNT(*)+1 AS num
|
|
|
|
FROM decisions
|
|
|
|
FROM decisions
|
|
|
|
WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day')
|
|
|
|
WHERE proposed BETWEEN DATE(:proposed) AND DATE(:proposed, '1 day')
|
|
|
|
))`,
|
|
|
|
))`,
|
|
|
|
d,
|
|
|
|
d,
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -202,7 +202,7 @@ VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :prop
|
|
|
|
return id, nil
|
|
|
|
return id, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) CloseDecisions(ctx context.Context) ([]*ClosedDecision, error) {
|
|
|
|
func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*ClosedMotion, error) {
|
|
|
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
|
|
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not start transaction: %w", err)
|
|
|
|
return nil, fmt.Errorf("could not start transaction: %w", err)
|
|
|
@ -225,10 +225,10 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
|
|
|
|
_ = rows.Close()
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
|
|
|
|
decisions := make([]*Decision, 0)
|
|
|
|
decisions := make([]*Motion, 0)
|
|
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
for rows.Next() {
|
|
|
|
decision := &Decision{}
|
|
|
|
decision := &Motion{}
|
|
|
|
if err = rows.StructScan(decision); err != nil {
|
|
|
|
if err = rows.StructScan(decision); err != nil {
|
|
|
|
return nil, fmt.Errorf("scanning row failed: %w", err)
|
|
|
|
return nil, fmt.Errorf("scanning row failed: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -240,9 +240,9 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
|
|
|
|
decisions = append(decisions, decision)
|
|
|
|
decisions = append(decisions, decision)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
results := make([]*ClosedDecision, 0, len(decisions))
|
|
|
|
results := make([]*ClosedMotion, 0, len(decisions))
|
|
|
|
|
|
|
|
|
|
|
|
var decisionResult *ClosedDecision
|
|
|
|
var decisionResult *ClosedMotion
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
@ -261,7 +261,7 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
|
|
|
|
return results, nil
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Decision) (*ClosedDecision, error) {
|
|
|
|
func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*ClosedMotion, error) {
|
|
|
|
quorum, majority := d.VoteType.QuorumAndMajority()
|
|
|
|
quorum, majority := d.VoteType.QuorumAndMajority()
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
var (
|
|
|
@ -297,14 +297,14 @@ func (m *DecisionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Decis
|
|
|
|
|
|
|
|
|
|
|
|
m.InfoLog.Printf("decision %s closed with result %s: reasoning '%s'", d.Tag, d.Status, reasoning)
|
|
|
|
m.InfoLog.Printf("decision %s closed with result %s: reasoning '%s'", d.Tag, d.Status, reasoning)
|
|
|
|
|
|
|
|
|
|
|
|
return &ClosedDecision{d, voteSums, reasoning}, nil
|
|
|
|
return &ClosedMotion{d, voteSums, reasoning}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) UnVotedDecisionsForVoter(_ context.Context, _ *Voter) ([]Decision, error) {
|
|
|
|
func (m *MotionModel) UnVotedDecisionsForVoter(_ context.Context, _ *Voter) ([]Motion, error) {
|
|
|
|
panic("not implemented")
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Decision) (*VoteSums, error) {
|
|
|
|
func (m *MotionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*VoteSums, error) {
|
|
|
|
voteRows, err := tx.QueryxContext(
|
|
|
|
voteRows, err := tx.QueryxContext(
|
|
|
|
ctx,
|
|
|
|
ctx,
|
|
|
|
`SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
|
|
|
|
`SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
|
|
|
@ -347,7 +347,7 @@ func (m *DecisionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Dec
|
|
|
|
return sums, nil
|
|
|
|
return sums, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *DecisionModel) NextPendingDecisionDue(ctx context.Context) (*time.Time, error) {
|
|
|
|
func (m *MotionModel) NextPendingDecisionDue(ctx context.Context) (*time.Time, error) {
|
|
|
|
row := m.DB.QueryRowContext(
|
|
|
|
row := m.DB.QueryRowContext(
|
|
|
|
ctx,
|
|
|
|
ctx,
|
|
|
|
`SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
|
|
|
|
`SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
|
|
|
@ -376,3 +376,205 @@ func (m *DecisionModel) NextPendingDecisionDue(ctx context.Context) (*time.Time,
|
|
|
|
|
|
|
|
|
|
|
|
return &due, nil
|
|
|
|
return &due, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type MotionForDisplay struct {
|
|
|
|
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
|
|
|
|
Tag string
|
|
|
|
|
|
|
|
Proponent int64
|
|
|
|
|
|
|
|
Proposer string
|
|
|
|
|
|
|
|
Proposed time.Time
|
|
|
|
|
|
|
|
Title string
|
|
|
|
|
|
|
|
Content string
|
|
|
|
|
|
|
|
Type VoteType `db:"votetype"`
|
|
|
|
|
|
|
|
Status VoteStatus
|
|
|
|
|
|
|
|
Due time.Time
|
|
|
|
|
|
|
|
Modified time.Time
|
|
|
|
|
|
|
|
Sums VoteSums
|
|
|
|
|
|
|
|
Votes []VoteForDisplay
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type VoteForDisplay struct {
|
|
|
|
|
|
|
|
Name string
|
|
|
|
|
|
|
|
Vote VoteChoice
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type MotionListOptions struct {
|
|
|
|
|
|
|
|
Limit int
|
|
|
|
|
|
|
|
Before, After *time.Time
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) TimestampRange(ctx context.Context) (*time.Time, *time.Time, error) {
|
|
|
|
|
|
|
|
row := m.DB.QueryRowxContext(ctx, `SELECT MIN(proposed), MAX(proposed) FROM decisions`)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err := row.Err(); err != nil {
|
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("could not query for motion timestamps: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
first, last sql.NullString
|
|
|
|
|
|
|
|
firstTs, lastTs *time.Time
|
|
|
|
|
|
|
|
err error
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err := row.Scan(&first, &last); err != nil {
|
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("could not scan timestamps: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !first.Valid || !last.Valid {
|
|
|
|
|
|
|
|
return nil, nil, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if firstTs, err = parseSqlite3TimeStamp(first.String); err != nil {
|
|
|
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if lastTs, err = parseSqlite3TimeStamp(last.String); err != nil {
|
|
|
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return firstTs, lastTs, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) GetMotions(ctx context.Context, options *MotionListOptions) ([]*MotionForDisplay, error) {
|
|
|
|
|
|
|
|
var rows *sqlx.Rows
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if options.Before != nil {
|
|
|
|
|
|
|
|
rows, err = m.DB.QueryxContext(
|
|
|
|
|
|
|
|
ctx,
|
|
|
|
|
|
|
|
`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.proposed < $1
|
|
|
|
|
|
|
|
ORDER BY proposed DESC
|
|
|
|
|
|
|
|
LIMIT $2`,
|
|
|
|
|
|
|
|
options.Before,
|
|
|
|
|
|
|
|
options.Limit,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
} else if options.After != nil {
|
|
|
|
|
|
|
|
rows, err = m.DB.QueryxContext(
|
|
|
|
|
|
|
|
ctx,
|
|
|
|
|
|
|
|
`WITH display_decision AS (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.proposed > $1
|
|
|
|
|
|
|
|
ORDER BY proposed
|
|
|
|
|
|
|
|
LIMIT $2)
|
|
|
|
|
|
|
|
SELECT *
|
|
|
|
|
|
|
|
FROM display_decision
|
|
|
|
|
|
|
|
ORDER BY proposed DESC`,
|
|
|
|
|
|
|
|
options.After,
|
|
|
|
|
|
|
|
options.Limit,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
rows, err = m.DB.QueryxContext(
|
|
|
|
|
|
|
|
ctx,
|
|
|
|
|
|
|
|
`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 $1`,
|
|
|
|
|
|
|
|
options.Limit,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not execute query: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
|
|
|
|
_ = rows.Close()
|
|
|
|
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
motions := make([]*MotionForDisplay, 0, options.Limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
|
|
|
var decision MotionForDisplay
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not fetch row: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err = rows.StructScan(&decision); err != nil {
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("could not scan result: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
motions = append(motions, &decision)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(motions) > 0 {
|
|
|
|
|
|
|
|
err = m.FillVoteSums(ctx, motions)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return motions, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*MotionForDisplay) error {
|
|
|
|
|
|
|
|
decisionIds := make([]int64, len(decisions))
|
|
|
|
|
|
|
|
decisionMap := make(map[int64]*MotionForDisplay, len(decisions))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for idx, decision := range decisions {
|
|
|
|
|
|
|
|
decisionIds[idx] = decision.ID
|
|
|
|
|
|
|
|
decisionMap[decision.ID] = decision
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
query, args, err := sqlx.In(
|
|
|
|
|
|
|
|
`SELECT v.decision, v.vote, COUNT(*) FROM votes v WHERE v.decision IN (?) GROUP BY v.decision, v.vote`,
|
|
|
|
|
|
|
|
decisionIds,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return fmt.Errorf("could not create IN query: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rows, err := m.DB.QueryContext(ctx, query, args...)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return fmt.Errorf("could not execute query: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defer func(rows *sql.Rows) {
|
|
|
|
|
|
|
|
_ = rows.Close()
|
|
|
|
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
|
|
|
|
return fmt.Errorf("could not fetch row: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
decisionID int64
|
|
|
|
|
|
|
|
vote VoteChoice
|
|
|
|
|
|
|
|
count int
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
err = rows.Scan(&decisionID, &vote, &count)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return fmt.Errorf("could not scan row: %w", err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch vote {
|
|
|
|
|
|
|
|
case voteAye:
|
|
|
|
|
|
|
|
decisionMap[decisionID].Sums.Ayes = count
|
|
|
|
|
|
|
|
case voteNaye:
|
|
|
|
|
|
|
|
decisionMap[decisionID].Sums.Nayes = count
|
|
|
|
|
|
|
|
case voteAbstain:
|
|
|
|
|
|
|
|
decisionMap[decisionID].Sums.Abstains = count
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|