🚨 go fmt; go mod tidy

This commit is contained in:
Brian Wiborg 2024-05-13 10:45:38 +02:00
parent fb2a29be51
commit b0657a3fb2
No known key found for this signature in database
GPG Key ID: BE53FA9286B719D6
20 changed files with 715 additions and 722 deletions

View File

@ -5,12 +5,12 @@ import (
) )
var App = cli.App{ var App = cli.App{
Name: "govote", Name: "govote",
Usage: "🌈 Referendums and concensus.", Usage: "🌈 Referendums and concensus.",
Commands: []*cli.Command{ Commands: []*cli.Command{
newCmd, newCmd,
showCmd, showCmd,
voteCmd, voteCmd,
serveCmd, serveCmd,
}, },
} }

View File

@ -16,93 +16,93 @@ import (
) )
var newCmd = &cli.Command{ var newCmd = &cli.Command{
Name: "new", Name: "new",
Usage: " Create a voting", Usage: " Create a voting",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "deadline", Name: "deadline",
Usage: "Duration for which this voting is open", Usage: "Duration for which this voting is open",
Aliases: []string{"D"}, Aliases: []string{"D"},
Value: "1m", Value: "1m",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "quorum", Name: "quorum",
Usage: "Minimum required number of participants", Usage: "Minimum required number of participants",
Aliases: []string{"Q"}, Aliases: []string{"Q"},
Value: "SIMPLE", Value: "SIMPLE",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "threshold", Name: "threshold",
Usage: "Minimum number of positive votes", Usage: "Minimum number of positive votes",
Aliases: []string{"T"}, Aliases: []string{"T"},
Value: "SIMPLE", Value: "SIMPLE",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "electors", Name: "electors",
Usage: "Comma-separated list of eligible electors or empty if anyone can vote", Usage: "Comma-separated list of eligible electors or empty if anyone can vote",
Aliases: []string{"E"}, Aliases: []string{"E"},
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "anonymous", Name: "anonymous",
Usage: "Public visibility of votes.", Usage: "Public visibility of votes.",
Aliases: []string{"A"}, Aliases: []string{"A"},
Value: false, Value: false,
}, },
}, },
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
deadline := ctx.String("deadline") deadline := ctx.String("deadline")
deadlineNum := fmt.Sprintf("%s", deadline[:len(deadline)-1]) deadlineNum := fmt.Sprintf("%s", deadline[:len(deadline)-1])
deadlineUnit := fmt.Sprintf("%s", deadline[len(deadline)-1:]) deadlineUnit := fmt.Sprintf("%s", deadline[len(deadline)-1:])
deadlineInt, err := strconv.Atoi(deadlineNum) deadlineInt, err := strconv.Atoi(deadlineNum)
if err != nil { if err != nil {
return err return err
} }
if !strings.Contains("mhd", deadlineUnit) { if !strings.Contains("mhd", deadlineUnit) {
return fmt.Errorf("invalid deadline unit '%s'. use one of: [ m | d | h ]", deadlineUnit) return fmt.Errorf("invalid deadline unit '%s'. use one of: [ m | d | h ]", deadlineUnit)
} }
var d time.Time var d time.Time
switch deadlineUnit { switch deadlineUnit {
case "m", "": case "m", "":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Minute).Round(time.Second) d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Minute).Round(time.Second)
case "h": case "h":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour).Round(time.Second) d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Hour).Round(time.Second)
case "d": case "d":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour*24).Round(time.Second) d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Hour * 24).Round(time.Second)
default: default:
panic("this code should never be reached") panic("this code should never be reached")
} }
var ( var (
q quorum.Quorum q quorum.Quorum
t threshold.Threshold t threshold.Threshold
) )
if q, err = quorum.FromString(ctx.String("quorum")); err != nil { if q, err = quorum.FromString(ctx.String("quorum")); err != nil {
return err return err
} }
if t, err = threshold.FromString(ctx.String("threshold")); err != nil { if t, err = threshold.FromString(ctx.String("threshold")); err != nil {
return err return err
} }
e := strings.Split(ctx.String("electors"), " ") e := strings.Split(ctx.String("electors"), " ")
a := ctx.Bool("anonymous") a := ctx.Bool("anonymous")
var r = "" var r = ""
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
fmt.Print("Give your referendum a concise name or subject: ") fmt.Print("Give your referendum a concise name or subject: ")
inputReader := bufio.NewReader(os.Stdin) inputReader := bufio.NewReader(os.Stdin)
r, _ = inputReader.ReadString('\n') r, _ = inputReader.ReadString('\n')
r = r[:len(r)-1] r = r[:len(r)-1]
} else { } else {
r = strings.Join(ctx.Args().Slice(), " ") r = strings.Join(ctx.Args().Slice(), " ")
} }
id := utils.GenerateRandomString(11) id := utils.GenerateRandomString(11)
if err := store.NewVoting(string(id), r, d, q, t, e, a); err != nil { if err := store.NewVoting(string(id), r, d, q, t, e, a); err != nil {
return err return err
} }
fmt.Println(string(id)) fmt.Println(string(id))
return nil return nil
}, },
} }

View File

@ -6,16 +6,16 @@ import (
) )
var serveCmd = &cli.Command{ var serveCmd = &cli.Command{
Name: "serve", Name: "serve",
Usage: "Start the HTTP server", Usage: "Start the HTTP server",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "bind-address", Name: "bind-address",
Usage: "The TCP address:port to bind to", Usage: "The TCP address:port to bind to",
Value: ":3000", Value: ":3000",
}, },
}, },
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
return http.Serve(ctx.String("bind-address")) return http.Serve(ctx.String("bind-address"))
}, },
} }

View File

