|
|
|
@ -35,8 +35,6 @@ type VoteType struct {
|
|
|
|
|
id uint8
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const unknownVariant = "unknown"
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
VoteTypeMotion = &VoteType{label: "motion", id: 0}
|
|
|
|
|
VoteTypeVeto = &VoteType{label: "veto", id: 1}
|
|
|
|
@ -103,19 +101,19 @@ func (v *VoteType) QuorumAndMajority() (int, float32) {
|
|
|
|
|
|
|
|
|
|
type VoteStatus struct {
|
|
|
|
|
Label string
|
|
|
|
|
Id int8
|
|
|
|
|
ID int8
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
voteStatusDeclined = &VoteStatus{Label: "declined", Id: -1}
|
|
|
|
|
voteStatusPending = &VoteStatus{Label: "pending", Id: 0}
|
|
|
|
|
voteStatusApproved = &VoteStatus{Label: "approved", Id: 1}
|
|
|
|
|
voteStatusWithdrawn = &VoteStatus{Label: "withdrawn", Id: -2}
|
|
|
|
|
voteStatusDeclined = &VoteStatus{Label: "declined", ID: -1}
|
|
|
|
|
voteStatusPending = &VoteStatus{Label: "pending", ID: 0}
|
|
|
|
|
voteStatusApproved = &VoteStatus{Label: "approved", ID: 1}
|
|
|
|
|
voteStatusWithdrawn = &VoteStatus{Label: "withdrawn", ID: -2}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func VoteStatusFromInt(id int64) (*VoteStatus, error) {
|
|
|
|
|
for _, vs := range []*VoteStatus{voteStatusPending, voteStatusApproved, voteStatusWithdrawn, voteStatusDeclined} {
|
|
|
|
|
if int64(vs.Id) == id {
|
|
|
|
|
if int64(vs.ID) == id {
|
|
|
|
|
return vs, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -144,23 +142,23 @@ func (v *VoteStatus) Scan(src any) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *VoteStatus) Value() (driver.Value, error) {
|
|
|
|
|
return int64(v.Id), nil
|
|
|
|
|
return int64(v.ID), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type VoteChoice struct {
|
|
|
|
|
label string
|
|
|
|
|
id int8
|
|
|
|
|
Label string
|
|
|
|
|
ID int8
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
VoteAye = &VoteChoice{label: "aye", id: 1}
|
|
|
|
|
VoteNaye = &VoteChoice{label: "naye", id: -1}
|
|
|
|
|
VoteAbstain = &VoteChoice{label: "abstain", id: 0}
|
|
|
|
|
VoteAye = &VoteChoice{Label: "aye", ID: 1}
|
|
|
|
|
VoteNaye = &VoteChoice{Label: "naye", ID: -1}
|
|
|
|
|
VoteAbstain = &VoteChoice{Label: "abstain", ID: 0}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func VoteChoiceFromString(label string) (*VoteChoice, error) {
|
|
|
|
|
for _, vc := range []*VoteChoice{VoteAye, VoteNaye, VoteAbstain} {
|
|
|
|
|
if strings.EqualFold(vc.label, label) {
|
|
|
|
|
if strings.EqualFold(vc.Label, label) {
|
|
|
|
|
return vc, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -170,7 +168,7 @@ func VoteChoiceFromString(label string) (*VoteChoice, error) {
|
|
|
|
|
|
|
|
|
|
func VoteChoiceFromInt(id int64) (*VoteChoice, error) {
|
|
|
|
|
for _, vc := range []*VoteChoice{VoteAye, VoteNaye, VoteAbstain} {
|
|
|
|
|
if int64(vc.id) == id {
|
|
|
|
|
if int64(vc.ID) == id {
|
|
|
|
|
return vc, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -179,7 +177,7 @@ func VoteChoiceFromInt(id int64) (*VoteChoice, error) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *VoteChoice) String() string {
|
|
|
|
|
return v.label
|
|
|
|
|
return v.Label
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *VoteChoice) Scan(src any) error {
|
|
|
|
@ -199,11 +197,11 @@ func (v *VoteChoice) Scan(src any) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *VoteChoice) Value() (driver.Value, error) {
|
|
|
|
|
return int64(v.id), nil
|
|
|
|
|
return int64(v.ID), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *VoteChoice) Equal(other *VoteChoice) bool {
|
|
|
|
|
return v.id == other.id
|
|
|
|
|
return v.ID == other.ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type VoteSums struct {
|
|
|
|
@ -233,7 +231,8 @@ func (v *VoteSums) CalculateResult(quorum int, majority float32) (*VoteStatus, s
|
|
|
|
|
type Motion struct {
|
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
|
Proposed time.Time
|
|
|
|
|
Proponent int64 `db:"proponent"`
|
|
|
|
|
Proponent int64 `db:"proponent"`
|
|
|
|
|
Proposer string `db:"proposer"`
|
|
|
|
|
Title string
|
|
|
|
|
Content string
|
|
|
|
|
Status *VoteStatus
|
|
|
|
@ -241,22 +240,9 @@ type Motion struct {
|
|
|
|
|
Modified time.Time
|
|
|
|
|
Tag string
|
|
|
|
|
Type *VoteType `db:"votetype"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ClosedMotion struct {
|
|
|
|
|
Decision *Motion
|
|
|
|
|
VoteSums *VoteSums
|
|
|
|
|
Reasoning string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type NewMotion struct {
|
|
|
|
|
Decision *Motion
|
|
|
|
|
Proposer *User
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UpdatedMotion struct {
|
|
|
|
|
Decision *Motion
|
|
|
|
|
User *User
|
|
|
|
|
Sums *VoteSums `db:"-"`
|
|
|
|
|
Votes []*Vote `db:"-"`
|
|
|
|
|
Reasoning string `db:"-"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MotionModel struct {
|
|
|
|
@ -279,6 +265,7 @@ func (m *MotionModel) Create(
|
|
|
|
|
Content: content,
|
|
|
|
|
Due: due.UTC(),
|
|
|
|
|
Type: voteType,
|
|
|
|
|
Status: voteStatusPending,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err := m.DB.NamedExecContext(
|
|
|
|
@ -305,7 +292,7 @@ VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :prop
|
|
|
|
|
return id, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*ClosedMotion, error) {
|
|
|
|
|
func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*Motion, error) {
|
|
|
|
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("could not start transaction: %w", err)
|
|
|
|
@ -343,9 +330,9 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
|
|
|
|
|
decisions = append(decisions, decision)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results := make([]*ClosedMotion, 0, len(decisions))
|
|
|
|
|
results := make([]*Motion, 0, len(decisions))
|
|
|
|
|
|
|
|
|
|
var decisionResult *ClosedMotion
|
|
|
|
|
var decisionResult *Motion
|
|
|
|
|
|
|
|
|
|
for _, decision := range decisions {
|
|
|
|
|
m.InfoLog.Printf("found closable decision %s", decision.Tag)
|
|
|
|
@ -364,7 +351,7 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
|
|
|
|
|
return results, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*ClosedMotion, error) {
|
|
|
|
|
func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*Motion, error) {
|
|
|
|
|
quorum, majority := d.Type.QuorumAndMajority()
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
@ -400,7 +387,10 @@ func (m *MotionModel) CloseDecision(ctx context.Context, tx *sqlx.Tx, d *Motion)
|
|
|
|
|
|
|
|
|
|
m.InfoLog.Printf("decision %s closed with result %s: reasoning '%s'", d.Tag, d.Status, reasoning)
|
|
|
|
|
|
|
|
|
|
return &ClosedMotion{d, voteSums, reasoning}, nil
|
|
|
|
|
d.Sums = voteSums
|
|
|
|
|
d.Reasoning = reasoning
|
|
|
|
|
|
|
|
|
|
return d, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) UnVotedDecisionsForVoter(_ context.Context, _ *User) ([]*Motion, error) {
|
|
|
|
@ -480,22 +470,6 @@ func (m *MotionModel) NextPendingDecisionDue(ctx context.Context) (*time.Time, e
|
|
|
|
|
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
|
|
|
|
@ -557,7 +531,7 @@ WHERE due >= ?
|
|
|
|
|
return firstTs, lastTs, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) GetMotions(ctx context.Context, options *MotionListOptions) ([]*MotionForDisplay, error) {
|
|
|
|
|
func (m *MotionModel) GetMotions(ctx context.Context, options *MotionListOptions) ([]*Motion, error) {
|
|
|
|
|
var (
|
|
|
|
|
rows *sqlx.Rows
|
|
|
|
|
err error
|
|
|
|
@ -580,10 +554,10 @@ func (m *MotionModel) GetMotions(ctx context.Context, options *MotionListOptions
|
|
|
|
|
_ = rows.Close()
|
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
|
|
motions := make([]*MotionForDisplay, 0, options.Limit)
|
|
|
|
|
motions := make([]*Motion, 0, options.Limit)
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var decision MotionForDisplay
|
|
|
|
|
var decision Motion
|
|
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("could not fetch row: %w", err)
|
|
|
|
@ -606,11 +580,12 @@ func (m *MotionModel) GetMotions(ctx context.Context, options *MotionListOptions
|
|
|
|
|
return motions, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*MotionForDisplay) error {
|
|
|
|
|
func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*Motion) error {
|
|
|
|
|
decisionIds := make([]int64, len(decisions))
|
|
|
|
|
decisionMap := make(map[int64]*MotionForDisplay, len(decisions))
|
|
|
|
|
decisionMap := make(map[int64]*Motion, len(decisions))
|
|
|
|
|
|
|
|
|
|
for idx, decision := range decisions {
|
|
|
|
|
decision.Sums = &VoteSums{}
|
|
|
|
|
decisionIds[idx] = decision.ID
|
|
|
|
|
decisionMap[decision.ID] = decision
|
|
|
|
|
}
|
|
|
|
@ -781,7 +756,7 @@ LIMIT ?`,
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) GetMotionByTag(ctx context.Context, tag string, withVotes bool) (*MotionForDisplay, error) {
|
|
|
|
|
func (m *MotionModel) GetMotionByTag(ctx context.Context, tag string, withVotes bool) (*Motion, error) {
|
|
|
|
|
row := m.DB.QueryRowxContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`SELECT decisions.id,
|
|
|
|
@ -804,13 +779,13 @@ WHERE decisions.tag = ?`,
|
|
|
|
|
return nil, fmt.Errorf("could not query motion: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result MotionForDisplay
|
|
|
|
|
var result Motion
|
|
|
|
|
|
|
|
|
|
if err := row.StructScan(&result); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("could not fill motion from query result: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := m.FillVoteSums(ctx, []*MotionForDisplay{&result}); err != nil {
|
|
|
|
|
if err := m.FillVoteSums(ctx, []*Motion{&result}); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("could not get vote sums: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -823,7 +798,7 @@ WHERE decisions.tag = ?`,
|
|
|
|
|
return &result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) FillVotes(ctx context.Context, md *MotionForDisplay) error {
|
|
|
|
|
func (m *MotionModel) FillVotes(ctx context.Context, md *Motion) error {
|
|
|
|
|
rows, err := m.DB.QueryxContext(ctx,
|
|
|
|
|
`SELECT voters.name, votes.vote
|
|
|
|
|
FROM voters
|
|
|
|
@ -848,13 +823,13 @@ ORDER BY voters.name`,
|
|
|
|
|
return fmt.Errorf("could not get row: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var voteDisplay VoteForDisplay
|
|
|
|
|
var vote Vote
|
|
|
|
|
|
|
|
|
|
if err := rows.StructScan(&voteDisplay); err != nil {
|
|
|
|
|
if err := rows.StructScan(&vote); err != nil {
|
|
|
|
|
return fmt.Errorf("could not scan row: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
md.Votes = append(md.Votes, &voteDisplay)
|
|
|
|
|
md.Votes = append(md.Votes, &vote)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
@ -876,7 +851,14 @@ func (m *MotionModel) GetByID(ctx context.Context, id int64) (*Motion, error) {
|
|
|
|
|
return &motion, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) Update(ctx context.Context, id int64, voteType *VoteType, title string, content string, due time.Time) error {
|
|
|
|
|
func (m *MotionModel) Update(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
id int64,
|
|
|
|
|
voteType *VoteType,
|
|
|
|
|
title string,
|
|
|
|
|
content string,
|
|
|
|
|
due time.Time,
|
|
|
|
|
) error {
|
|
|
|
|
_, err := m.DB.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`UPDATE decisions SET title=?, content=?, votetype=?, due=?, modified=? WHERE id=?`,
|
|
|
|
@ -888,3 +870,57 @@ func (m *MotionModel) Update(ctx context.Context, id int64, voteType *VoteType,
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Vote struct {
|
|
|
|
|
UserID int64 `db:"voter"`
|
|
|
|
|
MotionID int64 `db:"decision"`
|
|
|
|
|
Vote *VoteChoice `db:"vote"`
|
|
|
|
|
Voted time.Time `db:"voted"`
|
|
|
|
|
Notes string `db:"notes"`
|
|
|
|
|
Name string `db:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *MotionModel) DirectVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error {
|
|
|
|
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("could not start transaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer func(tx *sqlx.Tx) {
|
|
|
|
|
_ = tx.Rollback()
|
|
|
|
|
}(tx)
|
|
|
|
|
|
|
|
|
|
row := tx.QueryRowxContext(ctx, `SELECT * FROM votes WHERE voter=? AND decision=?`, userID, motionID)
|
|
|
|
|
if err := row.Err(); err != nil {
|
|
|
|
|
return fmt.Errorf("could not execute query: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vote := Vote{UserID: userID, MotionID: motionID}
|
|
|
|
|
|
|
|
|
|
if err := row.StructScan(&vote); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
return fmt.Errorf("could not scan vote structure: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
performVoteFn(&vote)
|
|
|
|
|
|
|
|
|
|
if _, err := tx.NamedExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`INSERT INTO votes (decision, voter, vote, voted, notes)
|
|
|
|
|
VALUES (:decision, :voter, :vote, :voted, :notes)
|
|
|
|
|
ON CONFLICT (decision, voter)
|
|
|
|
|
DO UPDATE SET vote=:vote,
|
|
|
|
|
voted=:voted,
|
|
|
|
|
notes=:notes
|
|
|
|
|
WHERE decision = :decision
|
|
|
|
|
AND voter = :voter`,
|
|
|
|
|
vote,
|
|
|
|
|
); err != nil {
|
|
|
|
|
return fmt.Errorf("could not insert or update vote: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
|
return fmt.Errorf("could not commit transaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|