Skip to content

Commit d59ea45

Browse files
committed
feat: implement unregister functionality for extracurricular activities and add corresponding tests
1 parent 0ee5192 commit d59ea45

File tree

5 files changed

+140
-9
lines changed

5 files changed

+140
-9
lines changed

requirements.txt

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

src/app.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
"""
23
High School Management System API
34
@@ -107,3 +108,15 @@ def signup_for_activity(activity_name: str, email: str):
107108
# Add student
108109
activity["participants"].append(email)
109110
return {"message": f"Signed up {email} for {activity_name}"}
111+
112+
113+
@app.post("/activities/{activity_name}/unregister")
114+
def unregister_from_activity(activity_name: str, email: str):
115+
"""Remove a student from an activity"""
116+
if activity_name not in activities:
117+
raise HTTPException(status_code=404, detail="Activity not found")
118+
activity = activities[activity_name]
119+
if email not in activity["participants"]:
120+
raise HTTPException(status_code=400, detail="Student is not registered for this activity")
121+
activity["participants"].remove(email)
122+
return {"message": f"Removed {email} from {activity_name}"}

src/static/app.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ document.addEventListener("DOMContentLoaded", () => {
7474
participantsDiv.appendChild(none);
7575
} else {
7676
const list = document.createElement("ul");
77+
list.style.listStyle = "none";
78+
list.style.padding = "0";
7779
participants.forEach((p) => {
7880
const { display, initials } = nameFromIdentifier(p);
7981
const li = document.createElement("li");
@@ -84,6 +86,25 @@ document.addEventListener("DOMContentLoaded", () => {
8486

8587
li.appendChild(span);
8688
li.appendChild(document.createTextNode(" " + display));
89+
90+
// Add delete icon
91+
const delBtn = document.createElement("button");
92+
delBtn.className = "delete-participant";
93+
delBtn.title = "Remove participant";
94+
delBtn.innerHTML = "🗑"; // trash can icon
95+
delBtn.style.marginLeft = "8px";
96+
delBtn.style.background = "none";
97+
delBtn.style.border = "none";
98+
delBtn.style.cursor = "pointer";
99+
delBtn.style.color = "#c62828";
100+
delBtn.style.fontSize = "15px";
101+
delBtn.setAttribute("aria-label", `Remove ${display}`);
102+
delBtn.addEventListener("click", (e) => {
103+
e.stopPropagation();
104+
unregisterParticipant(name, p);
105+
});
106+
li.appendChild(delBtn);
107+
87108
list.appendChild(li);
88109
});
89110
participantsDiv.appendChild(list);
@@ -148,4 +169,30 @@ document.addEventListener("DOMContentLoaded", () => {
148169

149170
// Initialize app
150171
fetchActivities();
172+
// Unregister participant function
173+
async function unregisterParticipant(activityName, participantId) {
174+
if (!confirm("Are you sure you want to remove this participant?")) return;
175+
try {
176+
const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(participantId)}`, {
177+
method: "POST"
178+
});
179+
const result = await response.json();
180+
if (response.ok) {
181+
messageDiv.textContent = result.message || "Participant removed.";
182+
messageDiv.className = "success";
183+
await fetchActivities();
184+
} else {
185+
messageDiv.textContent = result.detail || "Failed to remove participant.";
186+
messageDiv.className = "error";
187+
}
188+
messageDiv.classList.remove("hidden");
189+
setTimeout(() => messageDiv.classList.add("hidden"), 5000);
190+
} catch (error) {
191+
messageDiv.textContent = "Error removing participant.";
192+
messageDiv.className = "error";
193+
messageDiv.classList.remove("hidden");
194+
setTimeout(() => messageDiv.classList.add("hidden"), 5000);
195+
console.error("Error unregistering participant:", error);
196+
}
197+
}
151198
});

src/static/styles.css

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,35 @@ section h3 {
7575
margin-bottom: 8px;
7676
}
7777

78-
.activity-card .participants {
79-
margin-top: 12px;
80-
padding-top: 10px;
81-
border-top: 1px dashed #eee;
78+
.activity-card .participants ul {
79+
.activity-card .participants ul {
80+
display: flex;
81+
flex-wrap: wrap;
82+
gap: 8px;
83+
padding: 0;
84+
margin: 0;
85+
list-style: none;
86+
}
87+
.activity-card .participants li {
88+
position: relative;
8289
}
8390

84-
.activity-card .participants h5 {
85-
margin: 0 0 8px;
86-
font-size: 13px;
87-
color: #1a237e;
88-
font-weight: 600;
91+
92+
button.delete-participant {
93+
margin-left: 8px;
94+
background: none;
95+
border: none;
96+
cursor: pointer;
97+
color: #c62828;
98+
font-size: 15px;
99+
padding: 0 2px;
100+
vertical-align: middle;
101+
}
102+
button.delete-participant:hover {
103+
color: #a31515;
104+
}
105+
.activity-card .participants ul li::before {
106+
display: none !important;
89107
}
90108

91109
.activity-card .participants ul {

tests/test_api.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from fastapi.testclient import TestClient
2+
from src.app import app, activities
3+
4+
5+
client = TestClient(app)
6+
7+
8+
def test_get_activities():
9+
resp = client.get("/activities")
10+
assert resp.status_code == 200
11+
data = resp.json()
12+
assert isinstance(data, dict)
13+
# Ensure a known activity is present
14+
assert "Chess Club" in data
15+
16+
17+
def test_signup_and_unregister_flow():
18+
activity = "Chess Club"
19+
20+
21+
# Ensure clean state: remove if already present
22+
if email in activities[activity]["participants"]:
23+
activities[activity]["participants"].remove(email)
24+
25+
# Sign up
26+
resp = client.post(f"/activities/{activity}/signup", params={"email": email})
27+
assert resp.status_code == 200
28+
assert email in activities[activity]["participants"]
29+
30+
# Sign up again -> should fail (already signed up)
31+
resp2 = client.post(f"/activities/{activity}/signup", params={"email": email})
32+
assert resp2.status_code == 400
33+
34+
# Unregister
35+
resp3 = client.post(f"/activities/{activity}/unregister", params={"email": email})
36+
assert resp3.status_code == 200
37+
assert email not in activities[activity]["participants"]
38+
39+
40+
def test_unregister_nonexistent():
41+
activity = "Chess Club"
42+
43+
44+
# Ensure not registered
45+
if email in activities[activity]["participants"]:
46+
activities[activity]["participants"].remove(email)
47+
48+
resp = client.post(f"/activities/{activity}/unregister", params={"email": email})
49+
assert resp.status_code == 400

0 commit comments

Comments
 (0)