Skip to content

Commit f359aa5

Browse files
authored
Implement activity unregistration feature and update UI for participant management
1 parent 7ac7e0c commit f359aa5

File tree

6 files changed

+345
-4
lines changed

6 files changed

+345
-4
lines changed

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
fastapi
22
uvicorn
3+
pytest
4+
httpx

src/app.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,22 @@ def signup_for_activity(activity_name: str, email: str):
105105
# Add student
106106
activity["participants"].append(email)
107107
return {"message": f"Signed up {email} for {activity_name}"}
108+
109+
110+
@app.delete("/activities/{activity_name}/unregister")
111+
def unregister_from_activity(activity_name: str, email: str):
112+
"""Unregister a student from an activity"""
113+
# Validate activity exists
114+
if activity_name not in activities:
115+
raise HTTPException(status_code=404, detail="Activity not found")
116+
117+
# Get the specific activity
118+
activity = activities[activity_name]
119+
120+
# Validate student is signed up
121+
if email not in activity["participants"]:
122+
raise HTTPException(status_code=400, detail="Student not signed up for this activity")
123+
124+
# Remove student
125+
activity["participants"].remove(email)
126+
return {"message": f"Unregistered {email} from {activity_name}"}

src/static/app.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ document.addEventListener("DOMContentLoaded", () => {
2222

2323
const participantsList = details.participants.length > 0
2424
? `<ul class="participants-list">
25-
${details.participants.map(email => `<li>${email}</li>`).join('')}
25+
${details.participants.map(email => `
26+
<li>
27+
<span class="participant-email">${email}</span>
28+
<button class="delete-btn" data-activity="${name}" data-email="${email}" title="Unregister">🗑️</button>
29+
</li>
30+
`).join('')}
2631
</ul>`
2732
: `<p class="no-participants">No participants yet. Be the first to sign up!</p>`;
2833

@@ -69,6 +74,9 @@ document.addEventListener("DOMContentLoaded", () => {
6974
const result = await response.json();
7075

7176
if (response.ok) {
77+
// Refresh activities to show updated list
78+
await fetchActivities();
79+
7280
messageDiv.textContent = result.message;
7381
messageDiv.className = "success";
7482
signupForm.reset();
@@ -91,6 +99,52 @@ document.addEventListener("DOMContentLoaded", () => {
9199
}
92100
});
93101

102+
// Handle delete button clicks
103+
activitiesList.addEventListener("click", async (event) => {
104+
if (event.target.classList.contains("delete-btn")) {
105+
const activityName = event.target.dataset.activity;
106+
const email = event.target.dataset.email;
107+
108+
if (!confirm(`Are you sure you want to unregister ${email} from ${activityName}?`)) {
109+
return;
110+
}
111+
112+
try {
113+
const response = await fetch(
114+
`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`,
115+
{
116+
method: "DELETE",
117+
}
118+
);
119+
120+
const result = await response.json();
121+
122+
if (response.ok) {
123+
// Refresh activities to show updated list
124+
await fetchActivities();
125+
126+
messageDiv.textContent = result.message;
127+
messageDiv.className = "success";
128+
messageDiv.classList.remove("hidden");
129+
130+
// Hide message after 3 seconds
131+
setTimeout(() => {
132+
messageDiv.classList.add("hidden");
133+
}, 3000);
134+
} else {
135+
messageDiv.textContent = result.detail || "Failed to unregister";
136+
messageDiv.className = "error";
137+
messageDiv.classList.remove("hidden");
138+
}
139+
} catch (error) {
140+
messageDiv.textContent = "Failed to unregister. Please try again.";
141+
messageDiv.className = "error";
142+
messageDiv.classList.remove("hidden");
143+
console.error("Error unregistering:", error);
144+
}
145+
}
146+
});
147+
94148
// Initialize app
95149
fetchActivities();
96150
});

src/static/styles.css

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,38 @@ section h3 {
8686
}
8787

8888
.participants-list {
89-
list-style-type: disc;
90-
margin-left: 20px;
89+
list-style-type: none;
90+
margin-left: 0;
9191
color: #555;
9292
}
9393

9494
.participants-list li {
9595
margin-bottom: 5px;
96-
padding: 2px 0;
96+
padding: 8px;
97+
display: flex;
98+
justify-content: space-between;
99+
align-items: center;
100+
background-color: #f0f0f0;
101+
border-radius: 4px;
102+
}
103+
104+
.participant-email {
105+
flex-grow: 1;
106+
}
107+
108+
.delete-btn {
109+
background-color: transparent;
110+
border: none;
111+
cursor: pointer;
112+
font-size: 18px;
113+
padding: 4px 8px;
114+
margin-left: 10px;
115+
transition: transform 0.2s;
116+
}
117+
118+
.delete-btn:hover {
119+
transform: scale(1.2);
120+
background-color: transparent;
97121
}
98122

99123
.no-participants {

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests package for the Mergington High School API"""

tests/test_app.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""
2+
Tests for the Mergington High School API
3+
"""
4+
5+
import pytest
6+
from fastapi.testclient import TestClient
7+
from src.app import app, activities
8+
9+
10+
@pytest.fixture
11+
def client():
12+
"""Create a test client for the FastAPI app"""
13+
return TestClient(app)
14+
15+
16+
@pytest.fixture(autouse=True)
17+
def reset_activities():
18+
"""Reset activities data before each test"""
19+
# Store original state
20+
original_activities = {}
21+
for name, data in activities.items():
22+
original_activities[name] = {
23+
"description": data["description"],
24+
"schedule": data["schedule"],
25+
"max_participants": data["max_participants"],
26+
"participants": data["participants"].copy()
27+
}
28+
29+
yield
30+
31+
# Restore original state
32+
for name, data in original_activities.items():
33+
activities[name] = data
34+
35+
36+
class TestRoot:
37+
"""Tests for the root endpoint"""
38+
39+
def test_root_redirects_to_static(self, client):
40+
"""Test that root redirects to static index.html"""
41+
response = client.get("/", follow_redirects=False)
42+
assert response.status_code == 307
43+
assert response.headers["location"] == "/static/index.html"
44+
45+
46+
class TestGetActivities:
47+
"""Tests for the GET /activities endpoint"""
48+
49+
def test_get_activities_returns_all_activities(self, client):
50+
"""Test that all activities are returned"""
51+
response = client.get("/activities")
52+
assert response.status_code == 200
53+
data = response.json()
54+
assert isinstance(data, dict)
55+
assert len(data) > 0
56+
assert "Chess Club" in data
57+
assert "Programming Class" in data
58+
assert "Gym Class" in data
59+
60+
def test_activities_have_required_fields(self, client):
61+
"""Test that each activity has all required fields"""
62+
response = client.get("/activities")
63+
data = response.json()
64+
65+
for activity_name, activity_data in data.items():
66+
assert "description" in activity_data
67+
assert "schedule" in activity_data
68+
assert "max_participants" in activity_data
69+
assert "participants" in activity_data
70+
assert isinstance(activity_data["participants"], list)
71+
assert isinstance(activity_data["max_participants"], int)
72+
73+
74+
class TestSignupForActivity:
75+
"""Tests for the POST /activities/{activity_name}/signup endpoint"""
76+
77+
def test_signup_for_existing_activity(self, client):
78+
"""Test successful signup for an activity"""
79+
response = client.post(
80+
"/activities/Chess Club/[email protected]"
81+
)
82+
assert response.status_code == 200
83+
data = response.json()
84+
assert "message" in data
85+
assert "[email protected]" in data["message"]
86+
assert "Chess Club" in data["message"]
87+
88+
# Verify the student was added
89+
activities_response = client.get("/activities")
90+
activities_data = activities_response.json()
91+
assert "[email protected]" in activities_data["Chess Club"]["participants"]
92+
93+
def test_signup_for_nonexistent_activity(self, client):
94+
"""Test signup for an activity that doesn't exist"""
95+
response = client.post(
96+
"/activities/Nonexistent Club/[email protected]"
97+
)
98+
assert response.status_code == 404
99+
data = response.json()
100+
assert "detail" in data
101+
assert data["detail"] == "Activity not found"
102+
103+
def test_signup_twice_for_same_activity(self, client):
104+
"""Test that a student cannot sign up twice for the same activity"""
105+
106+
107+
# First signup should succeed
108+
response1 = client.post(
109+
f"/activities/Chess Club/signup?email={email}"
110+
)
111+
assert response1.status_code == 200
112+
113+
# Second signup should fail
114+
response2 = client.post(
115+
f"/activities/Chess Club/signup?email={email}"
116+
)
117+
assert response2.status_code == 400
118+
data = response2.json()
119+
assert "detail" in data
120+
assert data["detail"] == "Student already signed up for this activity"
121+
122+
def test_signup_with_special_characters_in_activity_name(self, client):
123+
"""Test signup with URL-encoded activity name"""
124+
response = client.post(
125+
"/activities/Art%20Club/[email protected]"
126+
)
127+
assert response.status_code == 200
128+
129+
# Verify the student was added
130+
activities_response = client.get("/activities")
131+
activities_data = activities_response.json()
132+
assert "[email protected]" in activities_data["Art Club"]["participants"]
133+
134+
135+
class TestUnregisterFromActivity:
136+
"""Tests for the DELETE /activities/{activity_name}/unregister endpoint"""
137+
138+
def test_unregister_from_activity(self, client):
139+
"""Test successful unregistration from an activity"""
140+
141+
142+
# Verify student is initially registered (from default data)
143+
activities_response = client.get("/activities")
144+
activities_data = activities_response.json()
145+
assert email in activities_data["Chess Club"]["participants"]
146+
147+
# Unregister
148+
response = client.delete(
149+
f"/activities/Chess Club/unregister?email={email}"
150+
)
151+
assert response.status_code == 200
152+
data = response.json()
153+
assert "message" in data
154+
assert email in data["message"]
155+
assert "Chess Club" in data["message"]
156+
157+
# Verify student was removed
158+
activities_response = client.get("/activities")
159+
activities_data = activities_response.json()
160+
assert email not in activities_data["Chess Club"]["participants"]
161+
162+
def test_unregister_from_nonexistent_activity(self, client):
163+
"""Test unregistration from an activity that doesn't exist"""
164+
response = client.delete(
165+
"/activities/Nonexistent Club/[email protected]"
166+
)
167+
assert response.status_code == 404
168+
data = response.json()
169+
assert "detail" in data
170+
assert data["detail"] == "Activity not found"
171+
172+
def test_unregister_when_not_signed_up(self, client):
173+
"""Test unregistration when student is not signed up"""
174+
response = client.delete(
175+
"/activities/Chess Club/[email protected]"
176+
)
177+
assert response.status_code == 400
178+
data = response.json()
179+
assert "detail" in data
180+
assert data["detail"] == "Student not signed up for this activity"
181+
182+
def test_signup_after_unregister(self, client):
183+
"""Test that a student can re-signup after unregistering"""
184+
185+
186+
# Unregister
187+
response1 = client.delete(
188+
f"/activities/Chess Club/unregister?email={email}"
189+
)
190+
assert response1.status_code == 200
191+
192+
# Re-signup
193+
response2 = client.post(
194+
f"/activities/Chess Club/signup?email={email}"
195+
)
196+
assert response2.status_code == 200
197+
198+
# Verify student is registered again
199+
activities_response = client.get("/activities")
200+
activities_data = activities_response.json()
201+
assert email in activities_data["Chess Club"]["participants"]
202+
203+
204+
class TestEdgeCases:
205+
"""Tests for edge cases and error handling"""
206+
207+
def test_activity_name_case_sensitivity(self, client):
208+
"""Test that activity names are case-sensitive"""
209+
response = client.post(
210+
"/activities/chess club/[email protected]"
211+
)
212+
assert response.status_code == 404
213+
214+
def test_empty_email(self, client):
215+
"""Test signup with empty email"""
216+
response = client.post(
217+
"/activities/Chess Club/signup?email="
218+
)
219+
# FastAPI should handle this - either 422 for validation error or process it
220+
assert response.status_code in [200, 422]
221+
222+
def test_multiple_activities_for_same_student(self, client):
223+
"""Test that a student can sign up for multiple activities"""
224+
225+
226+
# Sign up for multiple activities
227+
response1 = client.post(f"/activities/Chess Club/signup?email={email}")
228+
assert response1.status_code == 200
229+
230+
response2 = client.post(f"/activities/Art Club/signup?email={email}")
231+
assert response2.status_code == 200
232+
233+
response3 = client.post(f"/activities/Drama Club/signup?email={email}")
234+
assert response3.status_code == 200
235+
236+
# Verify student is in all three activities
237+
activities_response = client.get("/activities")
238+
activities_data = activities_response.json()
239+
assert email in activities_data["Chess Club"]["participants"]
240+
assert email in activities_data["Art Club"]["participants"]
241+
assert email in activities_data["Drama Club"]["participants"]

0 commit comments

Comments
 (0)