Merge pull request 'set-vote' (#17) from set-vote into main

Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2025-11-13 00:46:42 +00:00
4 changed files with 401 additions and 12 deletions

View File

@@ -1,7 +1,7 @@
package models package models
type Vote struct { type Vote struct {
PollID int `json:"poll_id"` PollId int64 `json:"poll_id"`
OptionIndex int `json:"option_index"` Vote bool `json:"vote"`
IsMember bool `json:"is_member"` Email string `json:"email"`
} }

57
server/models/voters.go Normal file
View File

@@ -0,0 +1,57 @@
package models
import (
"go-sjles-pta-vote/server/db"
)
type Voter struct {
Email string `json:"email"`
IsMember bool `json:"is_member"`
YesVote bool `json:"yes_vote"`
}
func GetVoters(pollId int64) ([]Voter, error) {
db_conn, err := db.Connect()
if err != nil {
return nil, err
}
defer db.Close()
rows, err := db_conn.Query(`
SELECT v.voter_email,
CASE
WHEN m.email IS NOT NULL THEN 1
ELSE 0
END AS is_member,
CASE
WHEN p.member_yes_votes + p.non_member_yes_votes > p.member_no_votes + p.non_member_no_votes THEN 1
ELSE 0
END AS yes_vote
FROM voters v
LEFT JOIN members m ON v.voter_email = m.email
LEFT JOIN polls p ON v.poll_id = p.id
WHERE v.poll_id = $1
`, pollId)
if err != nil {
return nil, err
}
defer rows.Close()
var voters []Voter
for rows.Next() {
var voter Voter
var isMember int
var yesVote int
if err := rows.Scan(&voter.Email, &isMember, &yesVote); err != nil {
return nil, err
}
voter.IsMember = isMember == 1
voter.YesVote = yesVote == 1
voters = append(voters, voter)
}
if err := rows.Err(); err != nil {
return nil, err
}
return voters, nil
}

View File

@@ -12,6 +12,7 @@ import (
var ErrQuestionAlreadyExists = errors.New("Question already exists") var ErrQuestionAlreadyExists = errors.New("Question already exists")
var ErrQuestionDoesntExist = errors.New("Question does not exist yet") var ErrQuestionDoesntExist = errors.New("Question does not exist yet")
var ErrVoterAlreadyVoted = errors.New("Voter already voted") var ErrVoterAlreadyVoted = errors.New("Voter already voted")
var ErrPollNotFound = errors.New("Poll not found")
func CreatePoll(poll *models.Poll) (*models.Poll, error) { func CreatePoll(poll *models.Poll) (*models.Poll, error) {
new_poll := models.Poll{} new_poll := models.Poll{}
@@ -100,7 +101,7 @@ func GetPollByQuestion(question string) (*models.Poll, error) {
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, ErrQuestionDoesntExist return nil, ErrPollNotFound
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
@@ -132,7 +133,7 @@ func GetPollByQuestion(question string) (*models.Poll, error) {
func GetAndCreatePollByQuestion(question string) (*models.Poll, error) { func GetAndCreatePollByQuestion(question string) (*models.Poll, error) {
new_poll, err := GetPollByQuestion(question) new_poll, err := GetPollByQuestion(question)
if err == ErrQuestionDoesntExist { if err == ErrPollNotFound {
create_poll := &models.Poll{ create_poll := &models.Poll{
Question: question, Question: question,
ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"),
@@ -150,7 +151,8 @@ func GetAndCreatePollByQuestion(question string) (*models.Poll, error) {
} }
} }
func SetVote(poll_id int64, email string, vote bool) error { // Use models.Vote to set votes
func SetVote(vote *models.Vote) error {
db_conn, err := db.Connect() db_conn, err := db.Connect()
if err != nil { if err != nil {
return err return err
@@ -158,7 +160,7 @@ func SetVote(poll_id int64, email string, vote bool) error {
defer db.Close() defer db.Close()
set_voter_stmt, err := db_conn.Prepare(` set_voter_stmt, err := db_conn.Prepare(`
INSERT IGNORE INTO voters INSERT OR IGNORE INTO voters
(poll_id, voter_email) (poll_id, voter_email)
VALUES ($1, $2) VALUES ($1, $2)
`) `)
@@ -167,7 +169,7 @@ func SetVote(poll_id int64, email string, vote bool) error {
} }
defer set_voter_stmt.Close() defer set_voter_stmt.Close()
res, err := set_voter_stmt.Exec(poll_id, email) res, err := set_voter_stmt.Exec(vote.PollId, vote.Email)
if err != nil { if err != nil {
return err return err
} else { } else {
@@ -191,7 +193,7 @@ func SetVote(poll_id int64, email string, vote bool) error {
var member_check int64 var member_check int64
is_member := true is_member := true
err = is_voter_member_stmt.QueryRow(email).Scan(&member_check) err = is_voter_member_stmt.QueryRow(vote.Email).Scan(&member_check)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
is_member = false is_member = false
} else if err != nil { } else if err != nil {
@@ -205,7 +207,7 @@ func SetVote(poll_id int64, email string, vote bool) error {
member_column_name = "non_" + member_column_name member_column_name = "non_" + member_column_name
} }
if vote { if vote.Vote {
member_column_name += "yes_votes" member_column_name += "yes_votes"
} else { } else {
member_column_name += "no_votes" member_column_name += "no_votes"
@@ -213,7 +215,7 @@ func SetVote(poll_id int64, email string, vote bool) error {
add_vote_stmt, err := db_conn.Prepare(` add_vote_stmt, err := db_conn.Prepare(`
UPDATE polls UPDATE polls
SET ` + member_column_name + ` = ` + member_column_name + ` 1 SET ` + member_column_name + ` = ` + member_column_name + ` + 1
WHERE id == $1 WHERE id == $1
`) `)
if err != nil { if err != nil {
@@ -221,7 +223,7 @@ func SetVote(poll_id int64, email string, vote bool) error {
} }
defer add_vote_stmt.Close() defer add_vote_stmt.Close()
res, err = add_vote_stmt.Exec(poll_id) res, err = add_vote_stmt.Exec(vote.PollId)
if err != nil { if err != nil {
return err return err
} }
@@ -232,5 +234,54 @@ func SetVote(poll_id int64, email string, vote bool) error {
return err return err
} }
return nil
}
// Delete a poll by name
func DeletePollByQuestion(question string) error {
db_conn, err := db.Connect()
if err != nil {
return err
}
defer db.Close()
delete_votes_stmt, err := db_conn.Prepare(`
DELETE FROM voters
WHERE poll_id IN (
SELECT id
FROM polls
WHERE question == $1
)
`)
if err != nil {
return err
}
defer delete_votes_stmt.Close()
_, err = delete_votes_stmt.Exec(question)
if err != nil {
return err
}
delete_poll_stmt, err := db_conn.Prepare(`
DELETE FROM polls
WHERE question == $1
`)
if err != nil {
return err
}
defer delete_poll_stmt.Close()
res, err := delete_poll_stmt.Exec(question)
if err != nil {
return err
}
if num, err := res.RowsAffected(); num != 1 {
return errors.New("Failed to delete poll")
} else if err != nil {
return err
}
return nil return nil
} }

View File

@@ -13,6 +13,85 @@ import (
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=`~!@#$%^&*()_+[]\\;',./{}|:\"<>?" const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=`~!@#$%^&*()_+[]\\;',./{}|:\"<>?"
var new_members = []struct{
email string
member_name string
}{
{"test1@mail.me", "test1"},
{"test2@mail.me", "test2"},
{"test3@mail.me", "test3"},
{"test4@mail.me", "test4"},
{"test5@mail.me", "test5"},
{"test6@mail.me", "test6"},
{"test7@mail.me", "test7"},
{"test100@mail.me", "test100"},
{"test101@mail.me", "test101"},
{"test102@mail.me", "test102"},
{"test103@mail.me", "test103"},
{"test104@mail.me", "test104"},
{"test105@mail.me", "test105"},
}
var new_polls = []struct{
question string
member_yes_votes int64
member_no_votes int64
non_member_yes_votes int64
non_member_no_votes int64
}{
{"ques1", 1, 2, 3, 4},
{"ques2", 3, 2, 4, 5},
{"ques3", 4, 3, 6, 5},
}
var new_voters = []struct{
poll_id int64
voter_email string
}{
{1, "test1@mail.me"},
{1, "test2@mail.me"},
{1, "test3@mail.me"},
{1, "test10@mail.me"},
{1, "test11@mail.me"},
{1, "test12@mail.me"},
{1, "test13@mail.me"},
{1, "test14@mail.me"},
{1, "test15@mail.me"},
{1, "test16@mail.me"},
{2, "test1@mail.me"},
{2, "test2@mail.me"},
{2, "test3@mail.me"},
{2, "test4@mail.me"},
{2, "test5@mail.me"},
{2, "test10@mail.me"},
{2, "test11@mail.me"},
{2, "test12@mail.me"},
{2, "test13@mail.me"},
{2, "test14@mail.me"},
{2, "test15@mail.me"},
{2, "test16@mail.me"},
{2, "test17@mail.me"},
{2, "test18@mail.me"},
{3, "test1@mail.me"},
{3, "test2@mail.me"},
{3, "test3@mail.me"},
{3, "test4@mail.me"},
{3, "test5@mail.me"},
{3, "test6@mail.me"},
{3, "test7@mail.me"},
{3, "test10@mail.me"},
{3, "test11@mail.me"},
{3, "test12@mail.me"},
{3, "test13@mail.me"},
{3, "test14@mail.me"},
{3, "test15@mail.me"},
{3, "test16@mail.me"},
{3, "test17@mail.me"},
{3, "test18@mail.me"},
{3, "test19@mail.me"},
{3, "test20@mail.me"},
}
func RandString(length int) string { func RandString(length int) string {
rand_bytes := make([]byte, length) rand_bytes := make([]byte, length)
for rand_index := range length { for rand_index := range length {
@@ -21,6 +100,44 @@ func RandString(length int) string {
return string(rand_bytes) return string(rand_bytes)
} }
func PreLoadDB() error {
db_conn, err := db.Connect()
if err != nil {
return err
}
defer db.Close()
// Insert members
for i := range new_members {
_, err := db_conn.Exec(`INSERT INTO members (email, member_name) VALUES (?, ?)`, new_members[i].email, new_members[i].member_name)
if err != nil {
return err
}
}
// Insert polls
for i := range new_polls {
result, err := db_conn.Exec(`INSERT INTO polls (question, member_yes_votes, member_no_votes, non_member_yes_votes, non_member_no_votes, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, new_polls[i].question, new_polls[i].member_yes_votes, new_polls[i].member_no_votes, new_polls[i].non_member_yes_votes, new_polls[i].non_member_no_votes, time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"))
if err != nil {
return err
}
_, err = result.LastInsertId()
if err != nil {
return err
}
}
// Insert voters
for i := range new_voters {
_, err := db_conn.Exec(`INSERT INTO voters (poll_id, voter_email) VALUES (?, ?)`, new_voters[i].poll_id, new_voters[i].voter_email)
if err != nil {
return err
}
}
return nil
}
func TestCreatePoll(t *testing.T) { func TestCreatePoll(t *testing.T) {
parameters := []struct{ parameters := []struct{
question string question string
@@ -214,3 +331,167 @@ func TestGetCreatePollByQuestion(t *testing.T) {
} }
} }
func TestSetVote(t *testing.T) {
// Preload the database with members, polls, and voters
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
if err != nil {
t.Fatalf("Failed to create temporary database: %v", err)
}
defer os.Remove(tmp_db.Name())
init_conf := &config.Config{
DBPath: string(tmp_db.Name()),
}
config.SetConfig(init_conf)
err = PreLoadDB()
if err != nil {
t.Fatalf("Failed to preload database: %v", err)
}
// Add a non-member vote
random_email := RandString(10) + "@mail.me"
vote := &models.Vote{
PollId: 1,
Email: random_email,
Vote: true,
}
err = SetVote(vote)
if err != nil {
t.Fatalf("Failed to set non-member vote: %v", err)
}
// Add a member vote
member_email := "test100@mail.me"
vote = &models.Vote{
PollId: 1,
Email: member_email,
Vote: true,
}
err = SetVote(vote)
if err != nil {
t.Fatalf("Failed to set member vote: %v", err)
}
// Verify the votes were added correctly
voters, err := models.GetVoters(1) // Use GetVoters from models
if err != nil {
t.Fatalf("Failed to get voters: %v", err)
}
expected_non_member_votes := 4 + 1 // Original non-member votes + new non-member vote
expected_member_votes := 3 + 1 // Original member votes + new member vote
for _, voter := range voters {
if voter.Email == random_email && voter.YesVote {
expected_non_member_votes--
} else if voter.Email == member_email && voter.YesVote {
expected_member_votes--
}
}
if expected_non_member_votes != 5 || expected_member_votes != 4 {
t.Errorf("Expected %d non-member votes and %d member votes, but got %d non-member votes and %d member votes", 4+1, 3+1, expected_non_member_votes, expected_member_votes)
}
}
func TestVoterAlreadyVoted(t *testing.T) {
// Preload the database with members, polls, and voters
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
if err != nil {
t.Fatalf("Failed to create temporary database: %v", err)
}
defer os.Remove(tmp_db.Name())
init_conf := &config.Config{
DBPath: string(tmp_db.Name()),
}
config.SetConfig(init_conf)
err = PreLoadDB()
if err != nil {
t.Fatalf("Failed to preload database: %v", err)
}
// Add a non-member vote
random_email := RandString(10) + "@mail.me"
vote := &models.Vote{
PollId: 1,
Email: random_email,
Vote: true,
}
err = SetVote(vote)
if err != nil {
t.Fatalf("Failed to set non-member vote: %v", err)
}
// Add a member vote
member_email := "test100@mail.me"
vote = &models.Vote{
PollId: 1,
Email: member_email,
Vote: true,
}
err = SetVote(vote)
if err != nil {
t.Fatalf("Failed to set member vote: %v", err)
}
// Attempt to add another non-member vote
vote = &models.Vote{
PollId: 1,
Email: random_email,
Vote: true,
}
err = SetVote(vote)
if err != ErrVoterAlreadyVoted {
t.Fatalf("Expected ErrVoterAlreadyVoted, but got %v", err)
}
// Attempt to add another member vote
vote = &models.Vote{
PollId: 1,
Email: member_email,
Vote: true,
}
err = SetVote(vote)
if err != ErrVoterAlreadyVoted {
t.Fatalf("Expected ErrVoterAlreadyVoted, but got %v", err)
}
}
func TestDeletePollByQuestion(t *testing.T) {
// Preload the database with members, polls, and voters
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
if err != nil {
t.Fatalf("Failed to create temporary database: %v", err)
}
defer os.Remove(tmp_db.Name())
init_conf := &config.Config{
DBPath: string(tmp_db.Name()),
}
config.SetConfig(init_conf)
err = PreLoadDB()
if err != nil {
t.Fatalf("Failed to preload database: %v", err)
}
// Get a question from the new_polls array
testQuestion := new_polls[0].question
// Delete the poll by question
err = DeletePollByQuestion(testQuestion)
if err != nil {
t.Fatalf("Failed to delete poll by question: %v", err)
}
// Verify that the poll was deleted
_, err = GetPollByQuestion(testQuestion)
if err == nil {
t.Fatalf("Expected error when getting deleted poll, but got none")
} else if err != ErrPollNotFound {
t.Fatalf("Expected ErrPollNotFound, but got %v", err)
}
}