diff --git a/README.md b/README.md
index e8107c1..bd7b455 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Getting Started with GitHub Copilot
+# Getting Started with GitHub Copilot
diff --git a/requirements.txt b/requirements.txt
index 97dc7cd..2522ad0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
fastapi
uvicorn
+pytest
+httpx
diff --git a/src/app.py b/src/app.py
index 4ebb1d9..ce6b13b 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": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
+ "max_participants": 15,
+ "participants": ["james@mergington.edu", "lucas@mergington.edu"]
+ },
+ "Swimming Club": {
+ "description": "Swimming lessons and competitive training",
+ "schedule": "Wednesdays, 3:30 PM - 5:00 PM",
+ "max_participants": 25,
+ "participants": ["ava@mergington.edu", "mia@mergington.edu"]
+ },
+ "Art Club": {
+ "description": "Explore various art mediums including painting and sculpture",
+ "schedule": "Mondays, 3:30 PM - 5:00 PM",
+ "max_participants": 18,
+ "participants": ["isabella@mergington.edu", "charlotte@mergington.edu"]
+ },
+ "Drama Club": {
+ "description": "Acting, theater production, and performance arts",
+ "schedule": "Thursdays, 3:30 PM - 5:30 PM",
+ "max_participants": 24,
+ "participants": ["amelia@mergington.edu", "harper@mergington.edu"]
+ },
+ "Debate Team": {
+ "description": "Develop critical thinking and public speaking skills through debates",
+ "schedule": "Wednesdays, 3:30 PM - 5:00 PM",
+ "max_participants": 16,
+ "participants": ["ethan@mergington.edu", "benjamin@mergington.edu"]
+ },
+ "Science Olympiad": {
+ "description": "Prepare for science competitions and conduct experiments",
+ "schedule": "Fridays, 3:30 PM - 5:30 PM",
+ "max_participants": 20,
+ "participants": ["alexander@mergington.edu", "william@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}/unregister")
+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")
+
+ # 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 not signed up for this activity")
+
+ # Remove student
+ 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..fb439c1 100644
--- a/src/static/app.js
+++ b/src/static/app.js
@@ -20,11 +20,26 @@ document.addEventListener("DOMContentLoaded", () => {
const spotsLeft = details.max_participants - details.participants.length;
+ const participantsList = details.participants.length > 0
+ ? `
+ ${details.participants.map(email => `
+ -
+ ${email}
+
+
+ `).join('')}
+
`
+ : `No participants yet. Be the first to sign up!
`;
+
activityCard.innerHTML = `
${name}
${details.description}
Schedule: ${details.schedule}
Availability: ${spotsLeft} spots left
+
+
+ ${participantsList}
+
`;
activitiesList.appendChild(activityCard);
@@ -59,6 +74,9 @@ document.addEventListener("DOMContentLoaded", () => {
const result = await response.json();
if (response.ok) {
+ // Refresh activities to show updated list
+ await fetchActivities();
+
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
@@ -81,6 +99,52 @@ document.addEventListener("DOMContentLoaded", () => {
}
});
+ // Handle delete button clicks
+ activitiesList.addEventListener("click", async (event) => {
+ if (event.target.classList.contains("delete-btn")) {
+ const activityName = event.target.dataset.activity;
+ const email = event.target.dataset.email;
+
+ if (!confirm(`Are you sure you want to unregister ${email} from ${activityName}?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch(
+ `/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`,
+ {
+ method: "DELETE",
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok) {
+ // Refresh activities to show updated list
+ await fetchActivities();
+
+ messageDiv.textContent = result.message;
+ messageDiv.className = "success";
+ messageDiv.classList.remove("hidden");
+
+ // Hide message after 3 seconds
+ setTimeout(() => {
+ messageDiv.classList.add("hidden");
+ }, 3000);
+ } else {
+ messageDiv.textContent = result.detail || "Failed to unregister";
+ messageDiv.className = "error";
+ messageDiv.classList.remove("hidden");
+ }
+ } catch (error) {
+ messageDiv.textContent = "Failed to unregister. Please try again.";
+ messageDiv.className = "error";
+ messageDiv.classList.remove("hidden");
+ console.error("Error unregistering:", error);
+ }
+ }
+ });
+
// Initialize app
fetchActivities();
});
diff --git a/src/static/styles.css b/src/static/styles.css
index a533b32..b48fac7 100644
--- a/src/static/styles.css
+++ b/src/static/styles.css
@@ -74,6 +74,58 @@ section h3 {
margin-bottom: 8px;
}
+.participants-section {
+ margin-top: 15px;
+ padding-top: 12px;
+ border-top: 1px solid #e0e0e0;
+}
+
+.participants-header {
+ margin-bottom: 8px;
+ color: #1a237e;
+}
+
+.participants-list {
+ list-style-type: none;
+ margin-left: 0;
+ color: #555;
+}
+
+.participants-list li {
+ margin-bottom: 5px;
+ padding: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+}
+
+.participant-email {
+ flex-grow: 1;
+}
+
+.delete-btn {
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ font-size: 18px;
+ padding: 4px 8px;
+ margin-left: 10px;
+ transition: transform 0.2s;
+}
+
+.delete-btn:hover {
+ transform: scale(1.2);
+ background-color: transparent;
+}
+
+.no-participants {
+ font-style: italic;
+ color: #999;
+ margin: 0;
+}
+
.form-group {
margin-bottom: 15px;
}
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..ae26017
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests package for the Mergington High School API"""
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..732478b
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,241 @@
+"""
+Tests for the Mergington High School API
+"""
+
+import pytest
+from fastapi.testclient import TestClient
+from src.app import app, activities
+
+
+@pytest.fixture
+def client():
+ """Create a test client for the FastAPI app"""
+ return TestClient(app)
+
+
+@pytest.fixture(autouse=True)
+def reset_activities():
+ """Reset activities data before each test"""
+ # Store original state
+ original_activities = {}
+ for name, data in activities.items():
+ original_activities[name] = {
+ "description": data["description"],
+ "schedule": data["schedule"],
+ "max_participants": data["max_participants"],
+ "participants": data["participants"].copy()
+ }
+
+ yield
+
+ # Restore original state
+ for name, data in original_activities.items():
+ activities[name] = data
+
+
+class TestRoot:
+ """Tests for the root endpoint"""
+
+ def test_root_redirects_to_static(self, client):
+ """Test that root redirects to static index.html"""
+ response = client.get("/", follow_redirects=False)
+ assert response.status_code == 307
+ assert response.headers["location"] == "/static/index.html"
+
+
+class TestGetActivities:
+ """Tests for the GET /activities endpoint"""
+
+ def test_get_activities_returns_all_activities(self, client):
+ """Test that all activities are returned"""
+ response = client.get("/activities")
+ assert response.status_code == 200
+ data = response.json()
+ assert isinstance(data, dict)
+ assert len(data) > 0
+ assert "Chess Club" in data
+ assert "Programming Class" in data
+ assert "Gym Class" in data
+
+ def test_activities_have_required_fields(self, client):
+ """Test that each activity has all required fields"""
+ response = client.get("/activities")
+ data = response.json()
+
+ for activity_name, activity_data in data.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)
+ assert isinstance(activity_data["max_participants"], int)
+
+
+class TestSignupForActivity:
+ """Tests for the POST /activities/{activity_name}/signup endpoint"""
+
+ def test_signup_for_existing_activity(self, client):
+ """Test successful signup for an activity"""
+ response = client.post(
+ "/activities/Chess Club/signup?email=test@mergington.edu"
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert "message" in data
+ assert "test@mergington.edu" in data["message"]
+ assert "Chess Club" in data["message"]
+
+ # Verify the student was added
+ activities_response = client.get("/activities")
+ activities_data = activities_response.json()
+ assert "test@mergington.edu" in activities_data["Chess Club"]["participants"]
+
+ def test_signup_for_nonexistent_activity(self, client):
+ """Test signup for an activity that doesn't exist"""
+ response = client.post(
+ "/activities/Nonexistent Club/signup?email=test@mergington.edu"
+ )
+ assert response.status_code == 404
+ data = response.json()
+ assert "detail" in data
+ assert data["detail"] == "Activity not found"
+
+ def test_signup_twice_for_same_activity(self, client):
+ """Test that a student cannot sign up twice for the same activity"""
+ email = "duplicate@mergington.edu"
+
+ # First signup should succeed
+ response1 = client.post(
+ f"/activities/Chess Club/signup?email={email}"
+ )
+ assert response1.status_code == 200
+
+ # Second signup should fail
+ response2 = client.post(
+ f"/activities/Chess Club/signup?email={email}"
+ )
+ assert response2.status_code == 400
+ data = response2.json()
+ assert "detail" in data
+ assert data["detail"] == "Student already signed up for this activity"
+
+ def test_signup_with_special_characters_in_activity_name(self, client):
+ """Test signup with URL-encoded activity name"""
+ response = client.post(
+ "/activities/Art%20Club/signup?email=artist@mergington.edu"
+ )
+ assert response.status_code == 200
+
+ # Verify the student was added
+ activities_response = client.get("/activities")
+ activities_data = activities_response.json()
+ assert "artist@mergington.edu" in activities_data["Art Club"]["participants"]
+
+
+class TestUnregisterFromActivity:
+ """Tests for the DELETE /activities/{activity_name}/unregister endpoint"""
+
+ def test_unregister_from_activity(self, client):
+ """Test successful unregistration from an activity"""
+ email = "michael@mergington.edu"
+
+ # Verify student is initially registered (from default data)
+ activities_response = client.get("/activities")
+ activities_data = activities_response.json()
+ assert email in activities_data["Chess Club"]["participants"]
+
+ # Unregister
+ response = client.delete(
+ f"/activities/Chess Club/unregister?email={email}"
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert "message" in data
+ assert email in data["message"]
+ assert "Chess Club" in data["message"]
+
+ # Verify student was removed
+ activities_response = client.get("/activities")
+ activities_data = activities_response.json()
+ assert email not in activities_data["Chess Club"]["participants"]
+
+ def test_unregister_from_nonexistent_activity(self, client):
+ """Test unregistration from an activity that doesn't exist"""
+ response = client.delete(
+ "/activities/Nonexistent Club/unregister?email=test@mergington.edu"
+ )
+ assert response.status_code == 404
+ data = response.json()
+ assert "detail" in data
+ assert data["detail"] == "Activity not found"
+
+ def test_unregister_when_not_signed_up(self, client):
+ """Test unregistration when student is not signed up"""
+ response = client.delete(
+ "/activities/Chess Club/unregister?email=notsignedup@mergington.edu"
+ )
+ assert response.status_code == 400
+ data = response.json()
+ assert "detail" in data
+ assert data["detail"] == "Student not signed up for this activity"
+
+ def test_signup_after_unregister(self, client):
+ """Test that a student can re-signup after unregistering"""
+ email = "michael@mergington.edu"
+
+ # Unregister
+ response1 = client.delete(
+ f"/activities/Chess Club/unregister?email={email}"
+ )
+ assert response1.status_code == 200
+
+ # Re-signup
+ response2 = client.post(
+ f"/activities/Chess Club/signup?email={email}"
+ )
+ assert response2.status_code == 200
+
+ # Verify student is registered again
+ activities_response = client.get("/activities")
+ activities_data = activities_response.json()
+ assert email in activities_data["Chess Club"]["participants"]
+
+
+class TestEdgeCases:
+ """Tests for edge cases and error handling"""
+
+ def test_activity_name_case_sensitivity(self, client):
+ """Test that activity names are case-sensitive"""
+ response = client.post(
+ "/activities/chess club/signup?email=test@mergington.edu"
+ )
+ assert response.status_code == 404
+
+ def test_empty_email(self, client):
+ """Test signup with empty email"""
+ response = client.post(
+ "/activities/Chess Club/signup?email="
+ )
+ # FastAPI should handle this - either 422 for validation error or process it
+ assert response.status_code in [200, 422]
+
+ def test_multiple_activities_for_same_student(self, client):
+ """Test that a student can sign up for multiple activities"""
+ email = "busy@mergington.edu"
+
+ # Sign up for multiple activities
+ response1 = client.post(f"/activities/Chess Club/signup?email={email}")
+ assert response1.status_code == 200
+
+ response2 = client.post(f"/activities/Art Club/signup?email={email}")
+ assert response2.status_code == 200
+
+ response3 = client.post(f"/activities/Drama Club/signup?email={email}")
+ assert response3.status_code == 200
+
+ # Verify student is in all three activities
+ activities_response = client.get("/activities")
+ activities_data = activities_response.json()
+ assert email in activities_data["Chess Club"]["participants"]
+ assert email in activities_data["Art Club"]["participants"]
+ assert email in activities_data["Drama Club"]["participants"]