Skip to content

Commit 2178c07

Browse files
Añadir funcionalidad para eliminar participantes de actividades y pruebas correspondientes
1 parent 792d423 commit 2178c07

6 files changed

Lines changed: 336 additions & 3 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
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}/participants/{email}")
111+
def remove_participant(activity_name: str, email: str):
112+
"""Remove a participant 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 is not signed up for this activity")
123+
124+
# Remove student
125+
activity["participants"].remove(email)
126+
return {"message": f"Removed {email} from {activity_name}"}

src/static/app.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ document.addEventListener("DOMContentLoaded", () => {
2828
<div class="participants">
2929
<h5>Participants (${details.participants.length})</h5>
3030
<ul>
31-
${details.participants.map(participant => `<li>${participant}</li>`).join('')}
31+
${details.participants.map(participant => `
32+
<li>
33+
<span>${participant}</span>
34+
<button class="delete-btn" data-activity="${name}" data-email="${participant}" title="Remove participant">🗑️</button>
35+
</li>
36+
`).join('')}
3237
</ul>
3338
</div>
3439
`;
@@ -68,6 +73,8 @@ document.addEventListener("DOMContentLoaded", () => {
6873
messageDiv.textContent = result.message;
6974
messageDiv.className = "success";
7075
signupForm.reset();
76+
// Reload activities to show the new participant
77+
fetchActivities();
7178
} else {
7279
messageDiv.textContent = result.detail || "An error occurred";
7380
messageDiv.className = "error";
@@ -87,6 +94,48 @@ document.addEventListener("DOMContentLoaded", () => {
8794
}
8895
});
8996

97+
// Handle delete button clicks
98+
activitiesList.addEventListener("click", async (event) => {
99+
if (event.target.classList.contains("delete-btn")) {
100+
const button = event.target;
101+
const activity = button.getAttribute("data-activity");
102+
const email = button.getAttribute("data-email");
103+
104+
try {
105+
const response = await fetch(
106+
`/activities/${encodeURIComponent(activity)}/participants/${encodeURIComponent(email)}`,
107+
{
108+
method: "DELETE",
109+
}
110+
);
111+
112+
const result = await response.json();
113+
114+
if (response.ok) {
115+
messageDiv.textContent = result.message;
116+
messageDiv.className = "success";
117+
// Reload activities
118+
fetchActivities();
119+
} else {
120+
messageDiv.textContent = result.detail || "Failed to remove participant";
121+
messageDiv.className = "error";
122+
}
123+
124+
messageDiv.classList.remove("hidden");
125+
126+
// Hide message after 5 seconds
127+
setTimeout(() => {
128+
messageDiv.classList.add("hidden");
129+
}, 5000);
130+
} catch (error) {
131+
messageDiv.textContent = "Failed to remove participant. Please try again.";
132+
messageDiv.className = "error";
133+
messageDiv.classList.remove("hidden");
134+
console.error("Error removing participant:", error);
135+
}
136+
}
137+
});
138+
90139
// Initialize app
91140
fetchActivities();
92141
});

src/static/styles.css

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,36 @@ section h3 {
8888
}
8989

9090
.activity-card .participants ul {
91-
margin-left: 20px;
92-
list-style-type: disc;
91+
margin-left: 0;
92+
list-style-type: none;
9393
}
9494

9595
.activity-card .participants li {
9696
margin-bottom: 6px;
9797
color: #555;
9898
font-size: 14px;
99+
display: flex;
100+
align-items: center;
101+
justify-content: space-between;
102+
}
103+
104+
.activity-card .participants li span {
105+
flex: 1;
106+
}
107+
108+
.delete-btn {
109+
background-color: transparent;
110+
border: none;
111+
font-size: 18px;
112+
cursor: pointer;
113+
padding: 4px 8px;
114+
margin-left: 10px;
115+
transition: transform 0.2s;
116+
}
117+
118+
.delete-btn:hover {
119+
background-color: transparent;
120+
transform: scale(1.2);
99121
}
100122

101123
.form-group {

tests/__init__.py

Whitespace-only changes.

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 Activities API
3+
"""
4+
5+
import pytest
6+
from fastapi.testclient import TestClient
7+
import sys
8+
from pathlib import Path
9+
10+
# Add src directory to path so we can import app
11+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
12+
13+
from app import app
14+
15+
client = TestClient(app)
16+
17+
18+
@pytest.fixture
19+
def reset_activities():
20+
"""Reset activities to initial state before each test"""
21+
from app import activities
22+
23+
# Store original state
24+
original_activities = {
25+
"Chess Club": {
26+
"description": "Learn strategies and compete in chess tournaments",
27+
"schedule": "Fridays, 3:30 PM - 5:00 PM",
28+
"max_participants": 12,
29+
"participants": ["michael@mergington.edu", "daniel@mergington.edu"]
30+
},
31+
"Programming Class": {
32+
"description": "Learn programming fundamentals and build software projects",
33+
"schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM",
34+
"max_participants": 20,
35+
"participants": ["emma@mergington.edu", "sophia@mergington.edu"]
36+
},
37+
"Gym Class": {
38+
"description": "Physical education and sports activities",
39+
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
40+
"max_participants": 30,
41+
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
42+
},
43+
"Basketball": {
44+
"description": "Team sport focusing on basketball skills and competitive play",
45+
"schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM",
46+
"max_participants": 15,
47+
"participants": ["alex@mergington.edu"]
48+
},
49+
"Tennis Club": {
50+
"description": "Learn tennis techniques and participate in matches",
51+
"schedule": "Saturdays, 10:00 AM - 11:30 AM",
52+
"max_participants": 10,
53+
"participants": ["james@mergington.edu"]
54+
},
55+
"Drama Club": {
56+
"description": "Perform in theatrical productions and develop acting skills",
57+
"schedule": "Wednesdays and Thursdays, 4:00 PM - 5:30 PM",
58+
"max_participants": 25,
59+
"participants": ["isabella@mergington.edu", "lucas@mergington.edu"]
60+
},
61+
"Art Studio": {
62+
"description": "Create visual art including painting, drawing, and sculpture",
63+
"schedule": "Tuesdays and Fridays, 3:30 PM - 5:00 PM",
64+
"max_participants": 18,
65+
"participants": ["sophie@mergington.edu"]
66+
},
67+
"Debate Team": {
68+
"description": "Develop public speaking and argumentation skills",
69+
"schedule": "Mondays and Thursdays, 3:30 PM - 4:45 PM",
70+
"max_participants": 16,
71+
"participants": ["noah@mergington.edu", "ava@mergington.edu"]
72+
},
73+
"Science Club": {
74+
"description": "Explore scientific concepts through experiments and projects",
75+
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
76+
"max_participants": 20,
77+
"participants": ["ethan@mergington.edu"]
78+
}
79+
}
80+
81+
# Clear and reset
82+
activities.clear()
83+
activities.update(original_activities)
84+
85+
yield
86+
87+
# Restore after test
88+
activities.clear()
89+
activities.update(original_activities)
90+
91+
92+
class TestGetActivities:
93+
"""Tests for GET /activities endpoint"""
94+
95+
def test_get_activities_returns_all_activities(self, reset_activities):
96+
"""Test that GET /activities returns all activities"""
97+
response = client.get("/activities")
98+
assert response.status_code == 200
99+
100+
data = response.json()
101+
assert "Chess Club" in data
102+
assert "Programming Class" in data
103+
assert len(data) == 9
104+
105+
def test_activities_have_required_fields(self, reset_activities):
106+
"""Test that each activity has required fields"""
107+
response = client.get("/activities")
108+
data = response.json()
109+
110+
for activity_name, activity_data in data.items():
111+
assert "description" in activity_data
112+
assert "schedule" in activity_data
113+
assert "max_participants" in activity_data
114+
assert "participants" in activity_data
115+
116+
def test_activities_have_participants(self, reset_activities):
117+
"""Test that activities contain participants"""
118+
response = client.get("/activities")
119+
data = response.json()
120+
121+
assert len(data["Chess Club"]["participants"]) == 2
122+
assert "michael@mergington.edu" in data["Chess Club"]["participants"]
123+
124+
125+
class TestSignupForActivity:
126+
"""Tests for POST /activities/{activity_name}/signup endpoint"""
127+
128+
def test_successful_signup(self, reset_activities):
129+
"""Test successfully signing up for an activity"""
130+
response = client.post(
131+
"/activities/Chess%20Club/signup?email=newstudent@mergington.edu",
132+
headers={"Content-Type": "application/json"}
133+
)
134+
assert response.status_code == 200
135+
data = response.json()
136+
assert "Signed up" in data["message"]
137+
assert "newstudent@mergington.edu" in data["message"]
138+
139+
def test_signup_adds_participant(self, reset_activities):
140+
"""Test that signup actually adds the participant"""
141+
client.post(
142+
"/activities/Basketball/signup?email=newplayer@mergington.edu",
143+
headers={"Content-Type": "application/json"}
144+
)
145+
146+
response = client.get("/activities")
147+
data = response.json()
148+
assert "newplayer@mergington.edu" in data["Basketball"]["participants"]
149+
150+
def test_signup_for_nonexistent_activity(self, reset_activities):
151+
"""Test signing up for a nonexistent activity"""
152+
response = client.post(
153+
"/activities/Nonexistent%20Club/signup?email=student@mergington.edu",
154+
headers={"Content-Type": "application/json"}
155+
)
156+
assert response.status_code == 404
157+
data = response.json()
158+
assert "Activity not found" in data["detail"]
159+
160+
def test_signup_duplicate_email(self, reset_activities):
161+
"""Test signing up with an email already registered"""
162+
response = client.post(
163+
"/activities/Chess%20Club/signup?email=michael@mergington.edu",
164+
headers={"Content-Type": "application/json"}
165+
)
166+
assert response.status_code == 400
167+
data = response.json()
168+
assert "already signed up" in data["detail"]
169+
170+
def test_multiple_signups(self, reset_activities):
171+
"""Test multiple students signing up"""
172+
client.post(
173+
"/activities/Tennis%20Club/signup?email=student1@mergington.edu",
174+
headers={"Content-Type": "application/json"}
175+
)
176+
client.post(
177+
"/activities/Tennis%20Club/signup?email=student2@mergington.edu",
178+
headers={"Content-Type": "application/json"}
179+
)
180+
181+
response = client.get("/activities")
182+
data = response.json()
183+
assert len(data["Tennis Club"]["participants"]) == 3
184+
assert "student1@mergington.edu" in data["Tennis Club"]["participants"]
185+
assert "student2@mergington.edu" in data["Tennis Club"]["participants"]
186+
187+
188+
class TestRemoveParticipant:
189+
"""Tests for DELETE /activities/{activity_name}/participants/{email} endpoint"""
190+
191+
def test_successful_removal(self, reset_activities):
192+
"""Test successfully removing a participant"""
193+
response = client.delete(
194+
"/activities/Chess%20Club/participants/michael@mergington.edu"
195+
)
196+
assert response.status_code == 200
197+
data = response.json()
198+
assert "Removed" in data["message"]
199+
assert "michael@mergington.edu" in data["message"]
200+
201+
def test_removal_updates_participants_list(self, reset_activities):
202+
"""Test that removal actually removes the participant"""
203+
client.delete(
204+
"/activities/Drama%20Club/participants/isabella@mergington.edu"
205+
)
206+
207+
response = client.get("/activities")
208+
data = response.json()
209+
assert "isabella@mergington.edu" not in data["Drama Club"]["participants"]
210+
assert len(data["Drama Club"]["participants"]) == 1
211+
212+
def test_remove_from_nonexistent_activity(self, reset_activities):
213+
"""Test removing from a nonexistent activity"""
214+
response = client.delete(
215+
"/activities/Nonexistent%20Club/participants/student@mergington.edu"
216+
)
217+
assert response.status_code == 404
218+
data = response.json()
219+
assert "Activity not found" in data["detail"]
220+
221+
def test_remove_nonexistent_participant(self, reset_activities):
222+
"""Test removing a participant who's not signed up"""
223+
response = client.delete(
224+
"/activities/Chess%20Club/participants/notregistered@mergington.edu"
225+
)
226+
assert response.status_code == 400
227+
data = response.json()
228+
assert "not signed up" in data["detail"]
229+
230+
def test_remove_multiple_participants(self, reset_activities):
231+
"""Test removing multiple participants"""
232+
client.delete(
233+
"/activities/Debate%20Team/participants/noah@mergington.edu"
234+
)
235+
client.delete(
236+
"/activities/Debate%20Team/participants/ava@mergington.edu"
237+
)
238+
239+
response = client.get("/activities")
240+
data = response.json()
241+
assert len(data["Debate Team"]["participants"]) == 0

0 commit comments

Comments
 (0)