Incomplete implementation of createVotes

This commit is contained in:
2025-01-02 19:24:32 +01:00
parent 9efe9a3537
commit 0229f78976
9 changed files with 136 additions and 13 deletions

View File

@ -3,7 +3,9 @@ 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"
"database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math/rand" "math/rand"
"net/http" "net/http"
@ -107,17 +109,16 @@ type createVotesRequestWithValidator struct {
} }
func (r *createVotesRequestWithValidator) isValid() bool { func (r *createVotesRequestWithValidator) isValid() bool {
r.CheckField(validator.NotBlank(*r.VoterIdentity), "voterIdentity", "must not be blank") r.CheckField(validator.GreaterThan(r.ElectionId, 0), "electionId", "must be greater than 0")
r.CheckField(validator.GreaterThan(*r.ElectionId, 0), "electionId", "must be greater than 0")
for _, choice := range *r.Choices { for _, choice := range r.Choices {
r.CheckField(validator.NotBlank(*choice.ChoiceText), "choiceText", "must not be blank") r.CheckField(validator.NotBlank(choice.ChoiceText), "choiceText", "must not be blank")
} }
return r.Valid() return r.Valid()
} }
func (app *application) createVote(w http.ResponseWriter, r *http.Request) { func (app *application) createVotes(w http.ResponseWriter, r *http.Request) {
var request createVotesRequestWithValidator var request createVotesRequestWithValidator
if err := app.unmarshalRequest(r, &request); err != nil { if err := app.unmarshalRequest(r, &request); err != nil {
@ -129,4 +130,32 @@ func (app *application) createVote(w http.ResponseWriter, r *http.Request) {
app.unprocessableEntityError(w, request.Validator) app.unprocessableEntityError(w, request.Validator)
return return
} }
election, err := app.elections.GetById(request.ElectionId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
app.unprocessableEntityErrorSingle(w, fmt.Errorf("election with id %v doesn't exist", request.ElectionId))
return
}
app.serverError(w, r, err)
}
// TODO check if he has voted
//var voterIdentity string
if election.AreVotersKnown {
if request.VoterIdentity == nil || validator.Blank(*request.VoterIdentity) {
app.unprocessableEntityErrorSingle(w, fmt.Errorf("election has known voters; you must provide an identity provided by the organizer"))
return
}
//voterIdentity = *request.VoterIdentity
} else {
// TODO: get requester's IP address as identity
}
// TODO verify if choice exists
// TODO count tokens to make sure user isn't trying to cheat
json, _ := json.Marshal(election)
w.Write(json)
} }

View File

