From 524fcc894acb4a6bfa4ad4256ccb080ddaaa2f91 Mon Sep 17 00:00:00 2001 From: Krishnaraj K P Date: Wed, 24 Jun 2026 05:24:17 +0000 Subject: [PATCH 1/3] Add extracurricular activities and signup validation to the API --- src/app.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 4ebb1d9..13ac3ae 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 team and practice sessions", + "schedule": "Tuesdays and Thursdays, 4:30 PM - 6:00 PM", + "max_participants": 15, + "participants": ["james@mergington.edu"] + }, + "Soccer Club": { + "description": "Soccer training and friendly matches", + "schedule": "Mondays and Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 25, + "participants": ["lucas@mergington.edu", "ava@mergington.edu"] + }, + "Art Studio": { + "description": "Painting, drawing, and visual arts creation", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["isabella@mergington.edu"] + }, + "Music Band": { + "description": "Learn to play instruments and perform in concerts", + "schedule": "Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 20, + "participants": ["noah@mergington.edu", "mia@mergington.edu"] + }, + "Debate Team": { + "description": "Competitive debate and public speaking skills", + "schedule": "Mondays and Thursdays, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": ["alexander@mergington.edu"] + }, + "Science Club": { + "description": "Explore STEM topics and conduct experiments", + "schedule": "Fridays, 2:00 PM - 3:30 PM", + "max_participants": 22, + "participants": ["charlotte@mergington.edu", "benjamin@mergington.edu"] } } @@ -54,7 +90,10 @@ def get_activities(): @app.post("/activities/{activity_name}/signup") def signup_for_activity(activity_name: str, email: str): - """Sign up a student for an activity""" + # Validate student is not already signed up + if email in activities[activity_name]["participants"]: + raise HTTPException(status_code=400, detail="Student is already signed up for this activity") + # Validate activity exists if activity_name not in activities: raise HTTPException(status_code=404, detail="Activity not found") From 840a017e41f4f7f3db2bf271fb35bdabe37bb51b Mon Sep 17 00:00:00 2001 From: Krishnaraj K P Date: Wed, 24 Jun 2026 05:34:02 +0000 Subject: [PATCH 2/3] Add unregister functionality for activities and enhance participant display --- src/app.py | 15 ++++++++ src/static/app.js | 84 +++++++++++++++++++++++++++++++++++++------ src/static/styles.css | 43 ++++++++++++++++++++++ 3 files changed, 132 insertions(+), 10 deletions(-) diff --git a/src/app.py b/src/app.py index 13ac3ae..661fb96 100644 --- a/src/app.py +++ b/src/app.py @@ -104,3 +104,18 @@ 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}/unregister") +def unregister_from_activity(activity_name: str, email: str): + # Validate activity exists + if activity_name not in activities: + raise HTTPException(status_code=404, detail="Activity not found") + + # Validate student is signed up + if email not in activities[activity_name]["participants"]: + raise HTTPException(status_code=404, detail="Participant not found in activity") + + # Remove student + activities[activity_name]["participants"].remove(email) + return {"message": f"Unregistered {email} from {activity_name}"} diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..1fc6d0c 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -12,22 +12,84 @@ document.addEventListener("DOMContentLoaded", () => { // Clear loading message activitiesList.innerHTML = ""; + // Reset activity select options + activitySelect.innerHTML = ''; // Populate activities list Object.entries(activities).forEach(([name, details]) => { - const activityCard = document.createElement("div"); - activityCard.className = "activity-card"; + const activityCard = document.createElement("div"); + activityCard.className = "activity-card"; - const spotsLeft = details.max_participants - details.participants.length; + const spotsLeft = details.max_participants - details.participants.length; - activityCard.innerHTML = ` -

${name}

-

${details.description}

-

Schedule: ${details.schedule}

-

Availability: ${spotsLeft} spots left

- `; + const participants = details.participants || []; + const participantsHTML = participants.length + ? ` +
+
Participants
+
    + ${participants + .map( + (p) => + `
  • ${p}
  • ` + ) + .join("")} +
+
+ ` + : ` +
+
Participants
+

No participants yet

+
+ `; - activitiesList.appendChild(activityCard); + activityCard.innerHTML = ` +

${name}

+

${details.description}

+

Schedule: ${details.schedule}

+

Availability: ${spotsLeft} spots left

+ ${participantsHTML} + `; + + activitiesList.appendChild(activityCard); + + // Attach unregister handlers for this activity card + activityCard.querySelectorAll(".unregister-btn").forEach((btn) => { + btn.addEventListener("click", async (e) => { + const email = btn.dataset.email; + const activity = btn.dataset.activity; + + try { + const resp = await fetch( + `/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent( + email + )}`, + { method: "DELETE" } + ); + + const result = await resp.json(); + + if (resp.ok) { + messageDiv.textContent = result.message; + messageDiv.className = "success"; + messageDiv.classList.remove("hidden"); + setTimeout(() => messageDiv.classList.add("hidden"), 3000); + // Refresh activities list + fetchActivities(); + } else { + messageDiv.textContent = result.detail || "An error occurred"; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + } + } catch (err) { + console.error("Error unregistering:", err); + messageDiv.textContent = "Failed to unregister. Please try again."; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + } + }); + }); // Add option to select dropdown const option = document.createElement("option"); @@ -62,6 +124,8 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + // Refresh activities so the new participant appears without page reload + 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..4806f71 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,49 @@ section h3 { margin-bottom: 8px; } +.participants { + margin-top: 12px; +} + +.participants h5 { + margin-bottom: 8px; + color: #333; + font-size: 15px; +} + +.participants-list { + list-style: none; + padding-left: 0; + margin: 0; +} + +.participants-list li { + padding: 4px 0; + color: #555; + font-size: 14px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.unregister-btn { + background: transparent; + border: none; + color: #c62828; + cursor: pointer; + font-size: 14px; + padding: 4px; +} + +.unregister-btn:hover { + opacity: 0.8; +} + +.no-participants { + font-style: italic; + color: #777; +} + .form-group { margin-bottom: 15px; } From 8a62f3f7f39084b1b59a41c1967f2ea1f4b0414b Mon Sep 17 00:00:00 2001 From: Krishnaraj K P Date: Wed, 24 Jun 2026 05:50:02 +0000 Subject: [PATCH 3/3] Add testing setup and implement tests for activity signup and unregistration --- .vscode/settings.json | 7 ++ requirements.txt | 3 +- src/app.py | 8 +-- tests/__init__.py | 1 + tests/test_app.py | 154 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tests/__init__.py create mode 100644 tests/test_app.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file 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/src/app.py b/src/app.py index 661fb96..742d017 100644 --- a/src/app.py +++ b/src/app.py @@ -90,14 +90,14 @@ def get_activities(): @app.post("/activities/{activity_name}/signup") def signup_for_activity(activity_name: str, email: str): - # Validate student is not already signed up - if email in activities[activity_name]["participants"]: - raise HTTPException(status_code=400, detail="Student is already signed up for this activity") - # Validate activity exists if activity_name not in activities: raise HTTPException(status_code=404, detail="Activity not found") + # Validate student is not already signed up + if email in activities[activity_name]["participants"]: + raise HTTPException(status_code=400, detail="Student is already signed up for this activity") + # Get the specific activity activity = activities[activity_name] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..65140f2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..280f3eb --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,154 @@ +import pytest +import copy +from fastapi.testclient import TestClient +from src.app import app, activities + + +# Store the original activities state for resetting between tests +ORIGINAL_ACTIVITIES = copy.deepcopy(activities) + + +@pytest.fixture(autouse=True) +def reset_activities(): + """Reset activities to their original state before and after each test.""" + # Reset before test + activities.clear() + activities.update(copy.deepcopy(ORIGINAL_ACTIVITIES)) + yield + # Reset after test + activities.clear() + activities.update(copy.deepcopy(ORIGINAL_ACTIVITIES)) + + +@pytest.fixture +def client(): + """Create a TestClient for the FastAPI app.""" + return TestClient(app) + + +class TestGetActivities: + def test_get_activities_returns_all_activities(self, client): + """Test that GET /activities returns all activities (Arrange-Act-Assert).""" + # Arrange: no special setup required (fixture provides `client`) + + # Act + response = client.get("/activities") + + # Assert + assert response.status_code == 200 + data = response.json() + assert isinstance(data, dict) + assert "Chess Club" in data + assert "Programming Class" in data + assert len(data) == 9 + + +class TestSignupForActivity: + def test_signup_for_activity_success(self, client): + """Test successful signup for an activity (Arrange-Act-Assert).""" + # Arrange + email = "newstudent@mergington.edu" + + # Act + response = client.post( + "/activities/Chess Club/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == 200 + assert response.json()["message"] == f"Signed up {email} for Chess Club" + assert email in activities["Chess Club"]["participants"] + + def test_signup_for_nonexistent_activity(self, client): + """Test signup for an activity that doesn't exist (Arrange-Act-Assert).""" + # Arrange + email = "student@mergington.edu" + + # Act + response = client.post( + "/activities/Nonexistent Club/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == 404 + assert response.json()["detail"] == "Activity not found" + + def test_signup_already_signed_up(self, client): + """Test signup when student is already signed up (Arrange-Act-Assert).""" + # Arrange + email = "michael@mergington.edu" # already present in fixture data + + # Act + response = client.post( + "/activities/Chess Club/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == 400 + assert response.json()["detail"] == "Student is already signed up for this activity" + + def test_signup_checks_activity_existence_first(self, client): + """Test that activity existence is checked before participant status (Arrange-Act-Assert).""" + # Arrange + email = "michael@mergington.edu" + + # Act + response = client.post( + "/activities/Nonexistent Club/signup", + params={"email": email} + ) + + # Assert + assert response.status_code == 404 + assert response.json()["detail"] == "Activity not found" + + +class TestUnregisterFromActivity: + def test_unregister_success(self, client): + """Test successful unregistration from an activity (Arrange-Act-Assert).""" + # Arrange + email = "michael@mergington.edu" + + # Act + response = client.delete( + "/activities/Chess Club/unregister", + params={"email": email} + ) + + # Assert + assert response.status_code == 200 + assert response.json()["message"] == f"Unregistered {email} from Chess Club" + assert email not in activities["Chess Club"]["participants"] + + def test_unregister_nonexistent_activity(self, client): + """Test unregistration from an activity that doesn't exist (Arrange-Act-Assert).""" + # Arrange + email = "student@mergington.edu" + + # Act + response = client.delete( + "/activities/Nonexistent Club/unregister", + params={"email": email} + ) + + # Assert + assert response.status_code == 404 + assert response.json()["detail"] == "Activity not found" + + def test_unregister_student_not_in_activity(self, client): + """Test unregistration when student is not in the activity (Arrange-Act-Assert).""" + # Arrange + email = "notastudet@mergington.edu" + + # Act + response = client.delete( + "/activities/Chess Club/unregister", + params={"email": email} + ) + + # Assert + assert response.status_code == 404 + assert response.json()["detail"] == "Participant not found in activity"