Generate voter data for known elections and simplify MaxVoters (0 = no maximum)
This commit is contained in:
@ -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 {
|
||||||
|
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 {
|
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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
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