From 4ad970cd8bec01d58e803019fecf69cc6fc0415c Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Sun, 5 Nov 2023 10:22:46 -0600 Subject: [PATCH 1/8] Added star ranking on live page + fixed paused bug --- .../src/components/judge/ProjectDisplay.tsx | 22 ++++++++++--- client/src/components/judge/Star.tsx | 2 +- client/src/pages/judge/live.tsx | 31 ++++++++++--------- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/client/src/components/judge/ProjectDisplay.tsx b/client/src/components/judge/ProjectDisplay.tsx index 2e943d6..4139166 100644 --- a/client/src/components/judge/ProjectDisplay.tsx +++ b/client/src/components/judge/ProjectDisplay.tsx @@ -3,11 +3,15 @@ import { twMerge } from 'tailwind-merge'; import Paragraph from '../Paragraph'; import { getRequest } from '../../api'; import { errorAlert } from '../../util'; +import StarDisplay from './StarDisplay'; interface ProjectDisplayProps { /* Project ID to display */ projectId: string; + /* Judge for the project */ + judge: Judge; + /* Define the className */ className?: string; } @@ -18,7 +22,7 @@ const ProjectDisplay = (props: ProjectDisplayProps) => { useEffect(() => { async function fetchData() { if (!props.projectId) return; - + const projRes = await getRequest(`/project/${props.projectId}`, 'judge'); if (projRes.status !== 200) { errorAlert(projRes); @@ -38,9 +42,19 @@ const ProjectDisplay = (props: ProjectDisplayProps) => { return (
-

{project.name}

-

Table {project.location}

- +

{project.name}

+

Table {project.location}

+
+ jp.project_id === project.id) + ?.stars || 0 + } + /> +
+
); }; diff --git a/client/src/components/judge/Star.tsx b/client/src/components/judge/Star.tsx index 2ec07bf..248a238 100644 --- a/client/src/components/judge/Star.tsx +++ b/client/src/components/judge/Star.tsx @@ -13,7 +13,7 @@ interface StarProps { } const Star = (props: StarProps) => { - const handleClick: React.MouseEventHandler = async (e) => { + const handleClick: React.MouseEventHandler = async () => { if (!props.clickable) return; // Update star count based on the star # clicked diff --git a/client/src/pages/judge/live.tsx b/client/src/pages/judge/live.tsx index f2b11c7..52c195d 100644 --- a/client/src/pages/judge/live.tsx +++ b/client/src/pages/judge/live.tsx @@ -73,7 +73,7 @@ const JudgeLive = () => { return; } if (startedRes.data?.ok !== 1) { - console.error(`Judging is paused!`); + setVerified(true); setInfoPage('paused'); return; } @@ -128,7 +128,8 @@ const JudgeLive = () => { useEffect(() => { if (!verified) return; - + if (infoPage === 'paused') return; + getJudgeData(); }, [verified]); @@ -256,6 +257,17 @@ const JudgeLive = () => { getJudgeData(); }; + // Display an error page if an error condition holds + const infoIndex = infoPages.indexOf(infoPage); + if (infoIndex !== -1) { + return ( + + ); + } + // Show loading screen if judge does not exist if (!judge) { return ( @@ -308,17 +320,6 @@ const JudgeLive = () => { ); }; - // Display an error page if an error condition holds - const infoIndex = infoPages.indexOf(infoPage); - if (infoIndex !== -1) { - return ( - - ); - } - return ( <> @@ -386,12 +387,12 @@ const JudgeLive = () => { - {judge.next && } + {judge.next && } {judge.prev && ( <>

Previous Project

- + )} Date: Tue, 14 Nov 2023 19:30:16 -0600 Subject: [PATCH 2/8] Added env check for email envs + refactor packages for utils --- server/config/env.go | 12 +++++++++-- server/{util => funcs}/csv.go | 2 +- server/{util => funcs}/email.go | 2 +- server/{util => funcs}/email_test.go | 5 ++--- server/{util => funcs}/judging_flow.go | 2 +- server/main.go | 4 ++-- server/router/admin.go | 14 ++++++------ server/router/judge.go | 19 ++++++++-------- server/router/project.go | 6 +++--- server/util/slices.go | 30 ++++++++++++++++++++++++++ 10 files changed, 67 insertions(+), 29 deletions(-) rename server/{util => funcs}/csv.go (99%) rename server/{util => funcs}/email.go (99%) rename server/{util => funcs}/email_test.go (75%) rename server/{util => funcs}/judging_flow.go (99%) create mode 100644 server/util/slices.go diff --git a/server/config/env.go b/server/config/env.go index 923c16b..60920f5 100644 --- a/server/config/env.go +++ b/server/config/env.go @@ -3,17 +3,25 @@ package config import ( "log" "os" + "server/util" ) var requiredEnvs = [...]string{"JURY_ADMIN_PASSWORD", "EMAIL_FROM"} +var smtpEnvs = []string{"EMAIL_HOST", "EMAIL_USERNAME", "EMAIL_PASSWORD"} +var sendgridEnvs = []string{"SENDGRID_API_KEY", "EMAIL_FROM_NAME"} // Checks to see if all required environmental variables are defined func CheckEnv() { for _, v := range requiredEnvs { if !hasEnv(v) { - log.Fatalf("%s environmental variable not defined\n", v) + log.Fatalf("ERROR: %s environmental variable not defined\n", v) } } + + // Check to see if either all smtp envs are defined or all sendgrid envs are defined + if !util.All(util.Map(smtpEnvs, hasEnv)) && !util.All(util.Map(sendgridEnvs, hasEnv)) { + log.Fatalf("ERROR: either all envs for smtp or sendgrid must be defined (one of these sets): %v OR %v\n", smtpEnvs, sendgridEnvs) + } } // Returns true if the environmental variable is defined and not empty @@ -28,7 +36,7 @@ func hasEnv(key string) bool { func GetEnv(key string) string { val, ok := os.LookupEnv(key) if !ok { - log.Fatalf("%s environmental variable not defined\n", key) + log.Fatalf("ERROR: %s environmental variable not defined\n", key) return "" } return val diff --git a/server/util/csv.go b/server/funcs/csv.go similarity index 99% rename from server/util/csv.go rename to server/funcs/csv.go index f27c8c8..1ba28ec 100644 --- a/server/util/csv.go +++ b/server/funcs/csv.go @@ -1,4 +1,4 @@ -package util +package funcs import ( "archive/zip" diff --git a/server/util/email.go b/server/funcs/email.go similarity index 99% rename from server/util/email.go rename to server/funcs/email.go index 34715d1..eb1250d 100644 --- a/server/util/email.go +++ b/server/funcs/email.go @@ -1,4 +1,4 @@ -package util +package funcs import ( "bytes" diff --git a/server/util/email_test.go b/server/funcs/email_test.go similarity index 75% rename from server/util/email_test.go rename to server/funcs/email_test.go index cf6c643..efe9830 100644 --- a/server/util/email_test.go +++ b/server/funcs/email_test.go @@ -1,8 +1,7 @@ -package util_test +package funcs import ( "server/models" - "server/util" "testing" "github.com/joho/godotenv" @@ -12,7 +11,7 @@ func TestSendJudgeEmail(t *testing.T) { godotenv.Load("../.env") judge := models.NewJudge("Michael Zhao", "michaelzhao314@gmail.com", "notes here") - err := util.SendJudgeEmail(judge, "http://localhost:3000") + err := SendJudgeEmail(judge, "http://localhost:3000") if err != nil { t.Errorf(err.Error()) t.FailNow() diff --git a/server/util/judging_flow.go b/server/funcs/judging_flow.go similarity index 99% rename from server/util/judging_flow.go rename to server/funcs/judging_flow.go index 818eba0..6ab2ffd 100644 --- a/server/util/judging_flow.go +++ b/server/funcs/judging_flow.go @@ -1,4 +1,4 @@ -package util +package funcs import ( "math/rand" diff --git a/server/main.go b/server/main.go index b0e4f19..1b8239f 100644 --- a/server/main.go +++ b/server/main.go @@ -13,10 +13,10 @@ func main() { // Load the env file err := godotenv.Load() if err != nil { - fmt.Printf("Error loading .env file (%s). This can be safely ignored in production.\n", err.Error()) + fmt.Printf("Did not load .env file (%s). This is expected when running in a Docker container\n", err.Error()) } - // Check for all necessary env files + // Check for all necessary env variables) config.CheckEnv() // Connect to the database diff --git a/server/router/admin.go b/server/router/admin.go index 4c09491..d6e9850 100644 --- a/server/router/admin.go +++ b/server/router/admin.go @@ -5,8 +5,8 @@ import ( "net/http" "server/config" "server/database" + "server/funcs" "server/models" - "server/util" "sort" "strconv" "strings" @@ -300,10 +300,10 @@ func ExportJudges(ctx *gin.Context) { } // Create the CSV - csvData := util.CreateJudgeCSV(judges) + csvData := funcs.CreateJudgeCSV(judges) // Send CSV - util.AddCsvData("judges", csvData, ctx) + funcs.AddCsvData("judges", csvData, ctx) } // POST /admin/export/projects - ExportProjects exports all projects to a CSV @@ -319,10 +319,10 @@ func ExportProjects(ctx *gin.Context) { } // Create the CSV - csvData := util.CreateProjectCSV(projects) + csvData := funcs.CreateProjectCSV(projects) // Send CSV - util.AddCsvData("projects", csvData, ctx) + funcs.AddCsvData("projects", csvData, ctx) } // POST /admin/export/challenges - ExportProjectsByChallenge exports all projects to a zip file, with CSVs each @@ -339,14 +339,14 @@ func ExportProjectsByChallenge(ctx *gin.Context) { } // Create the zip file - zipData, err := util.CreateProjectChallengeZip(projects) + zipData, err := funcs.CreateProjectChallengeZip(projects) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error creating zip file: " + err.Error()}) return } // Send zip file - util.AddZipFile("projects", zipData, ctx) + funcs.AddZipFile("projects", zipData, ctx) } func GetJudgingTimer(ctx *gin.Context) { diff --git a/server/router/judge.go b/server/router/judge.go index 166392d..e0f770b 100644 --- a/server/router/judge.go +++ b/server/router/judge.go @@ -4,6 +4,7 @@ import ( "net/http" "server/crowdbt" "server/database" + "server/funcs" "server/models" "server/util" @@ -51,13 +52,13 @@ func AddJudge(ctx *gin.Context) { hostname := util.GetFullHostname(ctx) // Make sure email is right - if !util.CheckEmail(judge.Email) { + if !funcs.CheckEmail(judge.Email) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid email"}) return } // Send email to judge - err = util.SendJudgeEmail(judge, hostname) + err = funcs.SendJudgeEmail(judge, hostname) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error sending judge email: " + err.Error()}) return @@ -160,7 +161,7 @@ func AddJudgesCsv(ctx *gin.Context) { } // Parse the CSV file - judges, err := util.ParseJudgeCSV(string(content), hasHeader) + judges, err := funcs.ParseJudgeCSV(string(content), hasHeader) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "error parsing CSV file: " + err.Error()}) return @@ -171,7 +172,7 @@ func AddJudgesCsv(ctx *gin.Context) { // Check all judge emails for _, judge := range judges { - if !util.CheckEmail(judge.Email) { + if !funcs.CheckEmail(judge.Email) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid email: " + judge.Email}) return } @@ -179,7 +180,7 @@ func AddJudgesCsv(ctx *gin.Context) { // Send emails to all judges for _, judge := range judges { - err = util.SendJudgeEmail(judge, hostname) + err = funcs.SendJudgeEmail(judge, hostname) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error sending judge " + judge.Name + " email: " + err.Error()}) return @@ -321,7 +322,7 @@ func JudgeVote(ctx *gin.Context) { // If there is no previous project, then this is the first project for the judge if prevProject == nil { // Get a new project for the judge - newProject, err := util.PickNextProject(db, judge) + newProject, err := funcs.PickNextProject(db, judge) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error picking next project: " + err.Error()}) return @@ -373,7 +374,7 @@ func JudgeVote(ctx *gin.Context) { winner.Votes += 1 // Get new project for judge - newProject, err := util.PickNextProject(db, judge) + newProject, err := funcs.PickNextProject(db, judge) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error picking next project: " + err.Error()}) return @@ -419,7 +420,7 @@ func GetJudgeIPO(ctx *gin.Context) { // Get the next project for the judge and update the judge/project seen in the database // If no project, return initial as false - project, err := util.PickNextProject(db, judge) + project, err := funcs.PickNextProject(db, judge) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error picking next project: " + err.Error()}) return @@ -588,7 +589,7 @@ func JudgeSkip(ctx *gin.Context) { } // Get a new project for the judge - newProject, err := util.PickNextProject(db, judge) + newProject, err := funcs.PickNextProject(db, judge) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error picking next project: " + err.Error()}) return diff --git a/server/router/project.go b/server/router/project.go index 87021c4..f090e85 100644 --- a/server/router/project.go +++ b/server/router/project.go @@ -4,8 +4,8 @@ import ( "context" "net/http" "server/database" + "server/funcs" "server/models" - "server/util" "sort" "strings" @@ -42,7 +42,7 @@ func AddDevpostCsv(ctx *gin.Context) { } // Parse the CSV file - projects, err := util.ParseDevpostCSV(string(content), db) + projects, err := funcs.ParseDevpostCSV(string(content), db) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "error parsing CSV file: " + err.Error()}) return @@ -177,7 +177,7 @@ func AddProjectsCsv(ctx *gin.Context) { } // Parse the CSV file - projects, err := util.ParseProjectCsv(string(content), hasHeader, db) + projects, err := funcs.ParseProjectCsv(string(content), hasHeader, db) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "error parsing CSV file: " + err.Error()}) return diff --git a/server/util/slices.go b/server/util/slices.go new file mode 100644 index 0000000..721be89 --- /dev/null +++ b/server/util/slices.go @@ -0,0 +1,30 @@ +package util + +// Map applies a function to each element of a slice and returns a new slice +func Map[T, U interface{}](arr []T, fn func(T) U) []U { + out := make([]U, len(arr)) + for i, v := range arr { + out[i] = fn(v) + } + return out +} + +// Any returns true if any element of the slice is true +func Any[T bool](arr []T) bool { + for _, v := range arr { + if v { + return true + } + } + return false +} + +// All returns true if all elements of the slice are true +func All[T bool](arr []T) bool { + for _, v := range arr { + if !v { + return false + } + } + return true +} From 0304c6fccfbf6e17e005bbb70a7ece1eae7157f1 Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Tue, 14 Nov 2023 19:34:48 -0600 Subject: [PATCH 3/8] updated func doc comments --- server/config/env.go | 4 +++- server/database/init.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/config/env.go b/server/config/env.go index 60920f5..7003edd 100644 --- a/server/config/env.go +++ b/server/config/env.go @@ -24,7 +24,7 @@ func CheckEnv() { } } -// Returns true if the environmental variable is defined and not empty +// hasEnv returns true if the environmental variable is defined and not empty func hasEnv(key string) bool { val, ok := os.LookupEnv(key) if !ok { @@ -33,6 +33,7 @@ func hasEnv(key string) bool { return val != "" } +// GetEnv returns the value of the environmental variable or panics if it does not exist func GetEnv(key string) string { val, ok := os.LookupEnv(key) if !ok { @@ -42,6 +43,7 @@ func GetEnv(key string) string { return val } +// GetOptEnv returns the value of the environmental variable or the default value if it does not exist func GetOptEnv(key string, defaultVal string) string { val, ok := os.LookupEnv(key) if !ok { diff --git a/server/database/init.go b/server/database/init.go index f07061a..1e38122 100644 --- a/server/database/init.go +++ b/server/database/init.go @@ -11,7 +11,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) -// Initializes the database connection to MongoDB. +// InitDb initializes the database connection to MongoDB. // This will proactively panic if any step of the connection protocol breaks func InitDb() *mongo.Database { // Use the SetServerAPIOptions() method to set the Stable API version to 1 From 674eea4675f7be28040c9900e5bd815a54688a95 Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Wed, 15 Nov 2023 00:39:12 -0600 Subject: [PATCH 4/8] Fix clock and added reset in settings --- client/src/pages/admin/settings.tsx | 87 ++++++++++++++++++++++------- server/router/admin.go | 2 +- server/router/init.go | 31 ++++++++-- 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/client/src/pages/admin/settings.tsx b/client/src/pages/admin/settings.tsx index 204c4c1..99eb35f 100644 --- a/client/src/pages/admin/settings.tsx +++ b/client/src/pages/admin/settings.tsx @@ -7,8 +7,20 @@ import Popup from '../../components/Popup'; import Checkbox from '../../components/Checkbox'; import Loading from '../../components/Loading'; +// Text components +const Section = ({ children: c }: { children: React.ReactNode }) => ( +

{c}

+); +const SubSection = ({ children: c }: { children: React.ReactNode }) => ( +

{c}

+); +const Description = ({ children: c }: { children: React.ReactNode }) => ( +

{c}

+); + const AdminSettings = () => { const [reassignPopup, setReassignPopup] = useState(false); + const [clockResetPopup, setClockResetPopup] = useState(false); const [groupChecked, setGroupChecked] = useState(false); const [groups, setGroups] = useState(''); const [judgingTimer, setJudgingTimer] = useState(''); @@ -87,11 +99,22 @@ const AdminSettings = () => { errorAlert(res); return; } - + alert('Timer updated!'); getOptions(); }; + const resetClock = async () => { + const res = await postRequest('/admin/clock/reset', 'admin', null); + if (res.status !== 200 || res.data?.ok !== 1) { + errorAlert(res); + return; + } + + alert('Clock reset!'); + setClockResetPopup(false); + }; + const exportCsv = async (type: string) => { const res = await fetch(`${process.env.REACT_APP_JURY_URL}/admin/export/${type}`, { method: 'GET', @@ -103,9 +126,9 @@ const AdminSettings = () => { return; } - saveToFile(await res.blob() as Blob, type, 'csv'); + saveToFile((await res.blob()) as Blob, type, 'csv'); }; - + const exportByChallenge = async () => { const res = await fetch(`${process.env.REACT_APP_JURY_URL}/admin/export/challenges`, { method: 'GET', @@ -117,9 +140,9 @@ const AdminSettings = () => { return; } - saveToFile(await res.blob() as Blob, 'challenge-projects', 'zip'); + saveToFile((await res.blob()) as Blob, 'challenge-projects', 'zip'); }; - + const saveToFile = (blob: Blob, name: string, ext: string) => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -136,12 +159,24 @@ const AdminSettings = () => {

Settings

{/* TODO: Add other settings */} -

Judging Timer

-

Set Judging Timer

-

+

Set Main Clock
+ Reset Clock + Reset the clock back to 00:00:00 + +
Judging Timer
+ Set Judging Timer + Set how long judges have to view each project. This will reflect on the timer that shows on the judging page. -

+
{ > Update Timer -

Project Numbers

-

Reassign Project Numbers

-

- Reassign all table numbers to the projects. This will keep the relative order - but reassign the table numbers starting from the first project. -

+
Project Numbers
+ Reassign Project Numbers + + Reassign all project numbers to the projects. This will keep the relative order + but reassign the project numbers starting from the first project. + -

Table Groupings

-

+ Table Groupings + Check this box to use table groupings. This will force judges to stay in a grouping for 3 rounds before moving on. This ideally should decrease the distance judges will have to walk, if groups are defined correctly. Note that group sizes must be greater than 3 otherwise the groupings will be ignored. -

+ { @@ -209,9 +244,9 @@ const AdminSettings = () => { > Update Groupings -

Export Data

-

Export Collections

-

Export each collection individually as a CSV download.

+
Export Data
+ Export Collection + Export each collection individually as a CSV download.
From 32b47e87282f53915ea0b2cdbe0025944d121fea Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Sat, 18 Nov 2023 02:44:16 -0600 Subject: [PATCH 8/8] Added drop db button to settings page --- client/src/pages/admin/settings.tsx | 42 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/client/src/pages/admin/settings.tsx b/client/src/pages/admin/settings.tsx index 7d21637..eb18f4a 100644 --- a/client/src/pages/admin/settings.tsx +++ b/client/src/pages/admin/settings.tsx @@ -21,6 +21,7 @@ const Description = ({ children: c }: { children: React.ReactNode }) => ( const AdminSettings = () => { const [reassignPopup, setReassignPopup] = useState(false); const [clockResetPopup, setClockResetPopup] = useState(false); + const [dropPopup, setDropPopup] = useState(false); const [groupChecked, setGroupChecked] = useState(false); const [groups, setGroups] = useState(''); const [judgingTimer, setJudgingTimer] = useState(''); @@ -115,6 +116,17 @@ const AdminSettings = () => { setClockResetPopup(false); }; + const dropDatabase = async () => { + const res = await postRequest('/admin/reset', 'admin', null); + if (res.status !== 200 || res.data?.ok !== 1) { + errorAlert(res); + return; + } + + alert('Database reset!'); + setDropPopup(false); + }; + const exportCsv = async (type: string) => { const res = await fetch(`${process.env.REACT_APP_JURY_URL}/admin/export/${type}`, { method: 'GET', @@ -224,11 +236,11 @@ const AdminSettings = () => { > Enable Table Groupings -

+ { 'List table groupings below. It should be in the form . Each group should be on its own line. No table numbers should overlap. If groups are defined, table numbers will be assigned according to the ranges defined here (ie. groups of 1-10, 101-110 will skip table numbers 11-100). If there are more table numbers than groups defined, the default behavior is to append incrementally to the last group.' } -

+