Skip to content

Commit f199967

Browse files
committed
Implement activity unregistration feature and add tests
1 parent c8b8aa2 commit f199967

6 files changed

Lines changed: 131 additions & 2 deletions

File tree

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"python.testing.pytestArgs": [
3+
"tests"
4+
],
5+
"python.testing.unittestEnabled": false,
6+
"python.testing.pytestEnabled": true
7+
}

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+
requests

src/app.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,20 @@ 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 (email) 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+
activity = activities[activity_name]
118+
119+
# Validate student is currently signed up
120+
if email not in activity["participants"]:
121+
raise HTTPException(status_code=404, detail="Participant not found in activity")
122+
123+
activity["participants"].remove(email)
124+
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
@@ -8,6 +8,10 @@ document.addEventListener("DOMContentLoaded", () => {
88
async function fetchActivities() {
99
try {
1010
const response = await fetch("/activities");
11+
if (!response.ok) {
12+
const text = await response.text().catch(() => '');
13+
throw new Error(`Failed to fetch activities: ${response.status} ${text}`);
14+
}
1115
const activities = await response.json();
1216

1317
// Clear loading message
@@ -59,8 +63,28 @@ document.addEventListener("DOMContentLoaded", () => {
5963
nameSpan.className = "participant-name";
6064
nameSpan.textContent = p;
6165

66+
// Delete/unregister icon
67+
const deleteIcon = document.createElement('span');
68+
deleteIcon.className = 'delete-icon';
69+
deleteIcon.textContent = '🗑️';
70+
deleteIcon.onclick = async () => {
71+
try {
72+
const res = await fetch(`/activities/${encodeURIComponent(name)}/unregister?email=${encodeURIComponent(p)}`, { method: 'DELETE' });
73+
if (!res.ok) {
74+
const err = await res.json().catch(() => ({ detail: 'Unknown error' }));
75+
console.error('Unregister failed:', err);
76+
return;
77+
}
78+
// Refresh activities list after successful unregister
79+
fetchActivities();
80+
} catch (e) {
81+
console.error('Network error while unregistering:', e);
82+
}
83+
};
84+
6285
li.appendChild(avatar);
6386
li.appendChild(nameSpan);
87+
li.appendChild(deleteIcon);
6488
ul.appendChild(li);
6589
});
6690

@@ -101,12 +125,14 @@ document.addEventListener("DOMContentLoaded", () => {
101125
}
102126
);
103127

104-
const result = await response.json();
128+
const result = await response.json().catch(() => ({}));
105129

106130
if (response.ok) {
107-
messageDiv.textContent = result.message;
131+
messageDiv.textContent = result.message || "Signed up successfully";
108132
messageDiv.className = "success";
109133
signupForm.reset();
134+
// Refresh activities to show the new participant immediately
135+
fetchActivities();
110136
} else {
111137
messageDiv.textContent = result.detail || "An error occurred";
112138
messageDiv.className = "error";

src/static/styles.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
}
77

88
body {
9+
10+
/* Add CSS to hide bullet points for participants list */
11+
.participants-list {
12+
list-style-type: none;
13+
padding: 0;
14+
}
15+
16+
.delete-icon {
17+
cursor: pointer;
18+
margin-left: 10px;
19+
color: red;
20+
}
921
font-family: Arial, sans-serif;
1022
line-height: 1.6;
1123
color: #333;

tests/test_app.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import copy
2+
from fastapi.testclient import TestClient
3+
import pytest
4+
5+
from src.app import app, activities
6+
7+
client = TestClient(app)
8+
9+
@pytest.fixture(autouse=True)
10+
def reset_activities():
11+
# Make a deep copy of the activities before each test and restore after
12+
original = copy.deepcopy(activities)
13+
yield
14+
activities.clear()
15+
activities.update(copy.deepcopy(original))
16+
17+
18+
def test_get_activities():
19+
res = client.get("/activities")
20+
assert res.status_code == 200
21+
data = res.json()
22+
assert isinstance(data, dict)
23+
assert "Football Club" in data
24+
assert isinstance(data["Football Club"]["participants"], list)
25+
26+
27+
def test_signup_and_unregister_flow():
28+
email = "temp@example.com"
29+
activity = "Football Club"
30+
31+
# Sign up
32+
res = client.post(f"/activities/{activity}/signup?email={email}")
33+
assert res.status_code == 200
34+
assert "Signed up" in res.json().get("message", "")
35+
36+
# Participant should be in the activity
37+
assert email in activities[activity]["participants"]
38+
39+
# Unregister
40+
res = client.delete(f"/activities/{activity}/unregister?email={email}")
41+
assert res.status_code == 200
42+
assert "Unregistered" in res.json().get("message", "")
43+
44+
# Participant should be removed
45+
assert email not in activities[activity]["participants"]
46+
47+
48+
def test_signup_existing():
49+
activity = "Football Club"
50+
email = "alex@mergington.edu" # already present
51+
res = client.post(f"/activities/{activity}/signup?email={email}")
52+
assert res.status_code == 400
53+
assert "already signed up" in res.json().get("detail", "").lower()
54+
55+
56+
def test_unregister_not_found():
57+
activity = "Football Club"
58+
email = "notfound@example.com"
59+
60+
# Ensure the participant is not present
61+
if email in activities[activity]["participants"]:
62+
activities[activity]["participants"].remove(email)
63+
64+
res = client.delete(f"/activities/{activity}/unregister?email={email}")
65+
assert res.status_code == 404

0 commit comments

Comments
 (0)