Generate voter data for known elections and simplify MaxVoters (0 = no maximum)

This commit is contained in:
2024-12-30 23:01:32 +01:00
parent 218f56c060
commit 195bc7d85e
6 changed files with 132 additions and 47 deletions

View File

@ -3,6 +3,8 @@ package main
import (
api "code.dlmw.ch/dlmw/qv/internal"
"code.dlmw.ch/dlmw/qv/internal/validator"
"encoding/json"
"math/rand"
"net/http"
"time"
)
@ -26,7 +28,7 @@ func (app *application) createElection(w http.ResponseWriter, r *http.Request) {
request.CheckField(validator.GreaterThan(len(request.Choices), 1), "choices", "there must be more than 1 choice")
request.CheckField(
!(request.AreVotersKnown && (request.MaxVoters == nil || *request.MaxVoters < 1)),
!(request.AreVotersKnown && request.MaxVoters < 1),
"maxVoters",
"must be greater than 0 when voters are known",
)
@ -36,13 +38,39 @@ func (app *application) createElection(w http.ResponseWriter, r *http.Request) {
return
}
_, err = app.elections.Insert(request.Name, request.Tokens, request.AreVotersKnown, request.MaxVoters, request.Choices, request.ExpiresAt)
electionId, err := app.elections.Insert(request.Name, request.Tokens, request.AreVotersKnown, request.MaxVoters, request.Choices, request.ExpiresAt)
if err != nil {
app.serverError(w, r, err)
return
}
//TODO: if voters are known, generate voters and write them in the response (change openapi as well)
var res []byte
if request.AreVotersKnown {
voterIdentities := make([]string, 0, request.MaxVoters)
for i := 0; i < request.MaxVoters; i++ {
randomIdentity := generateRandomVoterIdentity()
_, err := app.voters.Insert(randomIdentity, electionId)
if err != nil {
app.serverError(w, r, err)
}
voterIdentities = append(voterIdentities, randomIdentity)
}
w.Write([]byte("TODO"))
res, err = json.Marshal(api.CreateElectionResponse{VoterIdentities: &voterIdentities})
if err != nil {
app.serverError(w, r, err)
return
}
}
w.Write(res)
}
func generateRandomVoterIdentity() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 16)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}

View File

