diff --git a/.coverage b/.coverage
new file mode 100644
index 0000000..4e68f72
Binary files /dev/null and b/.coverage differ
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 4ebb1d9..ff2bd4a 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 training and tournaments",
+ "schedule": "Mondays and Thursdays, 4:00 PM - 5:30 PM",
+ "max_participants": 15,
+ "participants": ["james@mergington.edu"]
+ },
+ "Tennis Club": {
+ "description": "Learn tennis skills and compete in matches",
+ "schedule": "Tuesdays and Saturdays, 3:30 PM - 5:00 PM",
+ "max_participants": 12,
+ "participants": ["isabella@mergington.edu"]
+ },
+ "Art Studio": {
+ "description": "Explore painting, drawing, and sculpture techniques",
+ "schedule": "Wednesdays, 3:30 PM - 5:00 PM",
+ "max_participants": 18,
+ "participants": ["lucas@mergington.edu", "ava@mergington.edu"]
+ },
+ "Drama Club": {
+ "description": "Theatre arts, script writing, and stage performances",
+ "schedule": "Fridays, 4:00 PM - 5:30 PM",
+ "max_participants": 25,
+ "participants": ["noah@mergington.edu"]
+ },
+ "Debate Team": {
+ "description": "Develop public speaking and critical thinking skills",
+ "schedule": "Mondays and Wednesdays, 3:30 PM - 4:30 PM",
+ "max_participants": 16,
+ "participants": ["mia@mergington.edu", "ethan@mergington.edu"]
+ },
+ "Science Club": {
+ "description": "Conduct experiments and explore scientific concepts",
+ "schedule": "Tuesdays, 3:30 PM - 5:00 PM",
+ "max_participants": 20,
+ "participants": ["charlotte@mergington.edu"]
}
}
@@ -62,6 +98,29 @@ 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}"}
+
+
+@app.delete("/activities/{activity_name}/participants/{email}")
+def unregister_from_activity(activity_name: str, email: str):
+ """Unregister a student 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 is registered
+ if email not in activity["participants"]:
+ raise HTTPException(status_code=404, detail="Participant not found")
+
+ # Remove participant
+ activity["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..9a30179 100644
--- a/src/static/app.js
+++ b/src/static/app.js
@@ -25,10 +25,53 @@ document.addEventListener("DOMContentLoaded", () => {
${details.description}
Schedule: ${details.schedule}
Availability: ${spotsLeft} spots left
+
+
Participants (${details.participants.length}/${details.max_participants}):
+
+ ${details.participants.map(p => `- ${p}
`).join('')}
+
+
`;
activitiesList.appendChild(activityCard);
+ // Add delete event listeners to participant buttons
+ const deleteButtons = activityCard.querySelectorAll(".delete-participant-btn");
+ deleteButtons.forEach(btn => {
+ btn.addEventListener("click", async (e) => {
+ e.preventDefault();
+ const email = btn.getAttribute("data-email");
+ const activity = btn.getAttribute("data-activity");
+
+ try {
+ const response = await fetch(
+ `/activities/${encodeURIComponent(activity)}/participants/${encodeURIComponent(email)}`,
+ { method: "DELETE" }
+ );
+
+ if (response.ok) {
+ // Refresh activities list after successful deletion
+ fetchActivities();
+ messageDiv.textContent = `Unregistered ${email} from ${activity}`;
+ messageDiv.className = "success";
+ } else {
+ const result = await response.json();
+ messageDiv.textContent = result.detail || "Failed to unregister";
+ messageDiv.className = "error";
+ }
+ messageDiv.classList.remove("hidden");
+ setTimeout(() => {
+ messageDiv.classList.add("hidden");
+ }, 5000);
+ } catch (error) {
+ messageDiv.textContent = "Failed to unregister. Please try again.";
+ messageDiv.className = "error";
+ messageDiv.classList.remove("hidden");
+ console.error("Error unregistering:", error);
+ }
+ });
+ });
+
// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
@@ -62,6 +105,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";
diff --git a/src/static/styles.css b/src/static/styles.css
index a533b32..5fbe2b8 100644
--- a/src/static/styles.css
+++ b/src/static/styles.css
@@ -74,6 +74,61 @@ section h3 {
margin-bottom: 8px;
}
+.participants-section {
+ margin-top: 15px;
+ padding-top: 15px;
+ border-top: 1px solid #e0e0e0;
+}
+
+.participants-section p {
+ margin-bottom: 10px;
+ color: #555;
+ font-size: 14px;
+}
+
+.participants-list {
+ list-style: none;
+ margin-left: 0;
+ font-size: 13px;
+ color: #666;
+}
+
+.participants-list li {
+ margin-bottom: 5px;
+ padding: 4px 8px;
+ background-color: #f0f7ff;
+ border-radius: 3px;
+ border-left: 3px solid #0066cc;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.participant-email {
+ flex-grow: 1;
+}
+
+.delete-participant-btn {
+ background: none;
+ border: none;
+ color: #c62828;
+ font-size: 20px;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0 4px;
+ margin-left: 8px;
+ transition: color 0.2s, transform 0.1s;
+}
+
+.delete-participant-btn:hover {
+ color: #d32f2f;
+ transform: scale(1.2);
+}
+
+.delete-participant-btn:active {
+ transform: scale(0.95);
+}
+
.form-group {
margin-bottom: 15px;
}
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..739954c
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+# Tests package
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..3bbdb25
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,9 @@
+import pytest
+from fastapi.testclient import TestClient
+from src.app import app
+
+
+@pytest.fixture
+def client():
+ """Test client fixture for FastAPI app testing."""
+ return TestClient(app)
\ No newline at end of file
diff --git a/tests/test_activities.py b/tests/test_activities.py
new file mode 100644
index 0000000..6a8dac2
--- /dev/null
+++ b/tests/test_activities.py
@@ -0,0 +1,60 @@
+"""Tests for GET /activities endpoint."""
+
+
+def test_get_activities_returns_all_activities(client):
+ """Test that GET /activities returns all activities with correct structure."""
+ # Arrange: No specific setup needed as activities are predefined in app
+
+ # Act: Make GET request to /activities
+ response = client.get("/activities")
+
+ # Assert: Response status is 200
+ assert response.status_code == 200
+
+ # Assert: Response contains all 9 activities
+ activities = response.json()
+ assert len(activities) == 9
+
+ # Assert: Each activity has the expected structure
+ expected_activities = [
+ "Chess Club", "Programming Class", "Gym Class", "Basketball Team",
+ "Tennis Club", "Art Studio", "Drama Club", "Debate Team", "Science Club"
+ ]
+ assert set(activities.keys()) == set(expected_activities)
+
+ # Assert: Each activity has required fields
+ for activity_name, activity_data in activities.items():
+ assert "description" in activity_data
+ assert "schedule" in activity_data
+ assert "max_participants" in activity_data
+ assert "participants" in activity_data
+ assert isinstance(activity_data["participants"], list)
+
+
+def test_get_activities_participant_counts(client):
+ """Test that participant counts are correct for each activity."""
+ # Arrange: No specific setup needed
+
+ # Act: Make GET request to /activities
+ response = client.get("/activities")
+
+ # Assert: Response is successful
+ assert response.status_code == 200
+
+ activities = response.json()
+
+ # Assert: Specific participant counts match expected values
+ expected_counts = {
+ "Chess Club": 2, # michael@mergington.edu, daniel@mergington.edu
+ "Programming Class": 2, # emma@mergington.edu, sophia@mergington.edu
+ "Gym Class": 2, # john@mergington.edu, olivia@mergington.edu
+ "Basketball Team": 1, # james@mergington.edu
+ "Tennis Club": 1, # isabella@mergington.edu
+ "Art Studio": 2, # lucas@mergington.edu, ava@mergington.edu
+ "Drama Club": 1, # noah@mergington.edu
+ "Debate Team": 2, # mia@mergington.edu, ethan@mergington.edu
+ "Science Club": 1, # charlotte@mergington.edu
+ }
+
+ for activity_name, expected_count in expected_counts.items():
+ assert len(activities[activity_name]["participants"]) == expected_count
\ No newline at end of file
diff --git a/tests/test_signup.py b/tests/test_signup.py
new file mode 100644
index 0000000..9e8e054
--- /dev/null
+++ b/tests/test_signup.py
@@ -0,0 +1,64 @@
+"""Tests for POST /activities/{activity_name}/signup endpoint."""
+
+
+def test_signup_successful(client):
+ """Test successful signup for an activity."""
+ # Arrange: Choose an existing activity and a new email
+ activity_name = "Chess Club"
+ email = "newstudent@mergington.edu"
+
+ # Act: Make POST request to signup endpoint
+ response = client.post(f"/activities/{activity_name}/signup?email={email}")
+
+ # Assert: Response status is 200
+ assert response.status_code == 200
+
+ # Assert: Response contains success message
+ result = response.json()
+ assert "message" in result
+ assert f"Signed up {email} for {activity_name}" in result["message"]
+
+ # Assert: Participant was added to the activity
+ activities_response = client.get("/activities")
+ activities = activities_response.json()
+ assert email in activities[activity_name]["participants"]
+
+
+def test_signup_non_existent_activity(client):
+ """Test signup for a non-existent activity returns 404."""
+ # Arrange: Use an invalid activity name
+ invalid_activity = "NonExistentActivity"
+ email = "student@mergington.edu"
+
+ # Act: Make POST request to signup endpoint
+ response = client.post(f"/activities/{invalid_activity}/signup?email={email}")
+
+ # Assert: Response status is 404
+ assert response.status_code == 404
+
+ # Assert: Response contains appropriate error message
+ result = response.json()
+ assert "detail" in result
+ assert "Activity not found" in result["detail"]
+
+
+def test_signup_duplicate_registration(client):
+ """Test that duplicate signup for same activity returns 400."""
+ # Arrange: First, sign up a student
+ activity_name = "Programming Class"
+ email = "duplicatestudent@mergington.edu"
+
+ # Sign up once (should succeed)
+ first_response = client.post(f"/activities/{activity_name}/signup?email={email}")
+ assert first_response.status_code == 200
+
+ # Act: Attempt to sign up the same student again
+ second_response = client.post(f"/activities/{activity_name}/signup?email={email}")
+
+ # Assert: Second signup returns 400
+ assert second_response.status_code == 400
+
+ # Assert: Response contains appropriate error message
+ result = second_response.json()
+ assert "detail" in result
+ assert "Student already signed up" in result["detail"]
\ No newline at end of file
diff --git a/tests/test_unregister.py b/tests/test_unregister.py
new file mode 100644
index 0000000..48a0832
--- /dev/null
+++ b/tests/test_unregister.py
@@ -0,0 +1,69 @@
+"""Tests for DELETE /activities/{activity_name}/participants/{email} endpoint."""
+
+
+def test_unregister_successful(client):
+ """Test successful unregister from an activity."""
+ # Arrange: First sign up a participant, then unregister them
+ activity_name = "Gym Class"
+ email = "tempstudent@mergington.edu"
+
+ # Sign up the participant
+ signup_response = client.post(f"/activities/{activity_name}/signup?email={email}")
+ assert signup_response.status_code == 200
+
+ # Verify they are signed up
+ activities_response = client.get("/activities")
+ activities = activities_response.json()
+ assert email in activities[activity_name]["participants"]
+
+ # Act: Make DELETE request to unregister endpoint
+ response = client.delete(f"/activities/{activity_name}/participants/{email}")
+
+ # Assert: Response status is 200
+ assert response.status_code == 200
+
+ # Assert: Response contains success message
+ result = response.json()
+ assert "message" in result
+ assert f"Unregistered {email} from {activity_name}" in result["message"]
+
+ # Assert: Participant was removed from the activity
+ activities_response = client.get("/activities")
+ activities = activities_response.json()
+ assert email not in activities[activity_name]["participants"]
+
+
+def test_unregister_non_existent_activity(client):
+ """Test unregister from a non-existent activity returns 404."""
+ # Arrange: Use an invalid activity name
+ invalid_activity = "NonExistentActivity"
+ email = "student@mergington.edu"
+
+ # Act: Make DELETE request to unregister endpoint
+ response = client.delete(f"/activities/{invalid_activity}/participants/{email}")
+
+ # Assert: Response status is 404
+ assert response.status_code == 404
+
+ # Assert: Response contains appropriate error message
+ result = response.json()
+ assert "detail" in result
+ assert "Activity not found" in result["detail"]
+
+
+def test_unregister_non_existent_participant(client):
+ """Test unregister of a participant not signed up returns 404."""
+ # Arrange: Use valid activity but email not registered
+ activity_name = "Tennis Club"
+ unregistered_email = "notsignedup@mergington.edu"
+
+ # Act: Make DELETE request to unregister endpoint
+ response = client.delete(f"/activities/{activity_name}/participants/{unregistered_email}")
+
+ # Assert: Response status is 404
+ assert response.status_code == 404
+
+ # Assert: Response contains appropriate error message
+ result = response.json()
+ assert "detail" in result
+ assert "Participant not found" in result["detail"]
\ No newline at end of file