Generate voter data for known elections and simplify MaxVoters (0 = no maximum)
This commit is contained in:
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
36
internal/models/voters.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user