Skip to content

Commit cc4232f

Browse files
Implement participant unregistration feature and update UI for activity cards
1 parent 5e5d65f commit cc4232f

5 files changed

Lines changed: 152 additions & 22 deletions

File tree

requirements.txt

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

src/app.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,18 @@ def signup_for_activity(activity_name: str, email: str):
108108
# Add student
109109
activity["participants"].append(email)
110110
return {"message": f"Signed up {email} for {activity_name}"}
111+
112+
113+
# Unregister a participant from an activity
114+
from fastapi import Query
115+
116+
@app.delete("/activities/{activity_name}/unregister")
117+
def unregister_from_activity(activity_name: str, email: str = Query(...)):
118+
"""Remove a participant from an activity"""
119+
if activity_name not in activities:
120+
raise HTTPException(status_code=404, detail="Activity not found")
121+
activity = activities[activity_name]
122+
if email not in activity["participants"]:
123+
raise HTTPException(status_code=400, detail="Participant not found in this activity")
124+
activity["participants"].remove(email)
125+
return {"message": f"Unregistered {email} from {activity_name}"}

src/static/app.js

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,29 @@ document.addEventListener("DOMContentLoaded", () => {
2222

2323

2424
// Create participants list HTML
25-
let participantsHTML = '';
26-
if (details.participants.length > 0) {
27-
participantsHTML = `
28-
<div class="participants-section">
29-
<strong>Participants:</strong>
30-
<ul>
31-
${details.participants.map(p => `<li>${p}</li>`).join('')}
32-
</ul>
33-
</div>
34-
`;
35-
} else {
36-
participantsHTML = `
37-
<div class="participants-section empty">
38-
<strong>Participants:</strong>
39-
<p>No one has signed up yet.</p>
40-
</div>
41-
`;
42-
}
25+
let participantsHTML = '';
26+
if (details.participants.length > 0) {
27+
participantsHTML = `
28+
<div class="participants-section">
29+
<strong>Participants:</strong>
30+
<ul class="participants-list">
31+
${details.participants.map(p => `
32+
<li class="participant-item">
33+
<span class="participant-email">${p}</span>
34+
<span class="delete-participant" title="Unregister" data-activity="${name}" data-email="${p}">&#128465;</span>
35+
</li>
36+
`).join('')}
37+
</ul>
38+
</div>
39+
`;
40+
} else {
41+
participantsHTML = `
42+
<div class="participants-section empty">
43+
<strong>Participants:</strong>
44+
<p>No one has signed up yet.</p>
45+
</div>
46+
`;
47+
}
4348

4449
activityCard.innerHTML = `
4550
<h4>${name}</h4>
@@ -51,6 +56,33 @@ document.addEventListener("DOMContentLoaded", () => {
5156

5257
activitiesList.appendChild(activityCard);
5358

59+
// Add event listeners for delete icons
60+
activityCard.querySelectorAll('.delete-participant').forEach(icon => {
61+
icon.addEventListener('click', async (e) => {
62+
const activityName = icon.getAttribute('data-activity');
63+
const email = icon.getAttribute('data-email');
64+
if (confirm(`Unregister ${email} from ${activityName}?`)) {
65+
try {
66+
const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`, {
67+
method: 'DELETE',
68+
});
69+
const result = await response.json();
70+
if (response.ok) {
71+
fetchActivities(); // Refresh list
72+
messageDiv.textContent = result.message;
73+
messageDiv.style.color = 'green';
74+
} else {
75+
messageDiv.textContent = result.detail || 'Failed to unregister.';
76+
messageDiv.style.color = 'red';
77+
}
78+
} catch (err) {
79+
messageDiv.textContent = 'Error unregistering participant.';
80+
messageDiv.style.color = 'red';
81+
}
82+
}
83+
});
84+
});
85+
5486
// Add option to select dropdown
5587
const option = document.createElement("option");
5688
option.value = name;

src/static/styles.css

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,38 @@ section h3 {
9191
margin-bottom: 6px;
9292
}
9393

94-
.participants-section ul {
95-
list-style-type: disc;
96-
margin-left: 20px;
94+
95+
.participants-list {
96+
list-style-type: none;
97+
margin-left: 0;
9798
margin-bottom: 0;
99+
padding-left: 0;
98100
}
99101

100-
.participants-section li {
102+
.participant-item {
103+
display: flex;
104+
align-items: center;
101105
margin-bottom: 4px;
102106
color: #333;
103107
font-size: 14px;
104108
}
105109

110+
.participant-email {
111+
flex: 1;
112+
}
113+
114+
.delete-participant {
115+
cursor: pointer;
116+
color: #d32f2f;
117+
margin-left: 10px;
118+
font-size: 16px;
119+
transition: color 0.2s;
120+
}
121+
.delete-participant:hover {
122+
color: #b71c1c;
123+
text-shadow: 0 0 2px #d32f2f;
124+
}
125+
106126
.participants-section.empty {
107127
background: #fffde7;
108128
border: 1px solid #ffe082;

tests/test_app.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from src.app import app
4+
5+
client = TestClient(app)
6+
7+
# Test GET /activities
8+
def test_get_activities():
9+
response = client.get("/activities")
10+
assert response.status_code == 200
11+
data = response.json()
12+
assert "Chess Club" in data
13+
assert "Programming Class" in data
14+
assert "Gym Class" in data
15+
16+
# Test POST /activities/{activity_name}/signup
17+
@pytest.mark.parametrize("activity,email", [
18+
("Chess Club", "newstudent@mergington.edu"),
19+
("Programming Class", "newcoder@mergington.edu"),
20+
])
21+
def test_signup_for_activity(activity, email):
22+
response = client.post(f"/activities/{activity}/signup?email={email}")
23+
assert response.status_code == 200
24+
assert f"Signed up {email} for {activity}" in response.json()["message"]
25+
26+
# Test duplicate signup
27+
def test_duplicate_signup():
28+
activity = "Chess Club"
29+
email = "michael@mergington.edu"
30+
response = client.post(f"/activities/{activity}/signup?email={email}")
31+
assert response.status_code == 400
32+
assert "already signed up" in response.json()["detail"]
33+
34+
# Test DELETE /activities/{activity_name}/unregister
35+
@pytest.mark.parametrize("activity,email", [
36+
("Chess Club", "daniel@mergington.edu"),
37+
("Programming Class", "emma@mergington.edu"),
38+
])
39+
def test_unregister_from_activity(activity, email):
40+
response = client.delete(f"/activities/{activity}/unregister?email={email}")
41+
assert response.status_code == 200
42+
assert f"Unregistered {email} from {activity}" in response.json()["message"]
43+
44+
# Test unregister non-existent participant
45+
def test_unregister_nonexistent():
46+
activity = "Chess Club"
47+
email = "notfound@mergington.edu"
48+
response = client.delete(f"/activities/{activity}/unregister?email={email}")
49+
assert response.status_code == 400
50+
assert "Participant not found" in response.json()["detail"]
51+
52+
# Test activity not found
53+
@pytest.mark.parametrize("endpoint", [
54+
"/activities/Unknown/signup?email=test@mergington.edu",
55+
"/activities/Unknown/unregister?email=test@mergington.edu",
56+
])
57+
def test_activity_not_found(endpoint):
58+
method = "post" if "signup" in endpoint else "delete"
59+
response = getattr(client, method)(endpoint)
60+
assert response.status_code == 404
61+
assert "Activity not found" in response.json()["detail"]

0 commit comments

Comments
 (0)