@ -10,25 +10,25 @@ import (
) )
var showCmd = &cli.Command{ var showCmd = &cli.Command{
Name: "show", Name: "show",
Usage: "📈 Display a voting", Usage: "📈 Display a voting",
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
var r string var r string
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
inputReader := bufio.NewReader(os.Stdin) inputReader := bufio.NewReader(os.Stdin)
r, _ = inputReader.ReadString('\n') r, _ = inputReader.ReadString('\n')
r = r[:len(r)-1] r = r[:len(r)-1]
} }
id := ctx.Args().Get(0) id := ctx.Args().Get(0)
if id == "" { if id == "" {
return fmt.Errorf("Please provide an ID!") return fmt.Errorf("Please provide an ID!")
} }
voting, err := store.GetVoting(id) voting, err := store.GetVoting(id)
if err != nil { if err != nil {
return err return err
} }
fmt.Println(voting) fmt.Println(voting)
return nil return nil
}, },
} }

View File

@ -8,39 +8,39 @@ import (
) )
var voteCmd = &cli.Command{ var voteCmd = &cli.Command{
Name: "vote", Name: "vote",
Usage: "📄 Place vote", Usage: "📄 Place vote",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "voting-id", Name: "voting-id",
Usage: "Voting ID", Usage: "Voting ID",
Aliases: []string{"V"}, Aliases: []string{"V"},
Required: true, Required: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "elector", Name: "elector",
Usage: "Elector", Usage: "Elector",
Aliases: []string{"E"}, Aliases: []string{"E"},
Required: true, Required: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "choice", Name: "choice",
Usage: "Choice", Usage: "Choice",
Aliases: []string{"C"}, Aliases: []string{"C"},
Required: true, Required: true,
}, },
}, },
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
var ( var (
id = uuid.New().String() id = uuid.New().String()
votingID = ctx.String("voting-id") votingID = ctx.String("voting-id")
elector = ctx.String("elector") elector = ctx.String("elector")
) )
choice, err := vote.ChoiceFromString(ctx.String("choice")) choice, err := vote.ChoiceFromString(ctx.String("choice"))
if err != nil { if err != nil {
return err return err
} }
err = store.PlaceVote(id, votingID, elector, choice) err = store.PlaceVote(id, votingID, elector, choice)
return err return err
}, },
} }

1
go.mod
View File

@ -13,7 +13,6 @@ require (
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect

4
go.sum
View File

@ -8,8 +8,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -35,8 +33,6 @@ github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -19,137 +19,137 @@ import (
) )
func Serve(bindAddr string) error { func Serve(bindAddr string) error {
e := echo.New() e := echo.New()
e.Pre(middleware.RemoveTrailingSlash()) e.Pre(middleware.RemoveTrailingSlash())
// e.Use(middleware.Recover()) // e.Use(middleware.Recover())
NewTemplateRenderer(e, "tmpl/*.html") NewTemplateRenderer(e, "tmpl/*.html")
e.Static("/static", "static")
e.GET("/", handleIndex)
e.GET("/v", handleVotingForm)
e.POST("/v", handleNewVoting)
e.GET("/v/:id", handleShowVoting)
e.POST("/v/:id", handleVote)
return e.Start(bindAddr) e.Static("/static", "static")
e.GET("/", handleIndex)
e.GET("/v", handleVotingForm)
e.POST("/v", handleNewVoting)
e.GET("/v/:id", handleShowVoting)
e.POST("/v/:id", handleVote)
return e.Start(bindAddr)
} }
func handleIndex(ctx echo.Context) error { func handleIndex(ctx echo.Context) error {
return ctx.Redirect(http.StatusTemporaryRedirect, "/v") return ctx.Redirect(http.StatusTemporaryRedirect, "/v")
} }
func handleNewVoting(ctx echo.Context) error { func handleNewVoting(ctx echo.Context) error {
id :=utils.GenerateRandomString(11) id := utils.GenerateRandomString(11)
var ( var (
formReferendum = ctx.FormValue("referendum") formReferendum = ctx.FormValue("referendum")
formDeadline = ctx.FormValue("deadline") formDeadline = ctx.FormValue("deadline")
formQuorum = ctx.FormValue("quorum") formQuorum = ctx.FormValue("quorum")
formThreshold = ctx.FormValue("threshold") formThreshold = ctx.FormValue("threshold")
formElectors = ctx.FormValue("electors") formElectors = ctx.FormValue("electors")
formAnonymous = ctx.FormValue("anonymous") formAnonymous = ctx.FormValue("anonymous")
) )
var ( var (
err error err error
r string r string
d time.Time d time.Time
q quorum.Quorum q quorum.Quorum
t threshold.Threshold t threshold.Threshold
e = []string{} e = []string{}
a bool a bool
) )
r = formReferendum r = formReferendum
deadlineNum := fmt.Sprintf("%s", formDeadline[:len(formDeadline)-1]) deadlineNum := fmt.Sprintf("%s", formDeadline[:len(formDeadline)-1])
deadlineUnit := fmt.Sprintf("%s", formDeadline[len(formDeadline)-1:]) deadlineUnit := fmt.Sprintf("%s", formDeadline[len(formDeadline)-1:])
deadlineInt, err := strconv.Atoi(deadlineNum) deadlineInt, err := strconv.Atoi(deadlineNum)
if err != nil { if err != nil {
return err return err
} }
switch deadlineUnit { switch deadlineUnit {
case "m", "": case "m", "":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Minute).Round(time.Second) d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Minute).Round(time.Second)
case "h": case "h":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour).Round(time.Second) d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Hour).Round(time.Second)
case "d": case "d":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour*24).Round(time.Second) d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Hour * 24).Round(time.Second)
default: default:
panic("this code should never be reached") panic("this code should never be reached")
} }
if q, err = quorum.FromString(formQuorum); err != nil { if q, err = quorum.FromString(formQuorum); err != nil {
return err return err
} }
if t, err = threshold.FromString(formThreshold); err != nil { if t, err = threshold.FromString(formThreshold); err != nil {
return err return err
} }
e = strings.Split(formElectors, " ") e = strings.Split(formElectors, " ")
if formAnonymous == "on" { if formAnonymous == "on" {
a = true a = true
} }
store.NewVoting(id, r, d, q, t, e, a) store.NewVoting(id, r, d, q, t, e, a)
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", id)) return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", id))
} }
func handleVotingForm(ctx echo.Context) error { func handleVotingForm(ctx echo.Context) error {
return ctx.Render(http.StatusOK, "voting_form", nil) return ctx.Render(http.StatusOK, "voting_form", nil)
} }
func handleVote(ctx echo.Context) error { func handleVote(ctx echo.Context) error {
var ( var (
id = uuid.New().String() id = uuid.New().String()
vid = ctx.Param("id") vid = ctx.Param("id")
elector = ctx.Request().Header.Get("X-Remote-User") elector = ctx.Request().Header.Get("X-Remote-User")
choice = ctx.FormValue("vote") choice = ctx.FormValue("vote")
v *voting.Voting v *voting.Voting
c vote.Choice c vote.Choice
err error err error
) )
v, err = store.GetVoting(vid) v, err = store.GetVoting(vid)
if err != nil { if err != nil {
return err return err
} }
if time.Now().UTC().After(v.Deadline()) { if time.Now().UTC().After(v.Deadline()) {
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", vid)) return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", vid))
} }
if !eligible(elector, v.Electors()) { if !eligible(elector, v.Electors()) {
return ctx.String(http.StatusForbidden, "") return ctx.String(http.StatusForbidden, "")
} }
if c, err = vote.ChoiceFromString(choice); err != nil { if c, err = vote.ChoiceFromString(choice); err != nil {
return err return err
} }
store.PlaceVote(id, vid, elector, c) store.PlaceVote(id, vid, elector, c)
return ctx.Render(http.StatusFound, "thanks", map[string]interface{}{ return ctx.Render(http.StatusFound, "thanks", map[string]interface{}{
"Id": id, "Id": id,
"Vid": vid, "Vid": vid,
}) })
} }
func handleShowVoting(ctx echo.Context) error { func handleShowVoting(ctx echo.Context) error {
v, err := store.GetVoting(ctx.Param("id")) v, err := store.GetVoting(ctx.Param("id"))
if err != nil { if err != nil {
return err return err
} }
if v.Deadline().After(time.Now().UTC()) { if v.Deadline().After(time.Now().UTC()) {
if !eligible(ctx.Request().Header.Get("X-Remote-User"), v.Electors()) { if !eligible(ctx.Request().Header.Get("X-Remote-User"), v.Electors()) {
return ctx.String(http.StatusForbidden, "") return ctx.String(http.StatusForbidden, "")
} }
} }
return ctx.Render(http.StatusOK, "voting", map[string]interface{}{ return ctx.Render(http.StatusOK, "voting", map[string]interface{}{
"Voting": v, "Voting": v,
}) })
} }
func eligible(e string, electors []string) bool { func eligible(e string, electors []string) bool {
if electors == nil || len(electors) == 0 { if electors == nil || len(electors) == 0 {
return true return true
} }
for _, _e := range electors { for _, _e := range electors {
if strings.ToLower(_e) == strings.ToLower(e) { if strings.ToLower(_e) == strings.ToLower(e) {
return true return true
} }
} }
return false return false
} }

