Files
qv/cmd/web/handlers.go

371 lines
9.7 KiB
Go
Raw Normal View History

2024-12-27 18:03:46 +01:00
package main
import (
api "code.dlmw.ch/dlmw/qv/internal"
"code.dlmw.ch/dlmw/qv/internal/mappers"
"code.dlmw.ch/dlmw/qv/internal/models"
"code.dlmw.ch/dlmw/qv/internal/validator"
2025-01-13 10:56:36 +01:00
"code.dlmw.ch/dlmw/qv/ui"
"database/sql"
"encoding/json"
"errors"
"fmt"
2025-01-14 16:52:20 +01:00
"math"
"math/rand"
"net/http"
2025-01-08 13:57:49 +01:00
"slices"
"time"
)
func (app *application) indexPage(w http.ResponseWriter, r *http.Request) {
content, err := ui.Files.ReadFile("index.html")
if err != nil {
app.serverError(w, r, err)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
}
2025-01-13 10:56:36 +01:00
func (app *application) createElectionPage(w http.ResponseWriter, r *http.Request) {
content, err := ui.Files.ReadFile("create-election.html")
if err != nil {
2025-01-17 16:48:38 +01:00
app.serverError(w, r, err)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
}
2025-01-20 18:08:19 +01:00
func (app *application) getElectionResultsPage(w http.ResponseWriter, r *http.Request) {
content, err := ui.Files.ReadFile("election-results.html")
if err != nil {
app.serverError(w, r, err)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
}
2025-01-17 16:48:38 +01:00
func (app *application) getElectionPage(w http.ResponseWriter, r *http.Request) {
content, err := ui.Files.ReadFile("election.html")
if err != nil {
app.serverError(w, r, err)
2025-01-13 10:56:36 +01:00
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
}
type createElectionRequestWithValidator struct {
api.CreateElectionRequest
validator.Validator
}
2024-12-27 18:03:46 +01:00
2025-01-02 18:06:02 +01:00
func (r *createElectionRequestWithValidator) isValid() bool {
r.CheckField(validator.NotBlank(r.Name), "name", "must not be blank")
r.CheckField(validator.GreaterThan(r.Tokens, 0), "tokens", "must be greater than 0")
r.CheckField(validator.After(r.ExpiresAt, time.Now()), "expiresAt", "must expire in a future date")
r.CheckField(validator.GreaterThan(len(r.Choices), 1), "choices", "there must be more than 1 choice")
r.CheckField(validator.UniqueValues(r.Choices), "choices", "must not contain duplicate values")
r.CheckField(validator.LesserThan(r.MaxVoters, 101), "maxVoters", "cannot create a known-voters election with more than 100 voters")
2025-01-02 18:06:02 +01:00
for _, choice := range r.Choices {
r.CheckField(validator.NotBlank(choice), "choice", "must not be blank")
}
if r.AreVotersKnown {
r.CheckField(
validator.GreaterThan(r.MaxVoters, 0),
"maxVoters",
"must be greater than 0 when voters are known",
)
} else {
r.CheckField(
validator.GreaterThanOrEquals(r.MaxVoters, 0),
"maxVoters",
"must be a positive number",
)
}
return r.Valid()
}
2025-01-13 11:26:18 +01:00
func (app *application) CreateElection(w http.ResponseWriter, r *http.Request) {
var request createElectionRequestWithValidator
2024-12-30 23:13:39 +01:00
if err := app.unmarshalRequest(r, &request); err != nil {
app.clientError(w, http.StatusBadRequest, err.Error())
return
}
if !request.isValid() {
app.unprocessableEntityError(w, request.Validator)
return
}
2024-12-30 23:13:39 +01:00
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
}
var res []byte
if request.AreVotersKnown {
2025-01-13 20:07:18 +01:00
voterIdentities := make([]string, 0, request.MaxVoters)
for i := 0; i < request.MaxVoters; i++ {
2024-12-30 23:15:25 +01:00
randomIdentity := randomVoterIdentity()
voterIdentities = append(voterIdentities, randomIdentity)
}
2025-01-13 21:59:48 +01:00
_, err = app.voters.InsertMultiple(voterIdentities, electionId)
if err != nil {
app.serverError(w, r, err)
return
}
res, err = json.Marshal(api.CreateElectionResponse{VoterIdentities: &voterIdentities})
if err != nil {
app.serverError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
}
2024-12-31 15:33:46 +01:00
w.Header().Set("Location", fmt.Sprintf("/election/%v", electionId))
w.Write(res)
}
2024-12-30 23:15:25 +01:00
func randomVoterIdentity() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 16)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
2024-12-27 18:03:46 +01:00
}
type createVotesRequestWithValidator struct {
api.CreateVotesRequest
validator.Validator
}
func (r *createVotesRequestWithValidator) isValid() bool {
for _, choice := range r.Choices {
r.CheckField(validator.NotBlank(choice.ChoiceText), "choiceText", "must not be blank")
}
return r.Valid()
}
func (app *application) CreateVotes(w http.ResponseWriter, r *http.Request, id string) {
var request createVotesRequestWithValidator
if err := app.unmarshalRequest(r, &request); err != nil {
app.clientError(w, http.StatusBadRequest, err.Error())
return
}
if !request.isValid() {
app.unprocessableEntityError(w, request.Validator)
return
}
2025-01-13 11:26:18 +01:00
election, err := app.elections.GetById(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
app.clientError(w, http.StatusNotFound, "Couldn't find an election with the ID you provided")
return
}
app.serverError(w, r, err)
}
2025-01-08 13:57:49 +01:00
for _, c := range request.Choices {
choiceExists := slices.Contains(election.Choices, c.ChoiceText)
if !choiceExists {
app.clientError(w, http.StatusUnprocessableEntity, fmt.Sprintf("choice %v doesn't exist", c.ChoiceText))
2025-01-08 13:57:49 +01:00
return
}
}
tokensUsed := 0
for _, c := range request.Choices {
tokensUsed += c.Tokens
}
if tokensUsed > election.Tokens {
app.clientError(w, http.StatusUnprocessableEntity, fmt.Sprintf("you used too many tokens; must not exceed %v tokens", election.Tokens))
2025-01-08 13:57:49 +01:00
return
}
electionHasExpired := election.ExpiresAt.Before(time.Now())
if electionHasExpired {
app.clientError(w, http.StatusUnprocessableEntity, "election has expired")
2025-01-08 13:57:49 +01:00
return
}
var voterIdentity string
if election.AreVotersKnown {
voterIdentity, err = app.createVotesHandleKnownVotersElection(w, r, request, election)
if err != nil {
// everything will have been logged already
2025-01-08 13:57:49 +01:00
return
}
} else {
voterIdentity, err = app.createVotesHandleUnknownVotersElection(w, r, election)
if err != nil {
// everything will have been logged already
return
}
}
2025-01-08 13:57:49 +01:00
for _, c := range request.Choices {
_, err := app.votes.Insert(voterIdentity, election.ID, c.ChoiceText, c.Tokens)
if err != nil {
app.serverError(w, r, err)
2025-01-13 21:37:28 +01:00
return
2025-01-08 13:57:49 +01:00
}
}
2025-01-08 13:57:49 +01:00
w.WriteHeader(http.StatusCreated)
}
func (app *application) createVotesHandleKnownVotersElection(w http.ResponseWriter, r *http.Request, request createVotesRequestWithValidator, election *models.Election) (string, error) {
if request.VoterIdentity == nil || validator.Blank(*request.VoterIdentity) {
message := "election has known voters; you must provide an identity provided by the organizer"
app.clientError(w, http.StatusUnprocessableEntity, message)
return "", fmt.Errorf(message)
}
voterIdentity := *request.VoterIdentity
hasCastVotes, err := app.votes.Exists(voterIdentity, election.ID)
if err != nil {
app.serverError(w, r, err)
return "", err
}
if hasCastVotes {
message := "you already voted"
app.clientError(w, http.StatusUnprocessableEntity, message)
return "", fmt.Errorf(message)
}
voterExists, err := app.voters.Exists(voterIdentity, election.ID)
if err != nil {
app.serverError(w, r, err)
return "", err
}
if !voterExists {
message := "invalid voter identity"
app.clientError(w, http.StatusUnprocessableEntity, message)
return "", fmt.Errorf(message)
}
return voterIdentity, nil
}
func (app *application) createVotesHandleUnknownVotersElection(w http.ResponseWriter, r *http.Request, election *models.Election) (string, error) {
voterIdentity := r.RemoteAddr
voterExists, err := app.voters.Exists(voterIdentity, election.ID)
if err != nil {
app.serverError(w, r, err)
return "", err
}
if voterExists {
message := "you already voted"
app.clientError(w, http.StatusUnprocessableEntity, message)
return "", fmt.Errorf(message)
}
voterCount, err := app.voters.CountByElection(election.ID)
if err != nil && !errors.Is(sql.ErrNoRows, err) {
app.serverError(w, r, err)
return "", err
}
2025-01-17 16:48:38 +01:00
if voterCount != 0 && voterCount == election.MaxVoters {
message := "maximum voters reached"
app.clientError(w, http.StatusUnprocessableEntity, message)
return "", fmt.Errorf(message)
}
2025-01-17 16:48:38 +01:00
_, err = app.voters.InsertMultiple([]string{voterIdentity}, election.ID)
if err != nil {
app.serverError(w, r, err)
return "", err
}
return voterIdentity, nil
}
2025-01-13 11:26:18 +01:00
func (app *application) GetElectionResults(w http.ResponseWriter, r *http.Request, id string) {
2025-01-14 16:52:20 +01:00
votes, err := app.votes.GetByElection(id)
if err != nil {
if errors.Is(sql.ErrNoRows, err) {
app.clientError(w, http.StatusNotFound, fmt.Sprintf("couldn't find votes for an election with id %v", id))
return
}
app.serverError(w, r, err)
return
}
results := getResultsFromVotes(votes)
response, err := json.Marshal(api.ElectionResultsResponse{Results: &results})
if err != nil {
app.serverError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
2025-01-14 16:52:20 +01:00
w.Write(response)
}
func getResultsFromVotes(votes *[]models.Vote) []api.VotesForChoice {
votesForChoice := make(map[string]int)
for _, v := range *votes {
votesForChoice[v.ChoiceText] += int(math.Floor(math.Sqrt(float64(v.Tokens))))
}
results := make([]api.VotesForChoice, 0)
for choice, votes := range votesForChoice {
result := api.VotesForChoice{
Choice: choice,
Votes: votes,
}
results = append(results, result)
}
2025-01-13 11:26:18 +01:00
2025-01-14 16:52:20 +01:00
return results
2025-01-13 11:26:18 +01:00
}
func (app *application) GetElection(w http.ResponseWriter, r *http.Request, id string) {
election, err := app.elections.GetById(id)
if err != nil {
if errors.Is(sql.ErrNoRows, err) {
app.clientError(w, http.StatusNotFound, fmt.Sprintf("couldn't find election with id %v", id))
return
}
app.serverError(w, r, err)
return
}
response, err := json.Marshal(mappers.ElectionResponse(election))
if err != nil {
app.serverError(w, r, err)
return
}
2025-01-17 19:34:49 +01:00
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}