diff --git a/requirements.txt b/requirements.txt index 97dc7cd..9078b9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ +# For testing +pytest +httpx +pytest-asyncio fastapi uvicorn diff --git a/src/app.py b/src/app.py index cab7d58..ebebb88 100644 --- a/src/app.py +++ b/src/app.py @@ -10,10 +10,21 @@ from fastapi.responses import RedirectResponse import os from pathlib import Path +from fastapi.responses import JSONResponse + app = FastAPI(title="Mergington High School API", description="API for viewing and signing up for extracurricular activities") +# Redirect root URL to static index.html +@app.get("/") +def root(): + return RedirectResponse(url="/static/index.html") + +@app.get("/activities") +def get_activities(): + return JSONResponse(content=activities) + # Mount the static files directory current_dir = Path(__file__).parent app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent, @@ -83,6 +94,17 @@ } } +@app.delete("/activities/{activity_name}/unregister") +def unregister_from_activity(activity_name: str, email: str): + """Remove a student from an activity""" + if activity_name not in activities: + raise HTTPException(status_code=404, detail="Activity not found") + activity = activities[activity_name] + if email not in activity["participants"]: + raise HTTPException(status_code=404, detail="Participant not found in this activity") + activity["participants"].remove(email) + return {"message": f"Removed {email} from {activity_name}"} + @app.post("/activities/{activity_name}/signup") def signup_for_activity(activity_name: str, email: str): """Sign up a student for an activity""" diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..c71a87c 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -20,15 +20,64 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; + // Participants section + let participantsSection = ""; + if (details.participants.length > 0) { + participantsSection = ` +
+ Participants: + +
+ `; + } else { + participantsSection = ` +
+ No participants yet. +
+ `; + } + activityCard.innerHTML = `

${name}

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+ ${participantsSection} `; activitiesList.appendChild(activityCard); + // Add event listeners for delete buttons (after DOM insertion) + setTimeout(() => { + activityCard.querySelectorAll('.delete-participant-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + const activity = btn.getAttribute('data-activity'); + const email = btn.getAttribute('data-email'); + if (!confirm(`Remove ${email} from ${activity}?`)) return; + try { + const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, { + method: 'DELETE', + }); + const result = await response.json(); + if (response.ok) { + fetchActivities(); + } else { + alert(result.detail || 'Failed to remove participant.'); + } + } catch (err) { + alert('Error removing participant.'); + } + }); + }); + }, 0); + // Add option to select dropdown const option = document.createElement("option"); option.value = name; @@ -62,6 +111,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..49c2509 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,58 @@ section h3 { margin-bottom: 8px; } +/* Add some spacing for the participants section */ +.participants-section { + margin-top: 12px; + padding: 10px; + background-color: #eef2fa; + border-radius: 4px; +} + +.participants-section strong { + color: #3949ab; + display: block; + margin-bottom: 6px; +} + +.participants-list { + margin-left: 0; + margin-bottom: 0; + color: #333; + list-style: none; + padding-left: 0; + /* Remove default bullets */ +} + +.participant-item { + list-style: none; + display: flex; + align-items: center; + gap: 6px; +} + +.delete-participant-btn { + background: none; + border: none; + color: #c62828; + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0 4px; + margin-left: 4px; + transition: color 0.2s; +} +.delete-participant-btn:hover { + color: #ff1744; +} + +.participants-section.no-participants { + background-color: #f5f5f5; + color: #888; + font-style: italic; + padding: 8px 10px; +} + .form-group { margin-bottom: 15px; } diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..81c8b46 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,40 @@ +import pytest +from fastapi.testclient import TestClient +from src.app import app + +client = TestClient(app) + +def test_get_activities(): + response = client.get("/activities") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, dict) + assert "Chess Club" in data + +def test_signup_and_unregister(): + test_email = "testuser@mergington.edu" + activity = "Chess Club" + # Ensure user is not already signed up + client.delete(f"/activities/{activity}/unregister", params={"email": test_email}) + # Sign up + response = client.post(f"/activities/{activity}/signup", params={"email": test_email}) + assert response.status_code == 200 + assert f"Signed up {test_email}" in response.json()["message"] + # Unregister + response = client.delete(f"/activities/{activity}/unregister", params={"email": test_email}) + assert response.status_code == 200 + assert f"Removed {test_email}" in response.json()["message"] + +def test_signup_duplicate(): + test_email = "duplicate@mergington.edu" + activity = "Chess Club" + # Clean up + client.delete(f"/activities/{activity}/unregister", params={"email": test_email}) + # First signup + client.post(f"/activities/{activity}/signup", params={"email": test_email}) + # Duplicate signup + response = client.post(f"/activities/{activity}/signup", params={"email": test_email}) + assert response.status_code == 400 + assert "already signed up" in response.json()["detail"] + # Clean up + client.delete(f"/activities/{activity}/unregister", params={"email": test_email})