View File

@ -8,24 +8,24 @@ import (
) )
type Template struct { type Template struct {
Templates *template.Template Templates *template.Template
} }
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.Templates.ExecuteTemplate(w, name, data) return t.Templates.ExecuteTemplate(w, name, data)
} }
func NewTemplateRenderer(e *echo.Echo, paths ...string) { func NewTemplateRenderer(e *echo.Echo, paths ...string) {
tmpl := &template.Template{} tmpl := &template.Template{}
for i := range paths { for i := range paths {
template.Must(tmpl.ParseGlob(paths[i])) template.Must(tmpl.ParseGlob(paths[i]))
} }
t := newTemplate(tmpl) t := newTemplate(tmpl)
e.Renderer = t e.Renderer = t
} }
func newTemplate(templates *template.Template) echo.Renderer { func newTemplate(templates *template.Template) echo.Renderer {
return &Template{ return &Template{
Templates: templates, Templates: templates,
} }
} }

View File

@ -11,8 +11,8 @@ import (
//go:generate cp global.css static/css/ //go:generate cp global.css static/css/
func main() { func main() {
app := cmd.App app := cmd.App
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }

View File

@ -1,8 +1,8 @@
package store package store
import ( import (
"database/sql" "database/sql"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
const file = "./govote.db" const file = "./govote.db"
@ -10,12 +10,11 @@ const file = "./govote.db"
var db *sql.DB var db *sql.DB
func init() { func init() {
var err error var err error
if db, err = sql.Open("sqlite3", file); err != nil { if db, err = sql.Open("sqlite3", file); err != nil {
panic(err) panic(err)
} }
initCreateTables(db) initCreateTables(db)
initStmts(db) initStmts(db)
} }

View File

@ -12,103 +12,103 @@ import (
) )
func NewVoting( func NewVoting(
id string, id string,
r string, r string,
d time.Time, d time.Time,
q quorum.Quorum, q quorum.Quorum,
t threshold.Threshold, t threshold.Threshold,
e []string, e []string,
a bool, a bool,
) error { ) error {
electors := strings.Join(e, " ") electors := strings.Join(e, " ")
if _, err := votingInsert.Exec(id, r, d.String(), q.String(), t.String(), electors, a); err != nil { if _, err := votingInsert.Exec(id, r, d.String(), q.String(), t.String(), electors, a); err != nil {
return err return err
} }
return nil return nil
} }
func GetVoting(id string) (*voting.Voting, error) { func GetVoting(id string) (*voting.Voting, error) {
result := votingSelect.QueryRow(id) result := votingSelect.QueryRow(id)
if result == nil { if result == nil {
return nil, fmt.Errorf("not found: %s", id) return nil, fmt.Errorf("not found: %s", id)
} }
var ( var (
err error err error
r string r string
d time.Time d time.Time
q quorum.Quorum q quorum.Quorum
t threshold.Threshold t threshold.Threshold
e []string e []string
a bool a bool
dbDeadline string dbDeadline string
dbQuorum string dbQuorum string
dbThreshold string dbThreshold string
dbElectors string dbElectors string
) )
if err := result.Scan(&r, &dbDeadline, &dbQuorum, &dbThreshold, &dbElectors, &a); err != nil { if err := result.Scan(&r, &dbDeadline, &dbQuorum, &dbThreshold, &dbElectors, &a); err != nil {
return nil, err return nil, err
} }
if d, err = time.Parse("2006-01-02 15:04:05 -0700 MST", dbDeadline); err != nil {
return nil, err
}
if q, err = quorum.FromString(dbQuorum); err != nil { if d, err = time.Parse("2006-01-02 15:04:05 -0700 MST", dbDeadline); err != nil {
return nil, err return nil, err
} }
if t, err = threshold.FromString(dbThreshold); err != nil {
return nil, err
}
for _, _e := range strings.Split(dbElectors, " ") {
if _e != "" {
e = append(e, _e)
}
}
votes, err := getVotes(id) if q, err = quorum.FromString(dbQuorum); err != nil {
if err != nil { return nil, err
return nil, err }
} if t, err = threshold.FromString(dbThreshold); err != nil {
v := voting.NewVotingWithVotes(id, r, d, q, t, e, a, votes) return nil, err
return v, nil }
for _, _e := range strings.Split(dbElectors, " ") {
if _e != "" {
e = append(e, _e)
}
}
votes, err := getVotes(id)
if err != nil {
return nil, err
}
v := voting.NewVotingWithVotes(id, r, d, q, t, e, a, votes)
return v, nil
} }
func PlaceVote(id, votingID, elector string, choice vote.Choice) error { func PlaceVote(id, votingID, elector string, choice vote.Choice) error {
if _, err := voteInsert.Exec(id, votingID, elector, choice.String()); err != nil { if _, err := voteInsert.Exec(id, votingID, elector, choice.String()); err != nil {
return err return err
} }
return nil return nil
} }
func getVotes(id string) ([]vote.Vote, error) { func getVotes(id string) ([]vote.Vote, error) {
result, err := voteSelect.Query(id) result, err := voteSelect.Query(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var ( var (
e string e string
c vote.Choice c vote.Choice
ts time.Time ts time.Time
dbChoice string dbChoice string
dbTimestamp string dbTimestamp string
votes = []vote.Vote{} votes = []vote.Vote{}
) )
for result.Next() { for result.Next() {
if err = result.Scan(&e, &dbChoice, &dbTimestamp); err != nil { if err = result.Scan(&e, &dbChoice, &dbTimestamp); err != nil {
return nil, err return nil, err
} }
if c, err = vote.ChoiceFromString(dbChoice); err != nil { if c, err = vote.ChoiceFromString(dbChoice); err != nil {
return nil, err return nil, err
} }
if ts, err = time.Parse("2006-01-02 15:04:05", dbTimestamp); err != nil { if ts, err = time.Parse("2006-01-02 15:04:05", dbTimestamp); err != nil {
return nil, err return nil, err
} }
v := vote.NewVoteWithTimestamp(e, c, ts) v := vote.NewVoteWithTimestamp(e, c, ts)
votes = append(votes, v) votes = append(votes, v)
} }
return votes, nil return votes, nil
} }

View File

@ -3,16 +3,16 @@ package store
import "database/sql" import "database/sql"
var ( var (
votingInsert *sql.Stmt votingInsert *sql.Stmt
votingSelect *sql.Stmt votingSelect *sql.Stmt
voteEligible *sql.Stmt voteEligible *sql.Stmt
voteInsert *sql.Stmt voteInsert *sql.Stmt
voteSelect *sql.Stmt voteSelect *sql.Stmt
) )
func initCreateTables(db *sql.DB) { func initCreateTables(db *sql.DB) {
var err error var err error
createTables := ` createTables := `
CREATE TABLE IF NOT EXISTS voting ( CREATE TABLE IF NOT EXISTS voting (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
referendum TEXT NOT NULL, referendum TEXT NOT NULL,
@ -36,23 +36,23 @@ func initCreateTables(db *sql.DB) {
CREATE INDEX IF NOT EXISTS vote_voting ON vote ( voting ); CREATE INDEX IF NOT EXISTS vote_voting ON vote ( voting );
` `
if _, err = db.Exec(createTables); err != nil { if _, err = db.Exec(createTables); err != nil {
panic(err) panic(err)
} }
} }
func initStmts(db *sql.DB) { func initStmts(db *sql.DB) {
initStmtVotingInsert(db) initStmtVotingInsert(db)
initStmtVotingSelect(db) initStmtVotingSelect(db)
initStmtVoteEligible(db) initStmtVoteEligible(db)
initStmtVoteInsert(db) initStmtVoteInsert(db)
initStmtVoteSelect(db) initStmtVoteSelect(db)
} }
func initStmtVotingInsert(db *sql.DB) { func initStmtVotingInsert(db *sql.DB) {
var err error var err error
if votingInsert, err = db.Prepare(` if votingInsert, err = db.Prepare(`
INSERT INTO voting ( INSERT INTO voting (
id, id,
referendum, referendum,
@ -63,13 +63,13 @@ func initStmtVotingInsert(db *sql.DB) {
anonymous anonymous
) VALUES (?, ?, ?, ?, ?, ?, ?); ) VALUES (?, ?, ?, ?, ?, ?, ?);
`); err != nil { `); err != nil {
panic(err) panic(err)
} }
} }
func initStmtVotingSelect(db *sql.DB) { func initStmtVotingSelect(db *sql.DB) {
var err error var err error
if votingSelect, err = db.Prepare(` if votingSelect, err = db.Prepare(`
SELECT SELECT
referendum, referendum,
deadline, deadline,
@ -80,13 +80,13 @@ func initStmtVotingSelect(db *sql.DB) {
FROM voting FROM voting
WHERE id = ?; WHERE id = ?;
`); err != nil { `); err != nil {
panic(err) panic(err)
} }
} }
func initStmtVoteEligible(db *sql.DB) { func initStmtVoteEligible(db *sql.DB) {
var err error var err error
if voteEligible, err = db.Prepare(` if voteEligible, err = db.Prepare(`
SELECT SELECT
id, id,
referendum, referendum,
@ -104,26 +104,26 @@ func initStmtVoteEligible(db *sql.DB) {
) )
LIMIT 1; LIMIT 1;
`); err != nil { `); err != nil {
panic(err) panic(err)
} }
} }
func initStmtVoteInsert(db *sql.DB) { func initStmtVoteInsert(db *sql.DB) {
var err error var err error
if voteInsert, err = db.Prepare(` if voteInsert, err = db.Prepare(`
INSERT INTO vote ( INSERT INTO vote (
id, voting, elector, choice id, voting, elector, choice
) VALUES ( ) VALUES (
?, ?, ?, ? ?, ?, ?, ?
); );
`); err != nil { `); err != nil {
panic(err) panic(err)
} }
} }
func initStmtVoteSelect(db *sql.DB) { func initStmtVoteSelect(db *sql.DB) {
var err error var err error
if voteSelect, err = db.Prepare(` if voteSelect, err = db.Prepare(`
SELECT SELECT
elector, elector,
choice, choice,
@ -131,6 +131,6 @@ func initStmtVoteSelect(db *sql.DB) {
FROM vote FROM vote
WHERE voting = ?; WHERE voting = ?;
`); err != nil { `); err != nil {
panic(err) panic(err)
} }
} }

View File

@ -7,9 +7,9 @@ import (
const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func GenerateRandomString(length int) string { func GenerateRandomString(length int) string {
result := make([]byte, length) result := make([]byte, length)
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
result[i] = charSet[rand.Intn(len(charSet))] result[i] = charSet[rand.Intn(len(charSet))]
} }
return string(result) return string(result)
} }

View File

@ -13,95 +13,95 @@ import (
type ( type (
Voting struct { Voting struct {
id string id string
referendum string referendum string
deadline time.Time deadline time.Time
quorum quorum.Quorum quorum quorum.Quorum
threshold threshold.Threshold threshold threshold.Threshold
electors []string electors []string
annonymous bool annonymous bool
votes []vote.Vote votes []vote.Vote
} }
) )
func NewVoting(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool) *Voting { func NewVoting(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool) *Voting {
return &Voting{ return &Voting{
id: id, id: id,
referendum: r, referendum: r,
deadline: d, deadline: d,
quorum: q, quorum: q,
threshold: t, threshold: t,
electors: e, electors: e,
annonymous: a, annonymous: a,
votes: []vote.Vote{}, votes: []vote.Vote{},
} }
} }
func NewVotingWithVotes(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool, v []vote.Vote) *Voting { func NewVotingWithVotes(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool, v []vote.Vote) *Voting {
voting := NewVoting(id, r, d, q, t, e, a) voting := NewVoting(id, r, d, q, t, e, a)
for i := range v { for i := range v {
voting.votes = append(voting.votes, v[i]) voting.votes = append(voting.votes, v[i])
} }
return voting return voting
} }
func (v Voting) String() string { func (v Voting) String() string {
var ( var (
possibleVotes int = len(v.electors) possibleVotes int = len(v.electors)
totalVotes int = len(v.Votes()) totalVotes int = len(v.Votes())
yesVotes int = len(v.yesVotes()) yesVotes int = len(v.yesVotes())
noVotes int = len(v.noVotes()) noVotes int = len(v.noVotes())
deadlineStatus string = "🎭 ONGOING 🎭" deadlineStatus string = "🎭 ONGOING 🎭"
quorumStatus string = "❌ FAIL" quorumStatus string = "❌ FAIL"
thresholdStatus string = "❌ FAIL" thresholdStatus string = "❌ FAIL"
out string = "" out string = ""
) )
quorumSatisfied := v.quorum.IsSatisfied(possibleVotes, totalVotes)
thresholdSatisfied := v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes)
votingSatisfied := quorumSatisfied && thresholdSatisfied
if time.Now().UTC().After(v.deadline) { quorumSatisfied := v.quorum.IsSatisfied(possibleVotes, totalVotes)
if votingSatisfied { thresholdSatisfied := v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes)
deadlineStatus = "✅ APROVED ✅" votingSatisfied := quorumSatisfied && thresholdSatisfied
} else {
deadlineStatus = "❌ REJECTED ❌" if time.Now().UTC().After(v.deadline) {
} if votingSatisfied {
} deadlineStatus = "✅ APROVED ✅"
if v.quorum.IsSatisfied(possibleVotes, totalVotes) { } else {
quorumStatus = "✅ PASS" deadlineStatus = "❌ REJECTED ❌"
} }
if v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) { }
thresholdStatus = "✅ PASS" if v.quorum.IsSatisfied(possibleVotes, totalVotes) {
} quorumStatus = "✅ PASS"
}
if v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) {
thresholdStatus = "✅ PASS"
}
out += fmt.Sprintf("Referendum: %s\n", strings.ToUpper(v.referendum)) out += fmt.Sprintf("Referendum: %s\n", strings.ToUpper(v.referendum))
out += fmt.Sprintf("Deadline : %s UTC\n", v.deadline.Format(time.DateTime)) out += fmt.Sprintf("Deadline : %s UTC\n", v.deadline.Format(time.DateTime))
out += fmt.Sprintf("Quorum : %s (%d/%d) (required: %s)\n", quorumStatus, totalVotes, possibleVotes, v.quorum) out += fmt.Sprintf("Quorum : %s (%d/%d) (required: %s)\n", quorumStatus, totalVotes, possibleVotes, v.quorum)
out += fmt.Sprintf("Threshold : %s (%d/%d) (required: %s)\n", thresholdStatus, yesVotes, totalVotes, v.threshold) out += fmt.Sprintf("Threshold : %s (%d/%d) (required: %s)\n", thresholdStatus, yesVotes, totalVotes, v.threshold)
out += fmt.Sprintf("Electors : [ %d ] %s\n", len(v.electors), v.electors) out += fmt.Sprintf("Electors : [ %d ] %s\n", len(v.electors), v.electors)
out += fmt.Sprintf( out += fmt.Sprintf(
"Votes : [ %d | %d | %d ] (❎|❌|❔)\n", "Votes : [ %d | %d | %d ] (❎|❌|❔)\n",
len(v.yesVotes()), len(v.yesVotes()),
len(v.noVotes()), len(v.noVotes()),
len(v.abstainVotes()), len(v.abstainVotes()),
) )
out += fmt.Sprintf("Status : %s\n", deadlineStatus) out += fmt.Sprintf("Status : %s\n", deadlineStatus)
if !v.annonymous { if !v.annonymous {
out += "\n" out += "\n"
if v.votes != nil && len(v.votes) > 0 { if v.votes != nil && len(v.votes) > 0 {
for _, _v := range v.votes { for _, _v := range v.votes {
out += fmt.Sprintf("💬 %s\n", _v) out += fmt.Sprintf("💬 %s\n", _v)
} }
} }
} }
return out return out
} }
func (v Voting) ID() string { func (v Voting) ID() string {
return v.id return v.id
} }
func (v Voting) Referendum() string { func (v Voting) Referendum() string {
@ -109,15 +109,15 @@ func (v Voting) Referendum() string {
} }
func (v Voting) Deadline() time.Time { func (v Voting) Deadline() time.Time {
return v.deadline return v.deadline
} }
func (v Voting) Quorum() string { func (v Voting) Quorum() string {
return v.quorum.String() return v.quorum.String()
} }
func (v Voting) Threshold() string { func (v Voting) Threshold() string {
return v.threshold.String() return v.threshold.String()
} }
func (v Voting) Electors() []string { func (v Voting) Electors() []string {
@ -129,75 +129,74 @@ func (v Voting) Electors() []string {
} }
func (v Voting) IsOpen() bool { func (v Voting) IsOpen() bool {
return v.deadline.After(time.Now().UTC()) return v.deadline.After(time.Now().UTC())
} }
func (v Voting) Result() Result { func (v Voting) Result() Result {
var ( var (
possibleVotes = len(v.electors) possibleVotes = len(v.electors)
totalVotes = len(v.Votes()) totalVotes = len(v.Votes())
yesVotes = len(v.yesVotes()) yesVotes = len(v.yesVotes())
noVotes = len(v.noVotes()) noVotes = len(v.noVotes())
votes []vote.Vote votes []vote.Vote
quorumSatisfied = v.quorum.IsSatisfied(possibleVotes, totalVotes) quorumSatisfied = v.quorum.IsSatisfied(possibleVotes, totalVotes)
thresholdSatisfied = v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) thresholdSatisfied = v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes)
) )
if !v.annonymous { if !v.annonymous {
votes = v.Votes() votes = v.Votes()
} }
return Result{ return Result{
Quorum: quorumSatisfied, Quorum: quorumSatisfied,
Threshold: thresholdSatisfied, Threshold: thresholdSatisfied,
Yes: len(v.yesVotes()), Yes: len(v.yesVotes()),
No: len(v.noVotes()), No: len(v.noVotes()),
Abstain: len(v.Votes()) - len(v.yesVotes()) - len(v.noVotes()), Abstain: len(v.Votes()) - len(v.yesVotes()) - len(v.noVotes()),
Votes: votes, Votes: votes,
} }
} }
func (v *Voting) Vote(vote vote.Vote) error { func (v *Voting) Vote(vote vote.Vote) error {
if time.Now().UTC().After(v.deadline) { if time.Now().UTC().After(v.deadline) {
return fmt.Errorf("deadline has passed") return fmt.Errorf("deadline has passed")
} }
for _, elector := range v.electors { for _, elector := range v.electors {
if elector == vote.Elector { if elector == vote.Elector {
v.votes = append(v.votes, vote) v.votes = append(v.votes, vote)
return nil return nil
} }
} }
return fmt.Errorf("not eligable to vote: %s", vote.Elector) return fmt.Errorf("not eligable to vote: %s", vote.Elector)
} }
func (v Voting) Votes() []vote.Vote { func (v Voting) Votes() []vote.Vote {
votes := []vote.Vote{} votes := []vote.Vote{}
nextVote: nextVote:
for i := len(v.votes)-1; i >= 0; i-- { for i := len(v.votes) - 1; i >= 0; i-- {
elector := v.votes[i].Elector elector := v.votes[i].Elector
for _, e := range votes { for _, e := range votes {
if e.Elector == elector { if e.Elector == elector {
continue nextVote continue nextVote
} }
} }
votes = append(votes, v.votes[i]) votes = append(votes, v.votes[i])
} }
return votes return votes
} }
func (v Voting) yesVotes() []vote.Vote { func (v Voting) yesVotes() []vote.Vote {
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Yes } filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Yes }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote) return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
} }
func (v Voting) noVotes() []vote.Vote { func (v Voting) noVotes() []vote.Vote {
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.No } filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.No }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote) return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
} }
func (v Voting) abstainVotes() []vote.Vote { func (v Voting) abstainVotes() []vote.Vote {
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Abstain } filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Abstain }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote) return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
} }

