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 ( import (
api "code.dlmw.ch/dlmw/qv/internal" api "code.dlmw.ch/dlmw/qv/internal"
"code.dlmw.ch/dlmw/qv/internal/validator" "code.dlmw.ch/dlmw/qv/internal/validator"
"encoding/json"
"math/rand"
"net/http" "net/http"
"time" "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(validator.GreaterThan(len(request.Choices), 1), "choices", "there must be more than 1 choice")
request.CheckField( request.CheckField(
!(request.AreVotersKnown && (request.MaxVoters == nil || *request.MaxVoters < 1)), !(request.AreVotersKnown && request.MaxVoters < 1),
"maxVoters", "maxVoters",
"must be greater than 0 when voters are known", "must be greater than 0 when voters are known",
) )
@ -36,13 +38,39 @@ func (app *application) createElection(w http.ResponseWriter, r *http.Request) {
return 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 { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
} }
//TODO: if voters are known, generate voters and write them in the response (change openapi as well) var res []byte
if request.AreVotersKnown {
w.Write([]byte("TODO")) 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)
}
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 { type application struct {
logger *slog.Logger logger *slog.Logger
elections models.ElectionModelInterface elections models.ElectionModelInterface
voters models.VoterModelInterface
} }
var addr = ":8080" var addr = ":8080"
@ -41,6 +42,7 @@ func main() {
app := &application{ app := &application{
logger: logger, logger: logger,
elections: &models.ElectionModel{DB: db}, elections: &models.ElectionModel{DB: db},
voters: &models.VoterModel{DB: db},
} }
logger.Info("Starting server", "addr", addr) logger.Info("Starting server", "addr", addr)

View File

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

View File

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

View File

@ -2,22 +2,21 @@ package models
import ( import (
"database/sql" "database/sql"
"fmt"
"time" "time"
) )
type ElectionModelInterface interface { 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 { type ElectionModel struct {
DB *sql.DB 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() tx, err := e.DB.Begin()
if err != nil { if err != nil {
return 0, fmt.Errorf("begin transaction: %w", err) return 0, err
} }
defer tx.Rollback() 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
}