@ -19,6 +19,7 @@ import (
type application struct {
logger *slog.Logger
elections models.ElectionModelInterface
voters models.VoterModelInterface
}
var addr = ":8080"
@ -41,6 +42,7 @@ func main() {
app := &application{
logger: logger,
elections: &models.ElectionModel{DB: db},
voters: &models.VoterModel{DB: db},
}
logger.Info("Starting server", "addr", addr)

View File

@ -32,8 +32,12 @@ paths:
schema:
$ref: "#/components/schemas/CreateElectionRequest"
responses:
201:
description: Election created
200:
description: Election created. Body only returned if voterAreKnown is true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateElectionResponse"
422:
description: Unprocessable Content
content:
@ -49,8 +53,9 @@ components:
- id
- name
- tokens
- are_voters_known
- expires_at
- areVotersKnown
- maxVoters
- expiresAt
properties:
id:
type: integer
@ -60,20 +65,19 @@ components:
type: string
minLength: 1
tokens:
type: integer
minimum: 0
are_voters_known:
type: boolean
max_voters:
type: integer
minimum: 1
nullable: true
description: Required when voters are known
created_at:
areVotersKnown:
type: boolean
maxVoters:
type: integer
minimum: 0
description: Must be greater than 0 when voters are known
createdAt:
type: string
format: date-time
readOnly: true
expires_at:
expiresAt:
type: string
format: date-time
choices:
@ -89,12 +93,12 @@ components:
type: object
required:
- text
- election_id
- electionId
properties:
text:
type: string
minLength: 1
election_id:
electionId:
type: integer
format: int64
@ -102,13 +106,13 @@ components:
type: object
required:
- identity
- election_id
- electionId
properties:
identity:
type: string
minLength: 1
description: When voters are known, passcodes will be pre-generated
election_id:
electionId:
type: integer
format: int64
votes:
@ -119,27 +123,27 @@ components:
Vote:
type: object
required:
- voter_identity
- election_id
- voterIdentity
- electionId
properties:
voter_identity:
voterIdentity:
type: string
minLength: 1
election_id:
electionId:
type: integer
format: int64
choice_text:
choiceText:
type: string
nullable: true
tokens:
type: integer
minimum: 0
minimum: 1
nullable: true
calculated_vote_count:
calculatedVoteCount:
type: integer
readOnly: true
description: Calculated as floor(sqrt(tokens))
created_at:
createdAt:
type: string
format: date-time
readOnly: true
@ -149,24 +153,24 @@ components:
required:
- name
- tokens
- are_voters_known
- expires_at
- areVotersKnown
- maxVoters
- expiresAt
- choices
properties:
name:
type: string
minLength: 1
tokens:
type: integer
minimum: 0
are_voters_known:
type: boolean
max_voters:
type: integer
minimum: 1
nullable: true
description: Required when voters are known
expires_at:
areVotersKnown:
type: boolean
maxVoters:
type: integer
minimum: 0
description: Must be greater than 0 when voters are known; 0 = no limit
expiresAt:
type: string
format: date-time
choices:
@ -177,6 +181,17 @@ components:
minItems: 1
uniqueItems: true
CreateElectionResponse:
type: object
properties:
voterIdentities:
type: array
items:
type: string
minLength: 1
maxItems: 1
uniqueItems: true
ErrorResponse:
type: object
required:

View File

@ -13,16 +13,21 @@ import (
// CreateElectionRequest defines model for CreateElectionRequest.
type CreateElectionRequest struct {
AreVotersKnown bool `json:"are_voters_known"`
AreVotersKnown bool `json:"areVotersKnown"`
Choices []string `json:"choices"`
ExpiresAt time.Time `json:"expires_at"`
ExpiresAt time.Time `json:"expiresAt"`
// MaxVoters Required when voters are known
MaxVoters *int `json:"max_voters"`
// MaxVoters Must be greater than 0 when voters are known; 0 = no limit
MaxVoters int `json:"maxVoters"`
Name string `json:"name"`
Tokens int `json:"tokens"`
}
// CreateElectionResponse defines model for CreateElectionResponse.
type CreateElectionResponse struct {
VoterIdentities *[]string `json:"voterIdentities,omitempty"`
}
// ErrorResponse defines model for ErrorResponse.
type ErrorResponse struct {
// Code Machine-readable error code

View File

@ -2,22 +2,21 @@ package models
import (
"database/sql"
"fmt"
"time"
)
type ElectionModelInterface interface {
Insert(name string, tokens int, areVotersKnown bool, maxVoters *int, Choices []string, ExpiresAt time.Time) (int, error)
Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (int, error)
}
type ElectionModel struct {
DB *sql.DB
}
func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters *int, choices []string, expiresAt time.Time) (int, error) {
func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (int, error) {
tx, err := e.DB.Begin()
if err != nil {
return 0, fmt.Errorf("begin transaction: %w", err)
return 0, err
}
defer tx.Rollback()

36
internal/models/voters.go Normal file
View File

@ -0,0 +1,36 @@
package models
import (
"database/sql"
)
type VoterModelInterface interface {
Insert(identity string, electionID int) (int, error)
}
type VoterModel struct {
DB *sql.DB
}
func (v *VoterModel) Insert(identity string, electionID int) (int, error) {
tx, err := v.DB.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
result, err := tx.Exec(`
INSERT INTO voters (identity, election_id)
VALUES (?, ?)`,
identity, electionID)
if err != nil {
return 0, err
}
if err = tx.Commit(); err != nil {
return 0, err
}
voterId, err := result.LastInsertId()
return int(voterId), nil
}