Skip to content

Commit 54f088e

Browse files
authored
Merge pull request #2 from briefjo/accelerate-with-copilot
Improve student activity registration system
2 parents 884f796 + fcac7e6 commit 54f088e

6 files changed

Lines changed: 303 additions & 1 deletion

File tree

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
fastapi
22
uvicorn
33
httpx
4-
watchfiles
4+
watchfiles
5+
pytest

src/app.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,42 @@
3838
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
3939
"max_participants": 30,
4040
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
41+
},
42+
"Soccer Team": {
43+
"description": "Competitive soccer training and matches",
44+
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
45+
"max_participants": 22,
46+
"participants": ["lucas@mergington.edu", "mia@mergington.edu"]
47+
},
48+
"Basketball Team": {
49+
"description": "Basketball drills, scrimmages, and league games",
50+
"schedule": "Wednesdays and Fridays, 3:30 PM - 5:00 PM",
51+
"max_participants": 15,
52+
"participants": ["liam@mergington.edu", "noah@mergington.edu"]
53+
},
54+
"Art Club": {
55+
"description": "Explore painting, drawing, and mixed media techniques",
56+
"schedule": "Mondays, 3:30 PM - 5:00 PM",
57+
"max_participants": 15,
58+
"participants": ["ava@mergington.edu", "isabella@mergington.edu"]
59+
},
60+
"Drama Club": {
61+
"description": "Theater acting, stagecraft, and seasonal performances",
62+
"schedule": "Tuesdays and Thursdays, 3:30 PM - 5:00 PM",
63+
"max_participants": 20,
64+
"participants": ["amelia@mergington.edu", "harper@mergington.edu"]
65+
},
66+
"Debate Club": {
67+
"description": "Practice public speaking and compete in debate tournaments",
68+
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
69+
"max_participants": 16,
70+
"participants": ["ethan@mergington.edu", "charlotte@mergington.edu"]
71+
},
72+
"Science Club": {
73+
"description": "Hands-on experiments and science fair preparation",
74+
"schedule": "Fridays, 3:30 PM - 5:00 PM",
75+
"max_participants": 18,
76+
"participants": ["james@mergington.edu", "elijah@mergington.edu"]
4177
}
4278
}
4379

@@ -62,6 +98,25 @@ def signup_for_activity(activity_name: str, email: str):
6298
# Get the specific activity
6399
activity = activities[activity_name]
64100

101+
# Validate student is not already signed up
102+
if email in activity["participants"]:
103+
raise HTTPException(status_code=400, detail="Already signed up")
104+
65105
# Add student
66106
activity["participants"].append(email)
67107
return {"message": f"Signed up {email} for {activity_name}"}
108+
109+
110+
@app.delete("/activities/{activity_name}/signup")
111+
def unregister_from_activity(activity_name: str, email: str):
112+
"""Unregister a student from an activity"""
113+
if activity_name not in activities:
114+
raise HTTPException(status_code=404, detail="Activity not found")
115+
116+
activity = activities[activity_name]
117+
118+
if email not in activity["participants"]:
119+
raise HTTPException(status_code=400, detail="Student not signed up for this activity")
120+
121+
activity["participants"].remove(email)
122+
return {"message": f"Unregistered {email} from {activity_name}"}

