Adding not just the api but also some initial code for posting members

This commit is contained in:
2026-01-20 15:56:21 -05:00
parent 0359efe197
commit a694a73249
15 changed files with 219 additions and 57 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
db_path="./pta_vote.db"

3
.gitignore vendored
View File

@@ -22,6 +22,3 @@
go.work
go.work.sum
# env file
.env

View File

@@ -1,8 +1,8 @@
package config
import (
"encoding/json"
"fmt"
"log"
"os"
"strings"
)
@@ -17,6 +17,8 @@ var conf *Config
var conf_path string = ".env"
func GetConfig() *Config {
_ = GenerateEnvFileIfNotExists("./sjles-pta-vote.db")
if conf != nil {
return conf
}
@@ -25,9 +27,8 @@ func GetConfig() *Config {
// TODO: Make this into a ini or toml file
configContent, err := os.ReadFile(conf_path)
if err != nil {
fmt.Println("Error reading .env file: ", err)
log.Printf("Error reading .env file: %v", err)
os.Exit(1)
}
@@ -42,11 +43,6 @@ func GetConfig() *Config {
}
}
if err := json.Unmarshal([]byte(configContent), conf); err != nil {
fmt.Println("Error unmarshalling config file: ", err)
os.Exit(1)
}
// TODO: Better mapping of key to json values
// TODO: Better error checking if values are missing
// TODO: Default values
@@ -58,7 +54,7 @@ func GetConfig() *Config {
} else if strings.Contains(key, "redis_password") {
conf.RedisPassword = value
} else {
fmt.Println("Error, Unknown key value pair: ", key, " = ", value)
log.Printf("Error, Unknown key value pair: %s = %s", key, value)
}
}
@@ -68,3 +64,12 @@ func GetConfig() *Config {
func SetConfig(init_conf *Config) {
conf = init_conf
}
func GenerateEnvFileIfNotExists(dbPath string) error {
_, err := os.Stat(".env")
if err == nil {
return nil
}
envContent := fmt.Sprintf("db_path=\"%s\"\n", dbPath)
return os.WriteFile(".env", []byte(envContent), 0644)
}

View File

@@ -2,6 +2,10 @@ package db
import (
"database/sql"
"errors"
"log"
"os"
"strings"
"go-sjles-pta-vote/server/config"
@@ -32,28 +36,39 @@ CREATE TABLE IF NOT EXISTS members (
email TEXT NOT NULL,
member_name TEXT,
school_year UNSIGNED INT NOT NULL,
PRIMARY KEY (email)
PRIMARY KEY (email, school_year)
);
`
var db *sql.DB
func Connect() (*sql.DB, error) {
log.Printf("Connecting to database")
db_config := config.GetConfig()
log.Printf("Database path: %s", db_config.DBPath)
db, err := sql.Open("sqlite", db_config.DBPath)
if err != nil {
log.Printf("Error opening database: %v", err)
return nil, err
}
_, err = db.Exec(build_db_query)
if err != nil {
log.Printf("Error updating schema: %v", err)
_ = db.Close()
return nil, err
}
return db, err
return db, nil
}
func Close() {
if db != nil {
_ = db.Close()
err := db.Close()
if err != nil {
log.Printf("Error closing database: %v", err)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
server/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -5,13 +5,16 @@ import (
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"github.com/gorilla/mux"
"go-sjles-pta-vote/server/models"
"go-sjles-pta-vote/server/services"
)
func voteHandler(w http.ResponseWriter, r *http.Request) {
var vote services.Vote
var vote models.Vote
if err := json.NewDecoder(r.Body).Decode(&vote); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
@@ -28,15 +31,20 @@ func voteHandler(w http.ResponseWriter, r *http.Request) {
func voteIDHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
idStr := vars["id"]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid poll ID", http.StatusBadRequest)
return
}
vote := services.Vote{
vote := models.Vote{
PollId: id,
Email: "example@example.com", // Replace with actual email retrieval logic
Vote: true, // Replace with actual vote retrieval logic
}
err := services.SetVote(&vote)
err = services.SetVote(&vote)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -46,6 +54,11 @@ func voteIDHandler(w http.ResponseWriter, r *http.Request) {
}
func statsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
filePath := "./server/templates/stats.html"
log.Printf("Serving stats.html from %s", filePath)
http.ServeFile(w, r, filePath)
} else if r.Method == "POST" {
vars := mux.Vars(r)
id := vars["id"]
@@ -56,6 +69,9 @@ func statsHandler(w http.ResponseWriter, r *http.Request) {
}
json.NewEncoder(w).Encode(poll)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func statsIDHandler(w http.ResponseWriter, r *http.Request) {
@@ -77,16 +93,31 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
}
func adminIDHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
//vars := mux.Vars(r)
//id := vars["id"]
// Add admin functionality here
w.WriteHeader(http.StatusOK)
}
func membersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
file, handler, err := r.FormFile("members.csv")
func adminMembersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
filePath := "./server/templates/members.html"
log.Printf("Serving members.html from %s", filePath)
http.ServeFile(w, r, filePath)
} else if r.Method == "POST" {
var year int
var err error
r.ParseForm()
if y := r.FormValue("year"); y != "" {
year, err = strconv.Atoi(y)
if err != nil {
http.Error(w, "Invalid year", http.StatusBadRequest)
return
}
}
file, _, err := r.FormFile("members.csv")
if err != nil {
http.Error(w, "Failed to upload file", http.StatusBadRequest)
return
@@ -99,28 +130,37 @@ func membersHandler(w http.ResponseWriter, r *http.Request) {
return
}
members, err := services.ParseMembersFromBytes(2023, fileBytes) // Assuming year 2023 for demonstration purposes
err = services.ParseMembersFromBytes(year, fileBytes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(members)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func main() {
r := mux.NewRouter()
log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile)
r.HandleFunc("/vote", voteHandler).Methods("POST")
r.HandleFunc("/vote/{id}", voteIDHandler).Methods("POST")
r.HandleFunc("/stats", statsHandler).Methods("POST")
r.HandleFunc("/stats/{id}", statsIDHandler).Methods("POST")
r.HandleFunc("/admin", adminHandler).Methods("GET")
r.HandleFunc("/admin/{id}", adminIDHandler).Methods("GET")
r.HandleFunc("/admin/members", membersHandler).Methods("POST")
log.Printf("Starting server on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
http.HandleFunc("/vote", voteHandler)
http.HandleFunc("/vote/{id}", voteIDHandler)
http.HandleFunc("/stats", statsHandler)
http.HandleFunc("/stats/{id}", statsIDHandler)
http.HandleFunc("/admin", adminHandler)
http.HandleFunc("/admin/{id}", adminIDHandler)
http.HandleFunc("/admin/members", adminMembersHandler)
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
filePath := "./server/icons/favicon.ico"
http.ServeFile(w, r, filePath)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}

View File

@@ -3,9 +3,12 @@ package services
import (
"encoding/csv"
"fmt"
"log"
"strings"
"github.com/pkg/errors"
"go-sjles-pta-vote/server/db"
)
type Member struct {
@@ -13,14 +16,18 @@ type Member struct {
Email string
}
func ParseMembersFromBytes(year int, fileBytes []byte) ([]Member, error) {
BATCH_SIZE := 100
func ParseMembersFromBytes(year int, fileBytes []byte) error {
reader := csv.NewReader(strings.NewReader(string(fileBytes)))
reader.FieldsPerRecord = -1 // Allow variable number of fields per record
records, err := reader.ReadAll()
if err != nil {
return nil, errors.Wrap(err, "failed to read CSV from bytes")
return errors.Wrap(err, "failed to read CSV from bytes")
}
var members []Member
for i, record := range records {
if i == 0 {
continue // Skip the first line (column headers)
@@ -28,18 +35,86 @@ func ParseMembersFromBytes(year int, fileBytes []byte) ([]Member, error) {
if len(record) < 4 {
continue
}
firstName := record[1]
lastName := record[2]
email := record[3]
firstName := strings.TrimSpace(record[1])
lastName := strings.TrimSpace(record[2])
email := strings.TrimSpace(record[3])
members = append(members, Member{
Name: fmt.Sprintf("%s %s", firstName, lastName),
Email: email,
})
if len(record) < 30 {
continue
}
if len(members) == 0 {
members = []Member{}
email2 := strings.TrimSpace(record[27])
if email2 != "" {
firstName2 := strings.TrimSpace(record[29])
lastName2 := strings.TrimSpace(record[28])
members = append(members, Member{
Name: fmt.Sprintf("%s %s", firstName2, lastName2),
Email: email2,
})
}
}
return members, nil
return saveMember(year, members)
}
func saveMember(year int, members []Member) error {
insertMembersQuery := `
INSERT OR REPLACE INTO members (email, member_name, school_year)
VALUES ($1, $2, $3)
`
log.Printf("Starting to save %d members for year %d", len(members), year)
db_conn, err := db.Connect()
if err != nil {
return errors.Wrap(err, "failed to connect to database")
}
defer db_conn.Close()
tx, err := db_conn.Begin()
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
stmt, err := tx.Prepare(insertMembersQuery)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to prepare statement")
}
defer stmt.Close()
for index, member := range members {
_, err = stmt.Exec(member.Email, member.Name, year)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to execute insert")
}
if (index+1) % BATCH_SIZE == 0 {
err = tx.Commit()
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to commit transaction")
}
tx, err = db_conn.Begin()
if err != nil {
return errors.Wrap(err, "failed to begin new transaction")
}
stmt, err = tx.Prepare(insertMembersQuery)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to prepare new statement")
}
}
}
return tx.Commit()
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Members</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
</head>
<body>
<h1>Upload Members CSV</h1>
<form action="/admin/members" method="post" enctype="multipart/form-data">
<label for="year">Year:</label>
<input type="number" id="year" name="year" required><br><br>
<label for="members.csv">CSV File:</label>
<input type="file" id="members.csv" name="members.csv" accept=".csv" required><br><br>
<button type="submit">Upload</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stats</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>