View File

@ -8,96 +8,96 @@ import (
type Quorum uint8 type Quorum uint8
const ( const (
Simple Quorum = iota Simple Quorum = iota
OneFifth OneFifth
OneQuarter OneQuarter
OneThird OneThird
OneHalf OneHalf
TwoFifths TwoFifths
TwoThirds TwoThirds
ThreeQuarters ThreeQuarters
ThreeFifths ThreeFifths
FourFifths FourFifths
Unanimous Unanimous
) )
func ValidQuorums() []Quorum { func ValidQuorums() []Quorum {
return []Quorum{ return []Quorum{
Simple, Simple,
OneFifth, OneQuarter, OneThird, OneHalf, OneFifth, OneQuarter, OneThird, OneHalf,
TwoThirds, TwoFifths, TwoThirds, TwoFifths,
ThreeQuarters, ThreeFifths, ThreeQuarters, ThreeFifths,
FourFifths, FourFifths,
} }
} }
func FromString(s string) (Quorum, error) { func FromString(s string) (Quorum, error) {
for _, q := range ValidQuorums() { for _, q := range ValidQuorums() {
if strings.ToUpper(q.String()) == strings.ToUpper(s) { if strings.ToUpper(q.String()) == strings.ToUpper(s) {
return q, nil return q, nil
} }
} }
return Simple, fmt.Errorf("inalid quorum: %s", s) return Simple, fmt.Errorf("inalid quorum: %s", s)
} }
func (q Quorum) String() string { func (q Quorum) String() string {
switch q { switch q {
case Simple: case Simple:
return "SIMPLE" return "SIMPLE"
case OneFifth: case OneFifth:
return "1/5" return "1/5"
case OneQuarter: case OneQuarter:
return "1/4" return "1/4"
case OneThird: case OneThird:
return "1/3" return "1/3"
case OneHalf: case OneHalf:
return "1/2" return "1/2"
case TwoThirds: case TwoThirds:
return "2/3" return "2/3"
case TwoFifths: case TwoFifths:
return "2/5" return "2/5"
case ThreeQuarters: case ThreeQuarters:
return "3/4" return "3/4"
case ThreeFifths: case ThreeFifths:
return "3/5" return "3/5"
case FourFifths: case FourFifths:
return "4/5" return "4/5"
case Unanimous: case Unanimous:
return "ALL" return "ALL"
default: default:
panic("this code should never be reached") panic("this code should never be reached")
} }
} }
func (q Quorum) IsSatisfied(possibleVotes, totalVotes int) bool { func (q Quorum) IsSatisfied(possibleVotes, totalVotes int) bool {
if totalVotes == 0 { if totalVotes == 0 {
return false return false
} }
switch q { switch q {
case Simple: case Simple:
return true return true
case OneFifth: case OneFifth:
return totalVotes * 5 >= possibleVotes return totalVotes*5 >= possibleVotes
case OneQuarter: case OneQuarter:
return totalVotes * 4 >= possibleVotes return totalVotes*4 >= possibleVotes
case OneThird: case OneThird:
return totalVotes * 3 >= possibleVotes return totalVotes*3 >= possibleVotes
case OneHalf: case OneHalf:
return totalVotes * 2 >= possibleVotes return totalVotes*2 >= possibleVotes
case TwoThirds: case TwoThirds:
return totalVotes * 3 >= possibleVotes * 2 return totalVotes*3 >= possibleVotes*2
case TwoFifths: case TwoFifths:
return totalVotes * 5 >= possibleVotes * 2 return totalVotes*5 >= possibleVotes*2
case ThreeQuarters: case ThreeQuarters:
return totalVotes * 4 >= possibleVotes * 3 return totalVotes*4 >= possibleVotes*3
case ThreeFifths: case ThreeFifths:
return totalVotes * 5 >= possibleVotes * 3 return totalVotes*5 >= possibleVotes*3
case FourFifths: case FourFifths:
return totalVotes * 5 >= possibleVotes * 4 return totalVotes*5 >= possibleVotes*4
case Unanimous: case Unanimous:
return totalVotes >= possibleVotes return totalVotes >= possibleVotes
default: default:
panic("this code should never be reached⚜") panic("this code should never be reached⚜")
} }
return false return false
} }

View File

@ -3,8 +3,8 @@ package voting
import "code.c-base.org/baccenfutter/govote/voting/vote" import "code.c-base.org/baccenfutter/govote/voting/vote"
type Result struct { type Result struct {
Quorum bool Quorum bool
Threshold bool Threshold bool
Yes, No, Abstain int Yes, No, Abstain int
Votes []vote.Vote Votes []vote.Vote
} }

View File

@ -8,95 +8,95 @@ import (
type Threshold uint8 type Threshold uint8
const ( const (
Simple Threshold = iota Simple Threshold = iota
OneFifth OneFifth
OneQuarter OneQuarter
OneThird OneThird
OneHalf OneHalf
TwoThirds TwoThirds
TwoFifths TwoFifths
ThreeQuarters ThreeQuarters
ThreeFifths ThreeFifths
FourFifths FourFifths
Unanimous Unanimous
) )
func ValidThresholds() []Threshold { func ValidThresholds() []Threshold {
return []Threshold{ return []Threshold{
Simple, Simple,
OneFifth, OneQuarter, OneThird, OneHalf, OneFifth, OneQuarter, OneThird, OneHalf,
TwoFifths, TwoThirds, TwoFifths, TwoThirds,
ThreeFifths, ThreeQuarters, ThreeFifths, ThreeFifths, ThreeQuarters, ThreeFifths,
FourFifths, FourFifths,
} }
} }
func FromString(s string) (Threshold, error) { func FromString(s string) (Threshold, error) {
for _, t := range ValidThresholds() { for _, t := range ValidThresholds() {
if strings.ToUpper(t.String()) == strings.ToUpper(s) { if strings.ToUpper(t.String()) == strings.ToUpper(s) {
return t, nil return t, nil
} }
} }
return Simple, fmt.Errorf("invalid threshold: %s", s) return Simple, fmt.Errorf("invalid threshold: %s", s)
} }
func (t Threshold) String() string { func (t Threshold) String() string {
switch t { switch t {
case Simple: case Simple:
return "SIMPLE" return "SIMPLE"
case OneFifth: case OneFifth:
return "1/5" return "1/5"
case OneQuarter: case OneQuarter:
return "1/4" return "1/4"
case OneThird: case OneThird:
return "1/3" return "1/3"
case OneHalf: case OneHalf:
return "1/2" return "1/2"
case TwoThirds: case TwoThirds:
return "2/3" return "2/3"
case TwoFifths: case TwoFifths:
return "2/5" return "2/5"
case ThreeQuarters: case ThreeQuarters:
return "3/4" return "3/4"
case ThreeFifths: case ThreeFifths:
return "3/5" return "3/5"
case FourFifths: case FourFifths:
return "4/5" return "4/5"
case Unanimous: case Unanimous:
return "ALL" return "ALL"
default: default:
panic("this code should never be reached") panic("this code should never be reached")
} }
} }
func (t Threshold) IsSatisfied(totalVotes, yesVotes, noVotes int) bool { func (t Threshold) IsSatisfied(totalVotes, yesVotes, noVotes int) bool {
if totalVotes == 0 { if totalVotes == 0 {
return false return false
} }
switch t { switch t {
case Simple: case Simple:
return yesVotes > noVotes return yesVotes > noVotes
case OneFifth: case OneFifth:
return yesVotes * 5 >= totalVotes return yesVotes*5 >= totalVotes
case OneQuarter: case OneQuarter:
return yesVotes * 4 >= totalVotes return yesVotes*4 >= totalVotes
case OneThird: case OneThird:
return yesVotes * 3 >= totalVotes return yesVotes*3 >= totalVotes
case OneHalf: case OneHalf:
return yesVotes * 2 >= totalVotes return yesVotes*2 >= totalVotes
case TwoThirds: case TwoThirds:
return yesVotes * 3 >= totalVotes * 2 return yesVotes*3 >= totalVotes*2
case TwoFifths: case TwoFifths:
return yesVotes * 5 >= totalVotes * 2 return yesVotes*5 >= totalVotes*2
case ThreeQuarters: case ThreeQuarters:
return yesVotes * 4 >= totalVotes * 3 return yesVotes*4 >= totalVotes*3
case ThreeFifths: case ThreeFifths:
return yesVotes * 5 >= totalVotes * 3 return yesVotes*5 >= totalVotes*3
case FourFifths: case FourFifths:
return yesVotes * 5 >= totalVotes * 4 return yesVotes*5 >= totalVotes*4
case Unanimous: case Unanimous:
return yesVotes >= totalVotes return yesVotes >= totalVotes
default: default:
panic("this code should never be reached") panic("this code should never be reached")
} }
} }

View File

@ -8,33 +8,33 @@ import (
type Choice int8 type Choice int8
func (choice Choice) String() string { func (choice Choice) String() string {
switch choice { switch choice {
case Yes: case Yes:
return "YIP" return "YIP"
case No: case No:
return "NOPE" return "NOPE"
case Abstain: case Abstain:
return "DUNNO" return "DUNNO"
default: default:
panic("this code should never be reached") panic("this code should never be reached")
} }
} }
const ( const (
Abstain Choice = 0 Abstain Choice = 0
Yes Choice = 1 Yes Choice = 1
No Choice = -1 No Choice = -1
) )
func ValidChoices() []Choice { func ValidChoices() []Choice {
return []Choice{Yes, No, Abstain} return []Choice{Yes, No, Abstain}
} }
func ChoiceFromString(s string) (Choice, error) { func ChoiceFromString(s string) (Choice, error) {
for _, c := range ValidChoices() { for _, c := range ValidChoices() {
if strings.ToUpper(c.String()) == strings.ToUpper(s) { if strings.ToUpper(c.String()) == strings.ToUpper(s) {
return c, nil return c, nil
} }
} }
return Abstain, fmt.Errorf("invalid choice: %s", s) return Abstain, fmt.Errorf("invalid choice: %s", s)
} }

View File

@ -6,27 +6,27 @@ import (
) )
type Vote struct { type Vote struct {
Elector string Elector string
Choice Choice Choice Choice
timestamp time.Time timestamp time.Time
} }
func NewVote(elector string, choice Choice) Vote { func NewVote(elector string, choice Choice) Vote {
return Vote{ return Vote{
Elector: elector, Elector: elector,
Choice: choice, Choice: choice,
timestamp: time.Now().UTC(), timestamp: time.Now().UTC(),
} }
} }
func NewVoteWithTimestamp(elector string, choice Choice, timestamp time.Time) Vote { func NewVoteWithTimestamp(elector string, choice Choice, timestamp time.Time) Vote {
return Vote{ return Vote{
Elector: elector, Elector: elector,
Choice: choice, Choice: choice,
timestamp: timestamp, timestamp: timestamp,
} }
} }
func (vote Vote) String() string { func (vote Vote) String() string {
return fmt.Sprintf("%s %s %s", vote.timestamp.Format(time.DateTime), vote.Choice, vote.Elector) return fmt.Sprintf("%s %s %s", vote.timestamp.Format(time.DateTime), vote.Choice, vote.Elector)
} }