Skip to content

Commit 7e4942a

Browse files
committed
Implement activity participant management with signup and unregister functionality
1 parent 8e7bf3e commit 7e4942a

5 files changed

Lines changed: 213 additions & 70 deletions

File tree

src/activities.json

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"Chess Club": {
3+
"description": "Learn strategies and compete in chess tournaments",
4+
"schedule": "Fridays, 3:30 PM - 5:00 PM",
5+
"max_participants": 12,
6+
"participants": [
7+
"michael@mergington.edu",
8+
"daniel@mergington.edu"
9+
]
10+
},
11+
"Programming Class": {
12+
"description": "Learn programming fundamentals and build software projects",
13+
"schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM",
14+
"max_participants": 20,
15+
"participants": [
16+
"emma@mergington.edu",
17+
"sophia@mergington.edu"
18+
]
19+
},
20+
"Gym Class": {
21+
"description": "Physical education and sports activities",
22+
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
23+
"max_participants": 30,
24+
"participants": [
25+
"john@mergington.edu",
26+
"olivia@mergington.edu"
27+
]
28+
},
29+
"Soccer Team": {
30+
"description": "Practice and compete in interschool soccer matches",
31+
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
32+
"max_participants": 25,
33+
"participants": [
34+
"alex@mergington.edu",
35+
"mia@mergington.edu"
36+
]
37+
},
38+
"Swimming Club": {
39+
"description": "Swimming lessons and competitive training",
40+
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
41+
"max_participants": 15,
42+
"participants": [
43+
"lucas@mergington.edu"
44+
]
45+
},
46+
"Art Studio": {
47+
"description": "Explore painting, drawing, and mixed media art",
48+
"schedule": "Mondays, 3:30 PM - 5:00 PM",
49+
"max_participants": 18,
50+
"participants": [
51+
"isabella@mergington.edu",
52+
"ava@mergington.edu"
53+
]
54+
},
55+
"Drama Club": {
56+
"description": "Acting, theater production, and performance arts",
57+
"schedule": "Thursdays, 4:00 PM - 6:00 PM",
58+
"max_participants": 20,
59+
"participants": [
60+
"noah@mergington.edu"
61+
]
62+
},
63+
"Debate Team": {
64+
"description": "Develop critical thinking and public speaking skills",
65+
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
66+
"max_participants": 16,
67+
"participants": [
68+
"liam@mergington.edu",
69+
"charlotte@mergington.edu"
70+
]
71+
},
72+
"Science Olympiad": {
73+
"description": "Compete in science and engineering challenges",
74+
"schedule": "Fridays, 3:30 PM - 5:30 PM",
75+
"max_participants": 15,
76+
"participants": [
77+
"ethan@mergington.edu"
78+
]
79+
}
80+
}

