From 924f4c09a3c54a4e03ffab02b77edb63f1c30ec7 Mon Sep 17 00:00:00 2001 From: Anubis-programmer Date: Tue, 24 Feb 2026 21:17:32 +0000 Subject: [PATCH 1/3] Add extracurricular activities and signup validation to the API --- src/app.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/app.py b/src/app.py index 4ebb1d9..7c6cf1b 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 Team": { + "description": "Competitive basketball practice and games", + "schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM", + "max_participants": 15, + "participants": ["james@mergington.edu"] + }, + "Tennis Club": { + "description": "Tennis instruction and match play", + "schedule": "Tuesdays and Thursdays, 4:00 PM - 5:00 PM", + "max_participants": 16, + "participants": ["sarah@mergington.edu", "alex@mergington.edu"] + }, + "Drama Club": { + "description": "Perform in plays and musicals", + "schedule": "Wednesdays and Fridays, 3:30 PM - 5:00 PM", + "max_participants": 25, + "participants": ["grace@mergington.edu"] + }, + "Art Studio": { + "description": "Painting, drawing, and sculpture techniques", + "schedule": "Mondays and Thursdays, 3:30 PM - 4:45 PM", + "max_participants": 18, + "participants": ["isabella@mergington.edu", "lucas@mergington.edu"] + }, + "Debate Team": { + "description": "Develop argumentation and public speaking skills", + "schedule": "Tuesdays, 4:00 PM - 5:30 PM", + "max_participants": 10, + "participants": ["benjamin@mergington.edu"] + }, + "Science Club": { + "description": "Explore experiments and scientific discovery", + "schedule": "Fridays, 3:30 PM - 4:45 PM", + "max_participants": 20, + "participants": ["nina@mergington.edu", "ryan@mergington.edu"] } } @@ -62,6 +98,14 @@ def signup_for_activity(activity_name: str, email: str): # Get the specific activity activity = activities[activity_name] + # Check if student is already signed up + if email in activity["participants"]: + raise HTTPException(status_code=400, detail="Student already signed up for this activity") + + # Check if activity is full + if len(activity["participants"]) >= activity["max_participants"]: + raise HTTPException(status_code=400, detail="Activity is full") + # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} From 87eb414623e28030e82014fbdcaf282624148a67 Mon Sep 17 00:00:00 2001 From: Anubis-programmer Date: Tue, 24 Feb 2026 21:31:33 +0000 Subject: [PATCH 2/3] Implement participant removal feature and update UI for displaying participants --- src/app.py | 19 ++++++++++++++ src/static/app.js | 55 ++++++++++++++++++++++++++++++++++++++++ src/static/styles.css | 58 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/src/app.py b/src/app.py index 7c6cf1b..81efb31 100644 --- a/src/app.py +++ b/src/app.py @@ -109,3 +109,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") + + activity = activities[activity_name] + + # Check if participant exists + if email not in activity["participants"]: + raise HTTPException(status_code=404, detail="Participant not found") + + # Remove participant + 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 dcc1e38..16f2ca8 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -25,6 +25,19 @@ document.addEventListener("DOMContentLoaded", () => {

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+
+ Participants (${details.participants.length}): +
    + ${details.participants.length > 0 + ? details.participants.map(email => ` +
  • + ${email} + +
  • + `).join('') + : '
  • No participants yet
  • '} +
+
`; activitiesList.appendChild(activityCard); @@ -62,6 +75,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + fetchActivities(); } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; @@ -81,6 +95,47 @@ document.addEventListener("DOMContentLoaded", () => { } }); + // Handle delete participant button clicks + document.addEventListener("click", async (event) => { + if (event.target.classList.contains("delete-participant-btn")) { + const activity = event.target.dataset.activity; + const email = event.target.dataset.email; + + if (!confirm(`Remove ${email} from ${activity}?`)) { + return; + } + + try { + const response = await fetch( + `/activities/${encodeURIComponent(activity)}/participants/${encodeURIComponent(email)}`, + { method: "DELETE" } + ); + + if (response.ok) { + // Refresh activities list + fetchActivities(); + messageDiv.textContent = `Removed ${email} from ${activity}`; + messageDiv.className = "success"; + messageDiv.classList.remove("hidden"); + + setTimeout(() => { + messageDiv.classList.add("hidden"); + }, 5000); + } else { + const result = await response.json(); + messageDiv.textContent = result.detail || "Failed to remove participant"; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + } + } catch (error) { + messageDiv.textContent = "Error removing participant"; + 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 a533b32..5af7943 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,64 @@ section h3 { margin-bottom: 8px; } +.participants-section { + margin-top: 15px; + padding-top: 15px; + border-top: 2px solid #e0e0e0; +} + +.participants-section strong { + display: block; + margin-bottom: 8px; + color: #1a237e; + font-size: 14px; +} + +.participants-section ul { + list-style: none; + margin-left: 0; + padding-left: 0; +} + +.participant-item { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: #555; + margin-bottom: 6px; + padding: 8px; + background-color: #f0f0f0; + border-radius: 3px; +} + +.participant-item span { + word-break: break-word; + flex: 1; +} + +.delete-participant-btn { + background-color: #d32f2f; + color: white; + border: none; + padding: 4px 8px; + font-size: 14px; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.2s; + margin-left: 10px; + flex-shrink: 0; +} + +.delete-participant-btn:hover { + background-color: #b71c1c; +} + +.participants-section li.no-participants { + color: #999; + font-style: italic; +} + .form-group { margin-bottom: 15px; } From 38e39048fa3c9e69b9dd8272ecd62951dbda3e39 Mon Sep 17 00:00:00 2001 From: Anubis-programmer Date: Tue, 24 Feb 2026 21:38:59 +0000 Subject: [PATCH 3/3] Add tests for extracurricular activities API and update requirements --- requirements.txt | 3 +- tests/__init__.py | 0 tests/test_activities.py | 176 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_activities.py diff --git a/requirements.txt b/requirements.txt index 5d9efb5..f2821b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ fastapi uvicorn httpx -watchfiles \ No newline at end of file +watchfiles +pytest \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_activities.py b/tests/test_activities.py new file mode 100644 index 0000000..0b7be28 --- /dev/null +++ b/tests/test_activities.py @@ -0,0 +1,176 @@ +""" +Tests for Mergington High School extracurricular activities API +Using AAA (Arrange-Act-Assert) pattern +""" + +from fastapi.testclient import TestClient +from src.app import app + +client = TestClient(app) + + +class TestGetActivities: + """Test suite for GET /activities endpoint""" + + def test_get_activities_returns_all_activities(self): + # Arrange + expected_keys = {"Chess Club", "Programming Class", "Gym Class", + "Basketball Team", "Tennis Club", "Drama Club", + "Art Studio", "Debate Team", "Science Club"} + + # Act + response = client.get("/activities") + + # Assert + assert response.status_code == 200 + activities = response.json() + assert set(activities.keys()) == expected_keys + + def test_get_activities_returns_correct_structure(self): + # Arrange + required_fields = {"description", "schedule", "max_participants", "participants"} + + # Act + response = client.get("/activities") + activities = response.json() + + # Assert + assert response.status_code == 200 + for activity_name, activity_data in activities.items(): + assert set(activity_data.keys()) == required_fields + assert isinstance(activity_data["participants"], list) + assert isinstance(activity_data["max_participants"], int) + + +class TestSignupForActivity: + """Test suite for POST /activities/{activity_name}/signup endpoint""" + + def test_signup_success(self): + # Arrange + test_email = "test.student@mergington.edu" + activity_name = "Chess Club" + initial_participant_count = len(client.get("/activities").json()[activity_name]["participants"]) + + # Act + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": test_email} + ) + + # Assert + assert response.status_code == 200 + assert "Signed up" in response.json()["message"] + + # Verify participant was added + updated_activities = client.get("/activities").json() + assert test_email in updated_activities[activity_name]["participants"] + assert len(updated_activities[activity_name]["participants"]) == initial_participant_count + 1 + + def test_signup_duplicate_participant_error(self): + # Arrange + test_email = "duplicate.test@mergington.edu" + activity_name = "Programming Class" + + # Sign up once (should succeed) + client.post(f"/activities/{activity_name}/signup", params={"email": test_email}) + + # Act - Try to sign up again + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": test_email} + ) + + # Assert + assert response.status_code == 400 + assert "already signed up" in response.json()["detail"] + + def test_signup_invalid_activity_error(self): + # Arrange + test_email = "test@mergington.edu" + invalid_activity = "Nonexistent Club" + + # Act + response = client.post( + f"/activities/{invalid_activity}/signup", + params={"email": test_email} + ) + + # Assert + assert response.status_code == 404 + assert "Activity not found" in response.json()["detail"] + + def test_signup_activity_full_error(self): + # Arrange + activity_name = "Debate Team" # Has max_participants: 10 + activities = client.get("/activities").json() + + # Fill up the activity + test_emails = [f"student{i}@mergington.edu" for i in range(10)] + for email in test_emails: + client.post(f"/activities/{activity_name}/signup", params={"email": email}) + + # Act - Try to sign up when full + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": "extra.student@mergington.edu"} + ) + + # Assert + assert response.status_code == 400 + assert "full" in response.json()["detail"] + + +class TestRemoveParticipant: + """Test suite for DELETE /activities/{activity_name}/participants/{email} endpoint""" + + def test_remove_participant_success(self): + # Arrange + test_email = "remove.test@mergington.edu" + activity_name = "Tennis Club" + + # Sign up the participant + client.post(f"/activities/{activity_name}/signup", params={"email": test_email}) + activities = client.get("/activities").json() + initial_count = len(activities[activity_name]["participants"]) + + # Act + response = client.delete( + f"/activities/{activity_name}/participants/{test_email}" + ) + + # Assert + assert response.status_code == 200 + assert "Removed" in response.json()["message"] + + # Verify participant was removed + updated_activities = client.get("/activities").json() + assert test_email not in updated_activities[activity_name]["participants"] + assert len(updated_activities[activity_name]["participants"]) == initial_count - 1 + + def test_remove_participant_not_found_error(self): + # Arrange + nonexistent_email = "notinactivity@mergington.edu" + activity_name = "Art Studio" + + # Act + response = client.delete( + f"/activities/{activity_name}/participants/{nonexistent_email}" + ) + + # Assert + assert response.status_code == 404 + assert "Participant not found" in response.json()["detail"] + + def test_remove_participant_activity_not_found_error(self): + # Arrange + test_email = "test@mergington.edu" + invalid_activity = "Fake Club" + + # Act + response = client.delete( + f"/activities/{invalid_activity}/participants/{test_email}" + ) + + # Assert + assert response.status_code == 404 + assert "Activity not found" in response.json()["detail"]