@ -43,6 +43,15 @@ func (app *application) unprocessableEntityError(w http.ResponseWriter, v valida
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
func (app *application) unprocessableEntityErrorSingle(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusUnprocessableEntity)
var response = api.ErrorResponse{
Code: http.StatusUnprocessableEntity,
Message: err.Error(),
}
json.NewEncoder(w).Encode(response)
}
func (app *application) unmarshalRequest(r *http.Request, dst any) error { func (app *application) unmarshalRequest(r *http.Request, dst any) error {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {

View File

@ -221,15 +221,21 @@ components:
CreateVotesRequest: CreateVotesRequest:
type: object type: object
required:
- electionId
- choices
properties: properties:
voterIdentity: voterIdentity:
type: string type: string
minLength: 1 description: Must be filled if election has known voters
electionId: electionId:
type: integer type: integer
choices: choices:
type: array type: array
items: items:
required:
- choiceText
- tokens
properties: properties:
choiceText: choiceText:
type: string type: string

View File

@ -17,7 +17,7 @@ func (app *application) routes() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("POST /election", app.createElection) mux.HandleFunc("POST /election", app.createElection)
mux.HandleFunc("POST /vote", app.createVote) mux.HandleFunc("POST /votes", app.createVotes)
standard := alice.New(app.recoverPanic, app.logRequest) standard := alice.New(app.recoverPanic, app.logRequest)
return standard.Then(mux) return standard.Then(mux)

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"code.dlmw.ch/dlmw/qv/internal/models"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
@ -48,6 +49,18 @@ func (e *mockElectionModel) Insert(name string, tokens int, areVotersKnown bool,
return 1, nil return 1, nil
} }
func (e *mockElectionModel) GetById(id int) (*models.Election, error) {
return &models.Election{
ID: id,
Name: "Guy of the year",
Tokens: 100,
AreVotersKnown: false,
MaxVoters: 10,
CreatedAt: time.Now().String(),
ExpiresAt: time.Now().Add(100 * time.Hour).String(),
}, nil
}
type mockVoterModel struct { type mockVoterModel struct {
} }

View File

@ -30,11 +30,13 @@ type CreateElectionResponse struct {
// CreateVotesRequest defines model for CreateVotesRequest. // CreateVotesRequest defines model for CreateVotesRequest.
type CreateVotesRequest struct { type CreateVotesRequest struct {
Choices *[]struct { Choices []struct {
ChoiceText *string `json:"choiceText,omitempty"` ChoiceText string `json:"choiceText"`
Tokens *int `json:"tokens,omitempty"` Tokens int `json:"tokens"`
} `json:"choices,omitempty"` } `json:"choices"`
ElectionId *int `json:"electionId,omitempty"` ElectionId int `json:"electionId"`
// VoterIdentity Must be filled if election has known voters
VoterIdentity *string `json:"voterIdentity,omitempty"` VoterIdentity *string `json:"voterIdentity,omitempty"`
} }

View File

@ -3,7 +3,7 @@ CREATE TABLE elections (
name TEXT NOT NULL, name TEXT NOT NULL,
tokens INTEGER NOT NULL, tokens INTEGER NOT NULL,
are_voters_known INTEGER NOT NULL, are_voters_known INTEGER NOT NULL,
max_voters INTEGER, -- mandatory when voters are known max_voters INTEGER NOT NULL, -- must be greater than 0 when voters are known
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL, expires_at DATETIME NOT NULL,
CHECK (are_voters_known = 0 OR (are_voters_known = 1 AND max_voters IS NOT NULL AND max_voters >= 1)) CHECK (are_voters_known = 0 OR (are_voters_known = 1 AND max_voters IS NOT NULL AND max_voters >= 1))

View File

@ -2,17 +2,30 @@ 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)
GetById(id int) (*Election, error)
} }
type ElectionModel struct { type ElectionModel struct {
DB *sql.DB DB *sql.DB
} }
type Election struct {
ID int
Name string
Tokens int
AreVotersKnown bool
MaxVoters int
CreatedAt string
ExpiresAt string
Choices []string
}
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 {
@ -53,3 +66,50 @@ func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, max
return int(electionID), nil return int(electionID), nil
} }
func (e *ElectionModel) GetById(id int) (*Election, error) {
query := `
SELECT id, name, tokens, are_voters_known, max_voters, created_at, expires_at
FROM elections
WHERE id = ?
`
row := e.DB.QueryRow(query, id)
election := &Election{}
var areVotersKnown int
err := row.Scan(&election.ID, &election.Name, &election.Tokens, &areVotersKnown, &election.MaxVoters, &election.CreatedAt, &election.ExpiresAt)
if err != nil {
return nil, err
}
election.AreVotersKnown = areVotersKnown == 1
// Retrieve choices for the election
queryChoices := `
SELECT text
FROM choices
WHERE election_id = ?
`
rows, err := e.DB.Query(queryChoices, id)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var choice string
if err := rows.Scan(&choice); err != nil {
return nil, err
}
election.Choices = append(election.Choices, choice)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating over choices for election ID %d: %v", id, err)
}
return election, nil
}

View File

@ -36,6 +36,10 @@ func (v *Validator) CheckField(ok bool, key, message string) {
} }
} }
func Blank(value string) bool {
return strings.TrimSpace(value) == ""
}
func NotBlank(value string) bool { func NotBlank(value string) bool {
return strings.TrimSpace(value) != "" return strings.TrimSpace(value) != ""
} }