From c1560752f436aa7ec872d0aa53dc3f6fd9773898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20Palacios=20P=C3=A9rez?= Date: Tue, 3 Feb 2026 12:44:12 +0000 Subject: [PATCH 1/3] =?UTF-8?q?A=C3=B1adir=20nuevas=20actividades=20extrac?= =?UTF-8?q?urriculares=20y=20validaci=C3=B3n=20de=20inscripci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/app.py b/src/app.py index 4ebb1d9..3a3b5cd 100644 --- a/src/app.py +++ b/src/app.py @@ -38,6 +38,42 @@ "schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM", "max_participants": 30, "participants": ["john@mergington.edu", "olivia@mergington.edu"] + }, + "Basketball": { + "description": "Team sport focusing on basketball skills and competitive play", + "schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM", + "max_participants": 15, + "participants": ["alex@mergington.edu"] + }, + "Tennis Club": { + "description": "Learn tennis techniques and participate in matches", + "schedule": "Saturdays, 10:00 AM - 11:30 AM", + "max_participants": 10, + "participants": ["james@mergington.edu"] + }, + "Drama Club": { + "description": "Perform in theatrical productions and develop acting skills", + "schedule": "Wednesdays and Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": ["isabella@mergington.edu", "lucas@mergington.edu"] + }, + "Art Studio": { + "description": "Create visual art including painting, drawing, and sculpture", + "schedule": "Tuesdays and Fridays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["sophie@mergington.edu"] + }, + "Debate Team": { + "description": "Develop public speaking and argumentation skills", + "schedule": "Mondays and Thursdays, 3:30 PM - 4:45 PM", + "max_participants": 16, + "participants": ["noah@mergington.edu", "ava@mergington.edu"] + }, + "Science Club": { + "description": "Explore scientific concepts through experiments and projects", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 20, + "participants": ["ethan@mergington.edu"] } } @@ -62,6 +98,10 @@ def signup_for_activity(activity_name: str, email: str): # Get the specific activity activity = activities[activity_name] + # Validate student is not already signed up + if email in activity["participants"]: + raise HTTPException(status_code=400, detail="Student already signed up for this activity") + # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} From 792d423cf6ac36a44fb4f995e2502cb0ed1c2a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20Palacios=20P=C3=A9rez?= Date: Tue, 3 Feb 2026 12:50:55 +0000 Subject: [PATCH 2/3] =?UTF-8?q?A=C3=B1adir=20lista=20de=20participantes=20?= =?UTF-8?q?a=20las=20tarjetas=20de=20actividades=20y=20estilos=20correspon?= =?UTF-8?q?dientes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/static/app.js | 6 ++++++ src/static/styles.css | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..9b4d42c 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -25,6 +25,12 @@ document.addEventListener("DOMContentLoaded", () => {

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+
+
Participants (${details.participants.length})
+
    + ${details.participants.map(participant => `
  • ${participant}
  • `).join('')} +
+
`; activitiesList.appendChild(activityCard); diff --git a/src/static/styles.css b/src/static/styles.css index a533b32..9b084bb 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,30 @@ section h3 { margin-bottom: 8px; } +.activity-card .participants { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e0e0e0; +} + +.activity-card .participants h5 { + margin-bottom: 10px; + color: #1a237e; + font-size: 14px; + font-weight: bold; +} + +.activity-card .participants ul { + margin-left: 20px; + list-style-type: disc; +} + +.activity-card .participants li { + margin-bottom: 6px; + color: #555; + font-size: 14px; +} + .form-group { margin-bottom: 15px; } From 2178c074aa34eda7a3a935ffca7d2f337522a346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20Palacios=20P=C3=A9rez?= Date: Tue, 3 Feb 2026 12:59:18 +0000 Subject: [PATCH 3/3] =?UTF-8?q?A=C3=B1adir=20funcionalidad=20para=20elimin?= =?UTF-8?q?ar=20participantes=20de=20actividades=20y=20pruebas=20correspon?= =?UTF-8?q?dientes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 + src/app.py | 19 ++++ src/static/app.js | 51 ++++++++- src/static/styles.css | 26 ++++- tests/__init__.py | 0 tests/test_app.py | 241 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_app.py diff --git a/requirements.txt b/requirements.txt index 97dc7cd..2522ad0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ fastapi uvicorn +pytest +httpx diff --git a/src/app.py b/src/app.py index 3a3b5cd..a913c19 100644 --- a/src/app.py +++ b/src/app.py @@ -105,3 +105,22 @@ def signup_for_activity(activity_name: str, email: str): # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} + + +@app.delete("/activities/{activity_name}/participants/{email}") +def remove_participant(activity_name: str, email: str): + """Remove a participant from an activity""" + # Validate activity exists + if activity_name not in activities: + raise HTTPException(status_code=404, detail="Activity not found") + + # Get the specific activity + activity = activities[activity_name] + + # Validate student is signed up + if email not in activity["participants"]: + raise HTTPException(status_code=400, detail="Student is not signed up for this activity") + + # Remove student + activity["participants"].remove(email) + return {"message": f"Removed {email} from {activity_name}"} diff --git a/src/static/app.js b/src/static/app.js index 9b4d42c..9b5f7d7 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -28,7 +28,12 @@ document.addEventListener("DOMContentLoaded", () => {
Participants (${details.participants.length})
    - ${details.participants.map(participant => `
  • ${participant}
  • `).join('')} + ${details.participants.map(participant => ` +
  • + ${participant} + +
  • + `).join('')}
`; @@ -68,6 +73,8 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + // Reload activities to show the new participant + fetchActivities(); } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; @@ -87,6 +94,48 @@ document.addEventListener("DOMContentLoaded", () => { } }); + // Handle delete button clicks + activitiesList.addEventListener("click", async (event) => { + if (event.target.classList.contains("delete-btn")) { + const button = event.target; + const activity = button.getAttribute("data-activity"); + const email = button.getAttribute("data-email"); + + try { + const response = await fetch( + `/activities/${encodeURIComponent(activity)}/participants/${encodeURIComponent(email)}`, + { + method: "DELETE", + } + ); + + const result = await response.json(); + + if (response.ok) { + messageDiv.textContent = result.message; + messageDiv.className = "success"; + // Reload activities + fetchActivities(); + } else { + messageDiv.textContent = result.detail || "Failed to remove participant"; + messageDiv.className = "error"; + } + + messageDiv.classList.remove("hidden"); + + // Hide message after 5 seconds + setTimeout(() => { + messageDiv.classList.add("hidden"); + }, 5000); + } catch (error) { + messageDiv.textContent = "Failed to remove participant. Please try again."; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + console.error("Error removing participant:", error); + } + } + }); + // Initialize app fetchActivities(); }); diff --git a/src/static/styles.css b/src/static/styles.css index 9b084bb..f66d90f 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -88,14 +88,36 @@ section h3 { } .activity-card .participants ul { - margin-left: 20px; - list-style-type: disc; + margin-left: 0; + list-style-type: none; } .activity-card .participants li { margin-bottom: 6px; color: #555; font-size: 14px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.activity-card .participants li span { + flex: 1; +} + +.delete-btn { + background-color: transparent; + border: none; + font-size: 18px; + cursor: pointer; + padding: 4px 8px; + margin-left: 10px; + transition: transform 0.2s; +} + +.delete-btn:hover { + background-color: transparent; + transform: scale(1.2); } .form-group { diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..e580fa3 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,241 @@ +""" +Tests for the Mergington High School Activities API +""" + +import pytest +from fastapi.testclient import TestClient +import sys +from pathlib import Path + +# Add src directory to path so we can import app +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from app import app + +client = TestClient(app) + + +@pytest.fixture +def reset_activities(): + """Reset activities to initial state before each test""" + from app import activities + + # Store original state + original_activities = { + "Chess Club": { + "description": "Learn strategies and compete in chess tournaments", + "schedule": "Fridays, 3:30 PM - 5:00 PM", + "max_participants": 12, + "participants": ["michael@mergington.edu", "daniel@mergington.edu"] + }, + "Programming Class": { + "description": "Learn programming fundamentals and build software projects", + "schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM", + "max_participants": 20, + "participants": ["emma@mergington.edu", "sophia@mergington.edu"] + }, + "Gym Class": { + "description": "Physical education and sports activities", + "schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM", + "max_participants": 30, + "participants": ["john@mergington.edu", "olivia@mergington.edu"] + }, + "Basketball": { + "description": "Team sport focusing on basketball skills and competitive play", + "schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM", + "max_participants": 15, + "participants": ["alex@mergington.edu"] + }, + "Tennis Club": { + "description": "Learn tennis techniques and participate in matches", + "schedule": "Saturdays, 10:00 AM - 11:30 AM", + "max_participants": 10, + "participants": ["james@mergington.edu"] + }, + "Drama Club": { + "description": "Perform in theatrical productions and develop acting skills", + "schedule": "Wednesdays and Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": ["isabella@mergington.edu", "lucas@mergington.edu"] + }, + "Art Studio": { + "description": "Create visual art including painting, drawing, and sculpture", + "schedule": "Tuesdays and Fridays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["sophie@mergington.edu"] + }, + "Debate Team": { + "description": "Develop public speaking and argumentation skills", + "schedule": "Mondays and Thursdays, 3:30 PM - 4:45 PM", + "max_participants": 16, + "participants": ["noah@mergington.edu", "ava@mergington.edu"] + }, + "Science Club": { + "description": "Explore scientific concepts through experiments and projects", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 20, + "participants": ["ethan@mergington.edu"] + } + } + + # Clear and reset + activities.clear() + activities.update(original_activities) + + yield + + # Restore after test + activities.clear() + activities.update(original_activities) + + +class TestGetActivities: + """Tests for GET /activities endpoint""" + + def test_get_activities_returns_all_activities(self, reset_activities): + """Test that GET /activities returns all activities""" + response = client.get("/activities") + assert response.status_code == 200 + + data = response.json() + assert "Chess Club" in data + assert "Programming Class" in data + assert len(data) == 9 + + def test_activities_have_required_fields(self, reset_activities): + """Test that each activity has required fields""" + response = client.get("/activities") + data = response.json() + + for activity_name, activity_data in data.items(): + assert "description" in activity_data + assert "schedule" in activity_data + assert "max_participants" in activity_data + assert "participants" in activity_data + + def test_activities_have_participants(self, reset_activities): + """Test that activities contain participants""" + response = client.get("/activities") + data = response.json() + + assert len(data["Chess Club"]["participants"]) == 2 + assert "michael@mergington.edu" in data["Chess Club"]["participants"] + + +class TestSignupForActivity: + """Tests for POST /activities/{activity_name}/signup endpoint""" + + def test_successful_signup(self, reset_activities): + """Test successfully signing up for an activity""" + response = client.post( + "/activities/Chess%20Club/signup?email=newstudent@mergington.edu", + headers={"Content-Type": "application/json"} + ) + assert response.status_code == 200 + data = response.json() + assert "Signed up" in data["message"] + assert "newstudent@mergington.edu" in data["message"] + + def test_signup_adds_participant(self, reset_activities): + """Test that signup actually adds the participant""" + client.post( + "/activities/Basketball/signup?email=newplayer@mergington.edu", + headers={"Content-Type": "application/json"} + ) + + response = client.get("/activities") + data = response.json() + assert "newplayer@mergington.edu" in data["Basketball"]["participants"] + + def test_signup_for_nonexistent_activity(self, reset_activities): + """Test signing up for a nonexistent activity""" + response = client.post( + "/activities/Nonexistent%20Club/signup?email=student@mergington.edu", + headers={"Content-Type": "application/json"} + ) + assert response.status_code == 404 + data = response.json() + assert "Activity not found" in data["detail"] + + def test_signup_duplicate_email(self, reset_activities): + """Test signing up with an email already registered""" + response = client.post( + "/activities/Chess%20Club/signup?email=michael@mergington.edu", + headers={"Content-Type": "application/json"} + ) + assert response.status_code == 400 + data = response.json() + assert "already signed up" in data["detail"] + + def test_multiple_signups(self, reset_activities): + """Test multiple students signing up""" + client.post( + "/activities/Tennis%20Club/signup?email=student1@mergington.edu", + headers={"Content-Type": "application/json"} + ) + client.post( + "/activities/Tennis%20Club/signup?email=student2@mergington.edu", + headers={"Content-Type": "application/json"} + ) + + response = client.get("/activities") + data = response.json() + assert len(data["Tennis Club"]["participants"]) == 3 + assert "student1@mergington.edu" in data["Tennis Club"]["participants"] + assert "student2@mergington.edu" in data["Tennis Club"]["participants"] + + +class TestRemoveParticipant: + """Tests for DELETE /activities/{activity_name}/participants/{email} endpoint""" + + def test_successful_removal(self, reset_activities): + """Test successfully removing a participant""" + response = client.delete( + "/activities/Chess%20Club/participants/michael@mergington.edu" + ) + assert response.status_code == 200 + data = response.json() + assert "Removed" in data["message"] + assert "michael@mergington.edu" in data["message"] + + def test_removal_updates_participants_list(self, reset_activities): + """Test that removal actually removes the participant""" + client.delete( + "/activities/Drama%20Club/participants/isabella@mergington.edu" + ) + + response = client.get("/activities") + data = response.json() + assert "isabella@mergington.edu" not in data["Drama Club"]["participants"] + assert len(data["Drama Club"]["participants"]) == 1 + + def test_remove_from_nonexistent_activity(self, reset_activities): + """Test removing from a nonexistent activity""" + response = client.delete( + "/activities/Nonexistent%20Club/participants/student@mergington.edu" + ) + assert response.status_code == 404 + data = response.json() + assert "Activity not found" in data["detail"] + + def test_remove_nonexistent_participant(self, reset_activities): + """Test removing a participant who's not signed up""" + response = client.delete( + "/activities/Chess%20Club/participants/notregistered@mergington.edu" + ) + assert response.status_code == 400 + data = response.json() + assert "not signed up" in data["detail"] + + def test_remove_multiple_participants(self, reset_activities): + """Test removing multiple participants""" + client.delete( + "/activities/Debate%20Team/participants/noah@mergington.edu" + ) + client.delete( + "/activities/Debate%20Team/participants/ava@mergington.edu" + ) + + response = client.get("/activities") + data = response.json() + assert len(data["Debate Team"]["participants"]) == 0