src/static/app.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,24 @@ document.addEventListener("DOMContentLoaded", () => {
2020

2121
const spotsLeft = details.max_participants - details.participants.length;
2222

23+
const participantsList = details.participants
24+
.map(
25+
(p) =>
26+
`<li><span class="participant-email">${p}</span><button class="remove-btn" data-activity="${name}" data-email="${p}" title="Remove ${p}">&#x2715;</button></li>`
27+
)
28+
.join("");
29+
2330
activityCard.innerHTML = `
2431
<h4>${name}</h4>
2532
<p>${details.description}</p>
2633
<p><strong>Schedule:</strong> ${details.schedule}</p>
2734
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
35+
<div class="participants-section">
36+
<strong>Participants:</strong>
37+
<ul class="participants-list">
38+
${participantsList || "<li class='no-participants'>No participants yet</li>"}
39+
</ul>
40+
</div>
2841
`;
2942

3043
activitiesList.appendChild(activityCard);
@@ -62,6 +75,7 @@ document.addEventListener("DOMContentLoaded", () => {
6275
messageDiv.textContent = result.message;
6376
messageDiv.className = "success";
6477
signupForm.reset();
78+
fetchActivities();
6579
} else {
6680
messageDiv.textContent = result.detail || "An error occurred";
6781
messageDiv.className = "error";
@@ -81,6 +95,41 @@ document.addEventListener("DOMContentLoaded", () => {
8195
}
8296
});
8397

98+
// Handle participant removal
99+
activitiesList.addEventListener("click", async (event) => {
100+
const btn = event.target.closest(".remove-btn");
101+
if (!btn) return;
102+
103+
const activity = btn.dataset.activity;
104+
const email = btn.dataset.email;
105+
106+
try {
107+
const response = await fetch(
108+
`/activities/${encodeURIComponent(activity)}/signup?email=${encodeURIComponent(email)}`,
109+
{ method: "DELETE" }
110+
);
111+
112+
const result = await response.json();
113+
114+
if (response.ok) {
115+
messageDiv.textContent = result.message;
116+
messageDiv.className = "success";
117+
fetchActivities();
118+
} else {
119+
messageDiv.textContent = result.detail || "An error occurred";
120+
messageDiv.className = "error";
121+
}
122+
123+
messageDiv.classList.remove("hidden");
124+
setTimeout(() => messageDiv.classList.add("hidden"), 5000);
125+
} catch (error) {
126+
messageDiv.textContent = "Failed to unregister. Please try again.";
127+
messageDiv.className = "error";
128+
messageDiv.classList.remove("hidden");
129+
console.error("Error unregistering:", error);
130+
}
131+
});
132+
84133
// Initialize app
85134
fetchActivities();
86135
});

src/static/styles.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,53 @@ section h3 {
7474
margin-bottom: 8px;
7575
}
7676

77+
.participants-section {
78+
margin-top: 10px;
79+
padding-top: 10px;
80+
border-top: 1px solid #e0e0e0;
81+
}
82+
83+
.participants-list {
84+
list-style: none;
85+
padding: 0;
86+
margin-top: 6px;
87+
}
88+
89+
.participants-list li {
90+
padding: 4px 10px;
91+
margin-bottom: 4px;
92+
background-color: #e8eaf6;
93+
color: #1a237e;
94+
border-radius: 12px;
95+
font-size: 14px;
96+
display: inline-flex;
97+
align-items: center;
98+
gap: 6px;
99+
margin-right: 4px;
100+
}
101+
102+
.remove-btn {
103+
background: none;
104+
border: none;
105+
color: #c62828;
106+
cursor: pointer;
107+
font-size: 12px;
108+
padding: 0 2px;
109+
line-height: 1;
110+
border-radius: 50%;
111+
transition: background-color 0.2s;
112+
}
113+
114+
.remove-btn:hover {
115+
background-color: #ffcdd2;
116+
}
117+
118+
.participants-list li.no-participants {
119+
background-color: transparent;
120+
color: #999;
121+
font-style: italic;
122+
}
123+
77124
.form-group {
78125
margin-bottom: 15px;
79126
}

tests/__init__.py

Whitespace-only changes.

tests/test_app.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import copy
2+
import pytest
3+
from fastapi.testclient import TestClient
4+
from src.app import app, activities
5+
6+
@pytest.fixture(autouse=True)
7+
def reset_activities():
8+
"""Reset activities to original state before each test."""
9+
original = copy.deepcopy(activities)
10+
yield
11+
activities.clear()
12+
activities.update(original)
13+
14+
15+
client = TestClient(app)
16+
17+
18+
# ── GET / ────────────────────────────────────────────────────────────────
19+
20+
def test_root_redirect():
21+
# Arrange
22+
url = "/"
23+
24+
# Act
25+
response = client.get(url, follow_redirects=False)
26+
27+
# Assert
28+
assert response.status_code == 307
29+
assert response.headers["location"] == "/static/index.html"
30+
31+
32+
# ── GET /activities ──────────────────────────────────────────────────────
33+
34+
def test_get_activities_returns_all():
35+
# Arrange
36+
expected_keys = {"description", "schedule", "max_participants", "participants"}
37+
38+
# Act
39+
response = client.get("/activities")
40+
41+
# Assert
42+
assert response.status_code == 200
43+
data = response.json()
44+
assert isinstance(data, dict)
45+
assert len(data) == len(activities)
46+
for name, details in data.items():
47+
assert expected_keys == set(details.keys())
48+
assert isinstance(details["participants"], list)
49+
50+
51+
# ── POST /activities/{name}/signup ───────────────────────────────────────
52+
53+
def test_signup_success():
54+
# Arrange
55+
activity_name = "Chess Club"
56+
email = "newstudent@mergington.edu"
57+
58+
# Act
59+
response = client.post(
60+
f"/activities/{activity_name}/signup",
61+
params={"email": email},
62+
)
63+
64+
# Assert
65+
assert response.status_code == 200
66+
assert email in activities[activity_name]["participants"]
67+
assert response.json()["message"] == f"Signed up {email} for {activity_name}"
68+
69+
70+
def test_signup_duplicate():
71+
# Arrange
72+
activity_name = "Chess Club"
73+
email = "michael@mergington.edu" # already in participants
74+
75+
# Act
76+
response = client.post(
77+
f"/activities/{activity_name}/signup",
78+
params={"email": email},
79+
)
80+
81+
# Assert
82+
assert response.status_code == 400
83+
assert response.json()["detail"] == "Already signed up"
84+
85+
86+
def test_signup_unknown_activity():
87+
# Arrange
88+
activity_name = "Nonexistent Club"
89+
email = "student@mergington.edu"
90+
91+
# Act
92+
response = client.post(
93+
f"/activities/{activity_name}/signup",
94+
params={"email": email},
95+
)
96+
97+
# Assert
98+
assert response.status_code == 404
99+
assert response.json()["detail"] == "Activity not found"
100+
101+
102+
# ── DELETE /activities/{name}/signup ─────────────────────────────────────
103+
104+
def test_unregister_success():
105+
# Arrange
106+
activity_name = "Chess Club"
107+
email = "michael@mergington.edu" # existing participant
108+
109+
# Act
110+
response = client.delete(
111+
f"/activities/{activity_name}/signup",
112+
params={"email": email},
113+
)
114+
115+
# Assert
116+
assert response.status_code == 200
117+
assert email not in activities[activity_name]["participants"]
118+
assert response.json()["message"] == f"Unregistered {email} from {activity_name}"
119+
120+
121+
def test_unregister_not_signed_up():
122+
# Arrange
123+
activity_name = "Chess Club"
124+
email = "unknown@mergington.edu"
125+
126+
# Act
127+
response = client.delete(
128+
f"/activities/{activity_name}/signup",
129+
params={"email": email},
130+
)
131+
132+
# Assert
133+
assert response.status_code == 400
134+
assert response.json()["detail"] == "Student not signed up for this activity"
135+
136+
137+
def test_unregister_unknown_activity():
138+
# Arrange
139+
activity_name = "Nonexistent Club"
140+
email = "student@mergington.edu"
141+
142+
# Act
143+
response = client.delete(
144+
f"/activities/{activity_name}/signup",
145+
params={"email": email},
146+
)
147+
148+
# Assert
149+
assert response.status_code == 404
150+
assert response.json()["detail"] == "Activity not found"

0 commit comments

Comments
 (0)