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:
+
+ ${details.participants.length > 0 ? details.participants.map(email => `- ${email}
`).join('') : '- No participants yet
'}
+
+
`;
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