From c772da5158a30303542a0c9064148be3a8f3b8cd Mon Sep 17 00:00:00 2001 From: Sasmita Ojha <68559380+Sasmita07@users.noreply.github.com> Date: Mon, 20 Apr 2026 06:09:38 +0000 Subject: [PATCH 1/3] Add extracurricular activities and signup validation to API --- src/app.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/app.py b/src/app.py index 4ebb1d9..b824c51 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": "Join our competitive basketball team and play in tournaments", + "schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM", + "max_participants": 15, + "participants": ["alex@mergington.edu"] + }, + "Tennis Club": { + "description": "Learn tennis skills and compete in matches", + "schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 16, + "participants": ["lucas@mergington.edu"] + }, + "Art Studio": { + "description": "Explore painting, drawing, and other visual arts", + "schedule": "Mondays and Thursdays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["isabella@mergington.edu", "mia@mergington.edu"] + }, + "Music Ensemble": { + "description": "Play instruments and perform in concerts", + "schedule": "Wednesdays and Fridays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": ["noah@mergington.edu"] + }, + "Debate Club": { + "description": "Develop public speaking and argumentation skills through competitive debate", + "schedule": "Tuesdays, 4:00 PM - 5:30 PM", + "max_participants": 14, + "participants": ["ava@mergington.edu", "ethan@mergington.edu"] + }, + "Science Club": { + "description": "Conduct experiments and explore STEM topics", + "schedule": "Fridays, 4:00 PM - 5:00 PM", + "max_participants": 20, + "participants": ["liam@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 37f0589d466a1487130879f432a6590a92a6ac76 Mon Sep 17 00:00:00 2001 From: Sasmita Ojha <68559380+Sasmita07@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:33:01 +0000 Subject: [PATCH 2/3] Implement participant removal feature and update styles for activity management --- src/app.py | 19 ++++++++++++++ src/static/app.js | 59 ++++++++++++++++++++++++++++++++++++++++++- src/static/styles.css | 42 ++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index b824c51..276b84d 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}/signup") +def remove_from_activity(activity_name: str, email: str): + """Remove a student 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 dcc1e38..4986481 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -4,15 +4,54 @@ document.addEventListener("DOMContentLoaded", () => { const signupForm = document.getElementById("signup-form"); const messageDiv = document.getElementById("message"); + // Function to delete a participant from an activity + async function deleteParticipant(activityName, email) { + try { + const response = await fetch( + `/activities/${encodeURIComponent(activityName)}/signup?email=${encodeURIComponent(email)}`, + { + method: "DELETE", + } + ); + + const result = await response.json(); + + if (response.ok) { + messageDiv.textContent = result.message; + messageDiv.className = "success"; + // Refresh activities list + fetchActivities(); + } else { + messageDiv.textContent = result.detail || "An error occurred"; + 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); + } + } + // Function to fetch activities from API async function fetchActivities() { try { - const response = await fetch("/activities"); + const response = await fetch(`/activities?t=${Date.now()}`); const activities = await response.json(); // Clear loading message activitiesList.innerHTML = ""; + // Clear and repopulate activity select dropdown + activitySelect.innerHTML = ''; + // Populate activities list Object.entries(activities).forEach(([name, details]) => { const activityCard = document.createElement("div"); @@ -25,10 +64,26 @@ document.addEventListener("DOMContentLoaded", () => {

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+
+

Participants:

+ +
`; activitiesList.appendChild(activityCard); + // Add event listeners for delete buttons + const deleteButtons = activityCard.querySelectorAll('.delete-btn'); + deleteButtons.forEach(button => { + button.addEventListener('click', async (event) => { + const email = event.target.dataset.email; + const activity = event.target.dataset.activity; + await deleteParticipant(activity, email); + }); + }); + // Add option to select dropdown const option = document.createElement("option"); option.value = name; @@ -62,6 +117,8 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + // Refresh activities list + fetchActivities(); } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; diff --git a/src/static/styles.css b/src/static/styles.css index a533b32..a9692e3 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,48 @@ section h3 { margin-bottom: 8px; } +.participants-section { + margin-top: 15px; + border-top: 1px solid #eee; + padding-top: 10px; +} + +.participants-section p { + margin-bottom: 5px; + font-weight: bold; + color: #1a237e; +} + +.participants-list { + list-style-type: none; + margin-left: 0; + padding-left: 0; +} + +.participants-list li { + margin-bottom: 3px; + color: #555; + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.delete-btn { + background: none; + border: none; + color: #c62828; + cursor: pointer; + font-size: 18px; + font-weight: bold; + padding: 0 5px; + margin-left: 10px; +} + +.delete-btn:hover { + color: #b71c1c; +} + .form-group { margin-bottom: 15px; } From 3b55a8406e42d037370c838ebfb16232718888fb Mon Sep 17 00:00:00 2001 From: Sasmita Ojha <68559380+Sasmita07@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:58:11 +0000 Subject: [PATCH 3/3] Add tests for activity management and update requirements --- requirements.txt | 4 +- tests/__init__.py | 1 + tests/conftest.py | 27 ++++++++++ tests/test_activities.py | 53 ++++++++++++++++++ tests/test_delete.py | 77 ++++++++++++++++++++++++++ tests/test_integration.py | 110 ++++++++++++++++++++++++++++++++++++++ tests/test_signup.py | 77 ++++++++++++++++++++++++++ 7 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_activities.py create mode 100644 tests/test_delete.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_signup.py diff --git a/requirements.txt b/requirements.txt index 5d9efb5..2b7432a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ fastapi uvicorn httpx -watchfiles \ No newline at end of file +watchfiles +pytest +pytest-asyncio \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..665a74d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests for Mergington High School Activities API \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9a4f0ad --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,27 @@ +import pytest +from fastapi.testclient import TestClient +from src.app import app + + +@pytest.fixture +def client(): + """FastAPI test client fixture""" + return TestClient(app) + + +@pytest.fixture +def sample_activity(): + """Sample activity data for testing""" + return { + "name": "Test Activity", + "description": "A test activity", + "schedule": "Test schedule", + "max_participants": 10, + "participants": [] + } + + +@pytest.fixture +def sample_email(): + """Sample email for testing""" + return "test@example.com" \ No newline at end of file diff --git a/tests/test_activities.py b/tests/test_activities.py new file mode 100644 index 0000000..28cc741 --- /dev/null +++ b/tests/test_activities.py @@ -0,0 +1,53 @@ +import pytest +from fastapi.testclient import TestClient + + +class TestRootEndpoint: + """Test cases for the root endpoint""" + + def test_root_redirect(self, client: TestClient): + # Arrange + expected_status_code = 307 + expected_location = "/static/index.html" + + # Act - don't follow redirects + response = client.get("/", follow_redirects=False) + + # Assert + assert response.status_code == expected_status_code + assert response.headers["location"] == expected_location + + +class TestActivitiesEndpoint: + """Test cases for the activities endpoint""" + + def test_get_activities_success(self, client: TestClient): + # Arrange + expected_status_code = 200 + + # Act + response = client.get("/activities") + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert isinstance(data, dict) + assert len(data) > 0 # Should have activities + + # Check structure of first activity + first_activity = next(iter(data.values())) + required_keys = ["description", "schedule", "max_participants", "participants"] + for key in required_keys: + assert key in first_activity + + def test_get_activities_returns_all_activities(self, client: TestClient): + # Arrange + response = client.get("/activities") + data = response.json() + + # Act - count activities + activity_count = len(data) + + # Assert + assert activity_count > 0 + assert isinstance(data, dict) \ No newline at end of file diff --git a/tests/test_delete.py b/tests/test_delete.py new file mode 100644 index 0000000..c9c5e14 --- /dev/null +++ b/tests/test_delete.py @@ -0,0 +1,77 @@ +import pytest +from fastapi.testclient import TestClient + + +class TestDeleteParticipantEndpoint: + """Test cases for removing participants from activities""" + + def test_delete_participant_success(self, client: TestClient): + # Arrange + activity_name = "Chess Club" + email = "daniel@mergington.edu" # Already exists + expected_status_code = 200 + + # Act + response = client.delete( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "message" in data + assert email in data["message"] + assert activity_name in data["message"] + + def test_delete_from_nonexistent_activity(self, client: TestClient): + # Arrange + activity_name = "Nonexistent Activity" + email = "test@example.com" + expected_status_code = 404 + + # Act + response = client.delete( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "detail" in data + assert "not found" in data["detail"].lower() + + def test_delete_nonexistent_participant(self, client: TestClient): + # Arrange + activity_name = "Chess Club" + email = "nonexistent@example.com" + expected_status_code = 400 + + # Act + response = client.delete( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "detail" in data + assert "not signed up" in data["detail"].lower() + + def test_delete_with_url_encoding(self, client: TestClient): + # Arrange + activity_name = "Programming Class" + email = "emma@mergington.edu" + expected_status_code = 200 + + # Act + response = client.delete( + f"/activities/{activity_name}/signup?email={email}" + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "message" in data \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..789c591 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,110 @@ +import pytest +from fastapi.testclient import TestClient + + +class TestActivityWorkflow: + """Integration tests for complete activity signup workflow""" + + def test_full_signup_workflow(self, client: TestClient): + # Arrange + activity_name = "Basketball Team" + email = "newworkflow@example.com" + + # Act 1: Check initial state + response = client.get("/activities") + initial_data = response.json() + initial_participants = initial_data[activity_name]["participants"] + initial_count = len(initial_participants) + + # Act 2: Sign up for activity + signup_response = client.post( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert signup success + assert signup_response.status_code == 200 + signup_data = signup_response.json() + assert email in signup_data["message"] + + # Act 3: Verify participant was added + response = client.get("/activities") + updated_data = response.json() + updated_participants = updated_data[activity_name]["participants"] + updated_count = len(updated_participants) + + # Assert participant count increased + assert updated_count == initial_count + 1 + assert email in updated_participants + + # Act 4: Remove participant + delete_response = client.delete( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert delete success + assert delete_response.status_code == 200 + delete_data = delete_response.json() + assert email in delete_data["message"] + + # Act 5: Verify participant was removed + response = client.get("/activities") + final_data = response.json() + final_participants = final_data[activity_name]["participants"] + final_count = len(final_participants) + + # Assert participant count returned to original + assert final_count == initial_count + assert email not in final_participants + + def test_activity_capacity_limits(self, client: TestClient): + # Arrange + activity_name = "Chess Club" # Max 12 participants + test_email = "capacitytest@example.com" + + # Act: Try to sign up + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": test_email} + ) + + # Assert: Should succeed (we're not enforcing capacity in current implementation) + assert response.status_code == 200 + + # Cleanup: Remove the test participant + client.delete( + f"/activities/{activity_name}/signup", + params={"email": test_email} + ) + + def test_concurrent_signup_scenarios(self, client: TestClient): + # Arrange + activity_name = "Tennis Club" + email1 = "concurrent1@example.com" + email2 = "concurrent2@example.com" + + # Act: Sign up two different participants + response1 = client.post( + f"/activities/{activity_name}/signup", + params={"email": email1} + ) + response2 = client.post( + f"/activities/{activity_name}/signup", + params={"email": email2} + ) + + # Assert both succeed + assert response1.status_code == 200 + assert response2.status_code == 200 + + # Verify both are in the activity + response = client.get("/activities") + data = response.json() + participants = data[activity_name]["participants"] + assert email1 in participants + assert email2 in participants + + # Cleanup + client.delete(f"/activities/{activity_name}/signup", params={"email": email1}) + client.delete(f"/activities/{activity_name}/signup", params={"email": email2}) \ No newline at end of file diff --git a/tests/test_signup.py b/tests/test_signup.py new file mode 100644 index 0000000..6cc85e9 --- /dev/null +++ b/tests/test_signup.py @@ -0,0 +1,77 @@ +import pytest +from fastapi.testclient import TestClient + + +class TestSignupEndpoint: + """Test cases for activity signup functionality""" + + def test_signup_success(self, client: TestClient): + # Arrange + activity_name = "Chess Club" + email = "newstudent@example.com" + expected_status_code = 200 + + # Act + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "message" in data + assert email in data["message"] + assert activity_name in data["message"] + + def test_signup_nonexistent_activity(self, client: TestClient): + # Arrange + activity_name = "Nonexistent Activity" + email = "test@example.com" + expected_status_code = 404 + + # Act + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "detail" in data + assert "not found" in data["detail"].lower() + + def test_signup_duplicate_participant(self, client: TestClient): + # Arrange + activity_name = "Chess Club" + email = "michael@mergington.edu" # Already signed up + expected_status_code = 400 + + # Act + response = client.post( + f"/activities/{activity_name}/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "detail" in data + assert "already signed up" in data["detail"].lower() + + def test_signup_with_url_encoding(self, client: TestClient): + # Arrange + activity_name = "Programming Class" + email = "test+user@example.com" + expected_status_code = 200 + + # Act + response = client.post( + f"/activities/{activity_name}/signup?email={email}" + ) + + # Assert + assert response.status_code == expected_status_code + data = response.json() + assert "message" in data \ No newline at end of file