🚨 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{
Name: "govote",
Usage: "🌈 Referendums and concensus.",
Commands: []*cli.Command{
newCmd,
showCmd,
voteCmd,
serveCmd,
},
Name: "govote",
Usage: "🌈 Referendums and concensus.",
Commands: []*cli.Command{
newCmd,
showCmd,
voteCmd,
serveCmd,
},
}

View File

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

View File

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

View File

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

View File

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

1
go.mod
View File

@ -13,7 +13,6 @@ require (
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // 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/mattn/go-colorable v0.1.13 // 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/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/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/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
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=
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/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/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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 {
e := echo.New()
e.Pre(middleware.RemoveTrailingSlash())
// e.Use(middleware.Recover())
e := echo.New()
e.Pre(middleware.RemoveTrailingSlash())
// 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)
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)
return e.Start(bindAddr)
}
func handleIndex(ctx echo.Context) error {
return ctx.Redirect(http.StatusTemporaryRedirect, "/v")
return ctx.Redirect(http.StatusTemporaryRedirect, "/v")
}
func handleNewVoting(ctx echo.Context) error {
id :=utils.GenerateRandomString(11)
var (
formReferendum = ctx.FormValue("referendum")
formDeadline = ctx.FormValue("deadline")
formQuorum = ctx.FormValue("quorum")
formThreshold = ctx.FormValue("threshold")
formElectors = ctx.FormValue("electors")
formAnonymous = ctx.FormValue("anonymous")
)
id := utils.GenerateRandomString(11)
var (
formReferendum = ctx.FormValue("referendum")
formDeadline = ctx.FormValue("deadline")
formQuorum = ctx.FormValue("quorum")
formThreshold = ctx.FormValue("threshold")
formElectors = ctx.FormValue("electors")
formAnonymous = ctx.FormValue("anonymous")
)
var (
err error
r string
d time.Time
q quorum.Quorum
t threshold.Threshold
e = []string{}
a bool
)
var (
err error
r string
d time.Time
q quorum.Quorum
t threshold.Threshold
e = []string{}
a bool
)
r = formReferendum
deadlineNum := fmt.Sprintf("%s", formDeadline[:len(formDeadline)-1])
deadlineUnit := fmt.Sprintf("%s", formDeadline[len(formDeadline)-1:])
deadlineInt, err := strconv.Atoi(deadlineNum)
if err != nil {
return err
}
switch deadlineUnit {
case "m", "":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Minute).Round(time.Second)
case "h":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour).Round(time.Second)
case "d":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour*24).Round(time.Second)
default:
panic("this code should never be reached")
}
r = formReferendum
deadlineNum := fmt.Sprintf("%s", formDeadline[:len(formDeadline)-1])
deadlineUnit := fmt.Sprintf("%s", formDeadline[len(formDeadline)-1:])
deadlineInt, err := strconv.Atoi(deadlineNum)
if err != nil {
return err
}
switch deadlineUnit {
case "m", "":
d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Minute).Round(time.Second)
case "h":
d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Hour).Round(time.Second)
case "d":
d = time.Now().UTC().Add(time.Duration(deadlineInt) * time.Hour * 24).Round(time.Second)
default:
panic("this code should never be reached")
}
if q, err = quorum.FromString(formQuorum); err != nil {
return err
}
if t, err = threshold.FromString(formThreshold); err != nil {
return err
}
e = strings.Split(formElectors, " ")
if formAnonymous == "on" {
a = true
}
if q, err = quorum.FromString(formQuorum); err != nil {
return err
}
if t, err = threshold.FromString(formThreshold); err != nil {
return err
}
e = strings.Split(formElectors, " ")
if formAnonymous == "on" {
a = true
}
store.NewVoting(id, r, d, q, t, e, a)
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", id))
store.NewVoting(id, r, d, q, t, e, a)
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", id))
}
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 {
var (
id = uuid.New().String()
vid = ctx.Param("id")
elector = ctx.Request().Header.Get("X-Remote-User")
choice = ctx.FormValue("vote")
v *voting.Voting
c vote.Choice
err error
)
v, err = store.GetVoting(vid)
if err != nil {
return err
}
if time.Now().UTC().After(v.Deadline()) {
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", vid))
}
if !eligible(elector, v.Electors()) {
return ctx.String(http.StatusForbidden, "")
}
if c, err = vote.ChoiceFromString(choice); err != nil {
return err
}
store.PlaceVote(id, vid, elector, c)
return ctx.Render(http.StatusFound, "thanks", map[string]interface{}{
"Id": id,
"Vid": vid,
})
var (
id = uuid.New().String()
vid = ctx.Param("id")
elector = ctx.Request().Header.Get("X-Remote-User")
choice = ctx.FormValue("vote")
v *voting.Voting
c vote.Choice
err error
)
v, err = store.GetVoting(vid)
if err != nil {
return err
}
if time.Now().UTC().After(v.Deadline()) {
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", vid))
}
if !eligible(elector, v.Electors()) {
return ctx.String(http.StatusForbidden, "")
}
if c, err = vote.ChoiceFromString(choice); err != nil {
return err
}
store.PlaceVote(id, vid, elector, c)
return ctx.Render(http.StatusFound, "thanks", map[string]interface{}{
"Id": id,
"Vid": vid,
})
}
func handleShowVoting(ctx echo.Context) error {
v, err := store.GetVoting(ctx.Param("id"))
if err != nil {
return err
}
if v.Deadline().After(time.Now().UTC()) {
if !eligible(ctx.Request().Header.Get("X-Remote-User"), v.Electors()) {
return ctx.String(http.StatusForbidden, "")
}
}
return ctx.Render(http.StatusOK, "voting", map[string]interface{}{
"Voting": v,
})
v, err := store.GetVoting(ctx.Param("id"))
if err != nil {
return err
}
if v.Deadline().After(time.Now().UTC()) {
if !eligible(ctx.Request().Header.Get("X-Remote-User"), v.Electors()) {
return ctx.String(http.StatusForbidden, "")
}
}
return ctx.Render(http.StatusOK, "voting", map[string]interface{}{
"Voting": v,
})
}
func eligible(e string, electors []string) bool {
if electors == nil || len(electors) == 0 {
return true
}
for _, _e := range electors {
if strings.ToLower(_e) == strings.ToLower(e) {
return true
}
}
return false
if electors == nil || len(electors) == 0 {
return true
}
for _, _e := range electors {
if strings.ToLower(_e) == strings.ToLower(e) {
return true
}
}
return false
}