src/app.py

Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
for extracurricular activities at Mergington High School.
66
"""
77

8-
from fastapi import FastAPI, HTTPException
8+
from fastapi import FastAPI, HTTPException, Query
99
from fastapi.staticfiles import StaticFiles
1010
from fastapi.responses import RedirectResponse
11+
1112
import os
1213
from pathlib import Path
14+
import json
15+
from threading import Lock
1316

1417
app = FastAPI(title="Mergington High School API",
1518
description="API for viewing and signing up for extracurricular activities")
@@ -19,63 +22,20 @@
1922
app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent,
2023
"static")), name="static")
2124

22-
# In-memory activity database
23-
activities = {
24-
"Chess Club": {
25-
"description": "Learn strategies and compete in chess tournaments",
26-
"schedule": "Fridays, 3:30 PM - 5:00 PM",
27-
"max_participants": 12,
28-
"participants": ["michael@mergington.edu", "daniel@mergington.edu"]
29-
},
30-
"Programming Class": {
31-
"description": "Learn programming fundamentals and build software projects",
32-
"schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM",
33-
"max_participants": 20,
34-
"participants": ["emma@mergington.edu", "sophia@mergington.edu"]
35-
},
36-
"Gym Class": {
37-
"description": "Physical education and sports activities",
38-
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
39-
"max_participants": 30,
40-
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
41-
},
42-
"Soccer Team": {
43-
"description": "Practice and compete in interschool soccer matches",
44-
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
45-
"max_participants": 25,
46-
"participants": ["alex@mergington.edu", "mia@mergington.edu"]
47-
},
48-
"Swimming Club": {
49-
"description": "Swimming lessons and competitive training",
50-
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
51-
"max_participants": 15,
52-
"participants": ["lucas@mergington.edu"]
53-
},
54-
"Art Studio": {
55-
"description": "Explore painting, drawing, and mixed media art",
56-
"schedule": "Mondays, 3:30 PM - 5:00 PM",
57-
"max_participants": 18,
58-
"participants": ["isabella@mergington.edu", "ava@mergington.edu"]
59-
},
60-
"Drama Club": {
61-
"description": "Acting, theater production, and performance arts",
62-
"schedule": "Thursdays, 4:00 PM - 6:00 PM",
63-
"max_participants": 20,
64-
"participants": ["noah@mergington.edu"]
65-
},
66-
"Debate Team": {
67-
"description": "Develop critical thinking and public speaking skills",
68-
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
69-
"max_participants": 16,
70-
"participants": ["liam@mergington.edu", "charlotte@mergington.edu"]
71-
},
72-
"Science Olympiad": {
73-
"description": "Compete in science and engineering challenges",
74-
"schedule": "Fridays, 3:30 PM - 5:30 PM",
75-
"max_participants": 15,
76-
"participants": ["ethan@mergington.edu"]
77-
}
78-
}
25+
26+
# Persistent activities store
27+
DATA_FILE = os.path.join(Path(__file__).parent, "activities.json")
28+
_activities_lock = Lock()
29+
30+
def load_activities():
31+
with _activities_lock:
32+
with open(DATA_FILE, "r", encoding="utf-8") as f:
33+
return json.load(f)
34+
35+
def save_activities(activities):
36+
with _activities_lock:
37+
with open(DATA_FILE, "w", encoding="utf-8") as f:
38+
json.dump(activities, f, indent=2)
7939

8040

8141
@app.get("/")
@@ -85,24 +45,33 @@ def root():
8545

8646
@app.get("/activities")
8747
def get_activities():
88-
return activities
48+
return load_activities()
8949

9050

9151
@app.post("/activities/{activity_name}/signup")
9252
def signup_for_activity(activity_name: str, email: str):
9353
"""Sign up a student for an activity"""
94-
# Validate activity exists
54+
activities = load_activities()
9555
if activity_name not in activities:
9656
raise HTTPException(status_code=404, detail="Activity not found")
97-
98-
# Get the specific activity
9957
activity = activities[activity_name]
100-
101-
102-
# Validate student is not already signed up
10358
if email in activity["participants"]:
10459
raise HTTPException(status_code=400, detail="Student already signed up for this activity")
105-
106-
# Add student
10760
activity["participants"].append(email)
61+
save_activities(activities)
10862
return {"message": f"Signed up {email} for {activity_name}"}
63+
64+
65+
# Unregister a participant from an activity
66+
@app.delete("/activities/{activity_name}/unregister")
67+
def unregister_from_activity(activity_name: str, email: str = Query(...)):
68+
"""Unregister a student from an activity"""
69+
activities = load_activities()
70+
if activity_name not in activities:
71+
raise HTTPException(status_code=404, detail="Activity not found")
72+
activity = activities[activity_name]
73+
if email not in activity["participants"]:
74+
raise HTTPException(status_code=400, detail="Student is not registered for this activity")
75+
activity["participants"].remove(email)
76+
save_activities(activities)
77+
return {"message": f"Unregistered {email} from {activity_name}"}

src/static/app.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ document.addEventListener("DOMContentLoaded", () => {
2626
participantsHTML = `
2727
<div class="participants-section">
2828
<strong>Participants:</strong>
29-
<ul class="participants-list">
29+
<ul class="participants-list no-bullets">
3030
${details.participants
3131
.map(
3232
(p) =>
33-
`<li class="participant-item">${p}</li>`
33+
`<li class="participant-item">${p} <span class="delete-participant" title="Remove participant" data-activity="${name}" data-email="${p}">🗑️</span></li>`
3434
)
3535
.join("")}
3636
</ul>
@@ -44,6 +44,7 @@ document.addEventListener("DOMContentLoaded", () => {
4444
`;
4545
}
4646

47+
4748
activityCard.innerHTML = `
4849
<h4>${name}</h4>
4950
<p>${details.description}</p>
@@ -52,6 +53,30 @@ document.addEventListener("DOMContentLoaded", () => {
5253
${participantsHTML}
5354
`;
5455

56+
// Add event listeners for delete icons after rendering
57+
setTimeout(() => {
58+
activityCard.querySelectorAll('.delete-participant').forEach((icon) => {
59+
icon.addEventListener('click', async (e) => {
60+
const activity = icon.getAttribute('data-activity');
61+
const email = icon.getAttribute('data-email');
62+
if (!confirm(`Remove ${email} from ${activity}?`)) return;
63+
try {
64+
const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, {
65+
method: 'DELETE',
66+
});
67+
const result = await response.json();
68+
if (response.ok) {
69+
fetchActivities();
70+
} else {
71+
alert(result.detail || 'Failed to remove participant.');
72+
}
73+
} catch (err) {
74+
alert('Failed to remove participant.');
75+
}
76+
});
77+
});
78+
}, 0);
79+
5580
activitiesList.appendChild(activityCard);
5681

5782
// Add option to select dropdown
@@ -87,6 +112,7 @@ document.addEventListener("DOMContentLoaded", () => {
87112
messageDiv.textContent = result.message;
88113
messageDiv.className = "success";
89114
signupForm.reset();
115+
fetchActivities(); // Refresh activities list after successful signup
90116
} else {
91117
messageDiv.textContent = result.detail || "An error occurred";
92118
messageDiv.className = "error";

src/static/styles.css

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,44 @@ footer {
158158
.participants-list {
159159
margin: 8px 0 0 18px;
160160
padding-left: 0;
161-
list-style-type: disc;
161+
list-style-type: none;
162+
}
163+
164+
.no-bullets {
165+
list-style-type: none !important;
166+
margin-left: 0 !important;
167+
padding-left: 0 !important;
162168
}
163169

164170
.participant-item {
165171
color: #3949ab;
166172
font-size: 15px;
167173
margin-bottom: 2px;
174+
display: flex;
175+
align-items: center;
176+
gap: 8px;
177+
}
178+
179+
.delete-participant {
180+
display: inline-flex;
181+
align-items: center;
182+
justify-content: center;
183+
width: 22px;
184+
height: 22px;
185+
background: #e53935;
186+
color: #fff;
187+
border-radius: 50%;
188+
font-size: 16px;
189+
cursor: pointer;
190+
margin-left: 8px;
191+
border: none;
192+
transition: background 0.2s, box-shadow 0.2s;
193+
box-shadow: 0 1px 3px rgba(229,57,53,0.08);
194+
line-height: 1;
195+
}
196+
.delete-participant:hover {
197+
background: #b71c1c;
198+
box-shadow: 0 2px 6px rgba(229,57,53,0.18);
168199
}
169200

170201
.no-participants {

tests/test_app.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from src.app import app
4+
5+
client = TestClient(app)
6+
7+
def test_get_activities():
8+
response = client.get("/activities")
9+
assert response.status_code == 200
10+
data = response.json()
11+
assert isinstance(data, dict)
12+
assert "Chess Club" in data
13+
14+
def test_signup_and_unregister():
15+
test_email = "pytestuser@mergington.edu"
16+
activity = "Chess Club"
17+
18+
# Ensure not already signed up
19+
client.delete(f"/activities/{activity}/unregister", params={"email": test_email})
20+
21+
# Sign up
22+
response = client.post(f"/activities/{activity}/signup?email={test_email}")
23+
assert response.status_code == 200
24+
assert f"Signed up {test_email}" in response.json()["message"]
25+
26+
# Check participant is in list
27+
activities = client.get("/activities").json()
28+
assert test_email in activities[activity]["participants"]
29+
30+
# Unregister
31+
response = client.delete(f"/activities/{activity}/unregister", params={"email": test_email})
32+
assert response.status_code == 200
33+
assert f"Unregistered {test_email}" in response.json()["message"]
34+
35+
# Check participant is removed
36+
activities = client.get("/activities").json()
37+
assert test_email not in activities[activity]["participants"]

0 commit comments

Comments
 (0)