Dirty implementation of createVotes
This commit is contained in:
@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,22 +141,95 @@ func (app *application) createVotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
app.serverError(w, r, err)
|
app.serverError(w, r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check if he has voted
|
for _, c := range request.Choices {
|
||||||
|
choiceExists := slices.Contains(election.Choices, c.ChoiceText)
|
||||||
|
if !choiceExists {
|
||||||
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("choice %v doesn't exist", c.ChoiceText))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//var voterIdentity string
|
tokensUsed := 0
|
||||||
|
for _, c := range request.Choices {
|
||||||
|
tokensUsed += c.Tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokensUsed > election.Tokens {
|
||||||
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("you used too many tokens; must not exceed %v tokens", election.Tokens))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
electionHasExpired := election.ExpiresAt.Before(time.Now())
|
||||||
|
if electionHasExpired {
|
||||||
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("election has expired"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// this snippet of code also inserts in the `voters` table
|
||||||
|
voterIdentity := func() string {
|
||||||
|
var voterIdentity string
|
||||||
if election.AreVotersKnown {
|
if election.AreVotersKnown {
|
||||||
if request.VoterIdentity == nil || validator.Blank(*request.VoterIdentity) {
|
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"))
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("election has known voters; you must provide an identity provided by the organizer"))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
voterIdentity = *request.VoterIdentity
|
||||||
|
hasCastVotes, err := app.votes.Exists(voterIdentity, election.ID)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, r, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if hasCastVotes {
|
||||||
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("you already voted"))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
voterIdentity = r.RemoteAddr
|
||||||
|
|
||||||
|
// if voters are known, voter will always exist
|
||||||
|
voterExists, err := app.voters.Exists(voterIdentity, election.ID)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, r, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if voterExists {
|
||||||
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("you already voted"))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = app.voters.Insert(voterIdentity, election.ID)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, r, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return voterIdentity
|
||||||
|
}()
|
||||||
|
|
||||||
|
if voterIdentity == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !election.AreVotersKnown {
|
||||||
|
voterCount, err := app.voters.CountByElection(election.ID)
|
||||||
|
if err != nil && !errors.Is(sql.ErrNoRows, err) {
|
||||||
|
app.serverError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if voters are known, voterCount == election.MaxVoters in all cases
|
||||||
|
if voterCount == election.MaxVoters {
|
||||||
|
app.unprocessableEntityErrorSingle(w, fmt.Errorf("maximum voters reached"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//voterIdentity = *request.VoterIdentity
|
|
||||||
} else {
|
|
||||||
// TODO: get requester's IP address as identity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO verify if choice exists
|
for _, c := range request.Choices {
|
||||||
// TODO count tokens to make sure user isn't trying to cheat
|
_, err := app.votes.Insert(voterIdentity, election.ID, c.ChoiceText, c.Tokens)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
json, _ := json.Marshal(election)
|
w.WriteHeader(http.StatusCreated)
|
||||||
w.Write(json)
|
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ func main() {
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
elections: &models.ElectionModel{DB: db},
|
elections: &models.ElectionModel{DB: db},
|
||||||
voters: &models.VoterModel{DB: db},
|
voters: &models.VoterModel{DB: db},
|
||||||
|
votes: &models.VoteModel{DB: db},
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Starting server", "addr", addr)
|
logger.Info("Starting server", "addr", addr)
|
||||||
|
@ -11,6 +11,7 @@ type application struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
elections models.ElectionModelInterface
|
elections models.ElectionModelInterface
|
||||||
voters models.VoterModelInterface
|
voters models.VoterModelInterface
|
||||||
|
votes models.VoteModelInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) routes() http.Handler {
|
func (app *application) routes() http.Handler {
|
||||||
|
@ -15,6 +15,7 @@ func newTestApplication(t *testing.T) *application {
|
|||||||
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||||
elections: &mockElectionModel{},
|
elections: &mockElectionModel{},
|
||||||
voters: &mockVoterModel{},
|
voters: &mockVoterModel{},
|
||||||
|
votes: &mockVoteModel{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,8 +57,8 @@ func (e *mockElectionModel) GetById(id int) (*models.Election, error) {
|
|||||||
Tokens: 100,
|
Tokens: 100,
|
||||||
AreVotersKnown: false,
|
AreVotersKnown: false,
|
||||||
MaxVoters: 10,
|
MaxVoters: 10,
|
||||||
CreatedAt: time.Now().String(),
|
CreatedAt: time.Now(),
|
||||||
ExpiresAt: time.Now().Add(100 * time.Hour).String(),
|
ExpiresAt: time.Now().Add(100 * time.Hour),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,3 +68,22 @@ type mockVoterModel struct {
|
|||||||
func (v *mockVoterModel) Insert(identity string, electionID int) (int, error) {
|
func (v *mockVoterModel) Insert(identity string, electionID int) (int, error) {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *mockVoterModel) CountByElection(electionID int) (int, error) {
|
||||||
|
return 10, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockVoterModel) Exists(voterIdentity string, electionID int) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockVoteModel struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockVoteModel) Insert(voterIdentity string, electionId int, choiceText string, tokens int) (int, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockVoteModel) Exists(voterIdentity string, electionID int) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
@ -20,8 +20,8 @@ type Election struct {
|
|||||||
Tokens int
|
Tokens int
|
||||||
AreVotersKnown bool
|
AreVotersKnown bool
|
||||||
MaxVoters int
|
MaxVoters int
|
||||||
CreatedAt string
|
CreatedAt time.Time
|
||||||
ExpiresAt string
|
ExpiresAt time.Time
|
||||||
Choices []string
|
Choices []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,13 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VoterModelInterface interface {
|
type VoterModelInterface interface {
|
||||||
Insert(identity string, electionID int) (int, error)
|
Insert(identity string, electionID int) (int, error)
|
||||||
|
CountByElection(electionID int) (int, error)
|
||||||
|
Exists(voterIdentity string, electionID int) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VoterModel struct {
|
type VoterModel struct {
|
||||||
@ -34,3 +37,53 @@ func (v *VoterModel) Insert(identity string, electionID int) (int, error) {
|
|||||||
voterId, err := result.LastInsertId()
|
voterId, err := result.LastInsertId()
|
||||||
return int(voterId), nil
|
return int(voterId), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *VoterModel) CountByElection(electionID int) (int, error) {
|
||||||
|
// use a transaction to prevent race conditions
|
||||||
|
tx, err := v.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT COUNT(identity)
|
||||||
|
FROM voters
|
||||||
|
WHERE election_id = ?
|
||||||
|
GROUP BY election_id;
|
||||||
|
`
|
||||||
|
|
||||||
|
row := tx.QueryRow(query, electionID)
|
||||||
|
|
||||||
|
var voterCount int
|
||||||
|
|
||||||
|
err = row.Scan(&voterCount)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
return voterCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoterModel) Exists(voterIdentity string, electionID int) (bool, error) {
|
||||||
|
query := `
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM voters
|
||||||
|
WHERE identity = ? AND election_id = ?
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
|
||||||
|
err := v.DB.QueryRow(query, voterIdentity, electionID).Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(sql.ErrNoRows, err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return exists, nil
|
||||||
|
}
|
||||||
|
57
internal/models/votes.go
Normal file
57
internal/models/votes.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoteModelInterface interface {
|
||||||
|
Insert(voterIdentity string, electionId int, choiceText string, tokens int) (int, error)
|
||||||
|
Exists(voterIdentity string, electionID int) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoteModel) Insert(voterIdentity string, electionId int, choiceText string, tokens int) (int, error) {
|
||||||
|
tx, err := v.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
result, err := tx.Exec(`
|
||||||
|
INSERT INTO votes (voter_identity, election_id, choice_text, tokens)
|
||||||
|
VALUES (?, ?, ?, ?)`,
|
||||||
|
voterIdentity, electionId, choiceText, tokens)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
voteId, err := result.LastInsertId()
|
||||||
|
return int(voteId), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoteModel) Exists(voterIdentity string, electionID int) (bool, error) {
|
||||||
|
var exists bool
|
||||||
|
query := `
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM votes
|
||||||
|
WHERE voter_identity = ? AND election_id = ?
|
||||||
|
)
|
||||||
|
`
|
||||||
|
err := v.DB.QueryRow(query, voterIdentity, electionID).Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(sql.ErrNoRows, err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return exists, nil
|
||||||
|
}
|
Reference in New Issue
Block a user