View File

@ -8,24 +8,24 @@ import (
)
type Template struct {
Templates *template.Template
Templates *template.Template
}
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) {
tmpl := &template.Template{}
for i := range paths {
template.Must(tmpl.ParseGlob(paths[i]))
}
t := newTemplate(tmpl)
e.Renderer = t
tmpl := &template.Template{}
for i := range paths {
template.Must(tmpl.ParseGlob(paths[i]))
}
t := newTemplate(tmpl)
e.Renderer = t
}
func newTemplate(templates *template.Template) echo.Renderer {
return &Template{
Templates: templates,
}
return &Template{
Templates: templates,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,27 +6,27 @@ import (
)
type Vote struct {
Elector string
Choice Choice
timestamp time.Time
Elector string
Choice Choice
timestamp time.Time
}
func NewVote(elector string, choice Choice) Vote {
return Vote{
Elector: elector,
Choice: choice,
timestamp: time.Now().UTC(),
}
return Vote{
Elector: elector,
Choice: choice,
timestamp: time.Now().UTC(),
}
}
func NewVoteWithTimestamp(elector string, choice Choice, timestamp time.Time) Vote {
return Vote{
Elector: elector,
Choice: choice,
timestamp: timestamp,
}
return Vote{
Elector: elector,
Choice: choice,
timestamp: timestamp,
}
}
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)
}