214 lines
4.6 KiB
Go
214 lines
4.6 KiB
Go
|
/*
|
||
|
Copyright 2017-2022 CAcert Inc.
|
||
|
SPDX-License-Identifier: Apache-2.0
|
||
|
|
||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
you may not use this file except in compliance with the License.
|
||
|
You may obtain a copy of the License at
|
||
|
|
||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
Unless required by applicable law or agreed to in writing, software
|
||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
See the License for the specific language governing permissions and
|
||
|
limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package models
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"time"
|
||
|
|
||
|
"github.com/jmoiron/sqlx"
|
||
|
)
|
||
|
|
||
|
type VoteType uint8
|
||
|
|
||
|
const unknownVariant = "unknown"
|
||
|
|
||
|
const (
|
||
|
VoteTypeMotion VoteType = 0
|
||
|
VoteTypeVeto VoteType = 1
|
||
|
)
|
||
|
|
||
|
var voteTypeLabels = map[VoteType]string{
|
||
|
VoteTypeMotion: "motion",
|
||
|
VoteTypeVeto: "veto",
|
||
|
}
|
||
|
|
||
|
func (v VoteType) String() string {
|
||
|
if label, ok := voteTypeLabels[v]; ok {
|
||
|
return label
|
||
|
}
|
||
|
|
||
|
return unknownVariant
|
||
|
}
|
||
|
|
||
|
func (v VoteType) QuorumAndMajority() (int, float32) {
|
||
|
const (
|
||
|
majorityDefault = 0.99
|
||
|
majorityMotion = 0.50
|
||
|
quorumDefault = 1
|
||
|
quorumMotion = 3
|
||
|
)
|
||
|
|
||
|
if v == VoteTypeMotion {
|
||
|
return quorumMotion, majorityMotion
|
||
|
}
|
||
|
|
||
|
return quorumDefault, majorityDefault
|
||
|
}
|
||
|
|
||
|
type VoteStatus int8
|
||
|
|
||
|
const (
|
||
|
voteStatusDeclined VoteStatus = -1
|
||
|
voteStatusPending VoteStatus = 0
|
||
|
voteStatusApproved VoteStatus = 1
|
||
|
voteStatusWithdrawn VoteStatus = -2
|
||
|
)
|
||
|
|
||
|
var voteStatusLabels = map[VoteStatus]string{
|
||
|
voteStatusDeclined: "declined",
|
||
|
voteStatusPending: "pending",
|
||
|
voteStatusApproved: "approved",
|
||
|
voteStatusWithdrawn: "withdrawn",
|
||
|
}
|
||
|
|
||
|
func (v VoteStatus) String() string {
|
||
|
if label, ok := voteStatusLabels[v]; ok {
|
||
|
return label
|
||
|
}
|
||
|
|
||
|
return unknownVariant
|
||
|
}
|
||
|
|
||
|
type VoteChoice int
|
||
|
|
||
|
const (
|
||
|
voteAye VoteChoice = 1
|
||
|
voteNaye VoteChoice = -1
|
||
|
voteAbstain VoteChoice = 0
|
||
|
)
|
||
|
|
||
|
var voteChoiceLabels = map[VoteChoice]string{
|
||
|
voteAye: "aye",
|
||
|
voteNaye: "naye",
|
||
|
voteAbstain: "abstain",
|
||
|
}
|
||
|
|
||
|
func (v VoteChoice) String() string {
|
||
|
if label, ok := voteChoiceLabels[v]; ok {
|
||
|
return label
|
||
|
}
|
||
|
|
||
|
return unknownVariant
|
||
|
}
|
||
|
|
||
|
type Decision struct {
|
||
|
ID int64 `db:"id"`
|
||
|
Proposed time.Time
|
||
|
Proponent int64 `db:"proponent"`
|
||
|
Title string
|
||
|
Content string
|
||
|
Quorum int
|
||
|
Majority int
|
||
|
Status VoteStatus
|
||
|
Due time.Time
|
||
|
Modified time.Time
|
||
|
Tag string
|
||
|
VoteType VoteType
|
||
|
}
|
||
|
|
||
|
type DecisionModel struct {
|
||
|
DB *sqlx.DB
|
||
|
InfoLog *log.Logger
|
||
|
}
|
||
|
|
||
|
// Create a new decision.
|
||
|
func (m *DecisionModel) Create(
|
||
|
proponent *Voter,
|
||
|
voteType VoteType,
|
||
|
title, content string,
|
||
|
proposed, due time.Time,
|
||
|
) (int64, error) {
|
||
|
d := &Decision{
|
||
|
Proposed: proposed.UTC(),
|
||
|
Proponent: proponent.ID,
|
||
|
Title: title,
|
||
|
Content: content,
|
||
|
Due: due.UTC(),
|
||
|
VoteType: voteType,
|
||
|
}
|
||
|
|
||
|
result, err := m.DB.NamedExec(`INSERT INTO decisions
|
||
|
(proposed, proponent, title, content, votetype, status, due, modified, tag)
|
||
|
VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :proposed,
|
||
|
'm' || strftime('%Y%m%d', :proposed) || '.' || (
|
||
|
SELECT COUNT(*)+1 AS num
|
||
|
FROM decisions
|
||
|
WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day')
|
||
|
))`, d)
|
||
|
if err != nil {
|
||
|
return 0, fmt.Errorf("creating motion failed: %w", err)
|
||
|
}
|
||
|
|
||
|
id, err := result.LastInsertId()
|
||
|
if err != nil {
|
||
|
return 0, fmt.Errorf("could not get inserted decision id: %w", err)
|
||
|
}
|
||
|
|
||
|
return id, nil
|
||
|
}
|
||
|
|
||
|
func (m *DecisionModel) CloseDecisions() error {
|
||
|
rows, err := m.DB.NamedQuery(`
|
||
|
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
|
||
|
decisions.votetype, decisions.status, decisions.due, decisions.modified
|
||
|
FROM decisions
|
||
|
WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now().UTC()})
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("fetching closable decisions failed: %w", err)
|
||
|
}
|
||
|
|
||
|
defer func(rows *sqlx.Rows) {
|
||
|
_ = rows.Close()
|
||
|
}(rows)
|
||
|
|
||
|
decisions := make([]*Decision, 0)
|
||
|
|
||
|
for rows.Next() {
|
||
|
decision := &Decision{}
|
||
|
if err = rows.StructScan(decision); err != nil {
|
||
|
return fmt.Errorf("scanning row failed: %w", err)
|
||
|
}
|
||
|
|
||
|
if rows.Err() != nil {
|
||
|
return fmt.Errorf("row error: %w", err)
|
||
|
}
|
||
|
|
||
|
decisions = append(decisions, decision)
|
||
|
}
|
||
|
|
||
|
for _, decision := range decisions {
|
||
|
m.InfoLog.Printf("found closable decision %s", decision.Tag)
|
||
|
|
||
|
if err = m.Close(decision.Tag); err != nil {
|
||
|
return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *DecisionModel) Close(tag string) error {
|
||
|
panic("not implemented")
|
||
|
}
|
||
|
|
||
|
func (m *DecisionModel) FindUnVotedDecisionsForVoter(v *Voter) ([]Decision, error) {
|
||
|
panic("not implemented")
|
||
|
}
|