diff --git a/requirements.txt b/requirements.txt index 97dc7cd..6394691 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ fastapi uvicorn +pytest +httpx \ No newline at end of file diff --git a/src/activities.json b/src/activities.json new file mode 100644 index 0000000..38992e2 --- /dev/null +++ b/src/activities.json @@ -0,0 +1,80 @@ +{ + "Chess Club": { + "description": "Learn strategies and compete in chess tournaments", + "schedule": "Fridays, 3:30 PM - 5:00 PM", + "max_participants": 12, + "participants": [ + "michael@mergington.edu", + "daniel@mergington.edu" + ] + }, + "Programming Class": { + "description": "Learn programming fundamentals and build software projects", + "schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM", + "max_participants": 20, + "participants": [ + "emma@mergington.edu", + "sophia@mergington.edu" + ] + }, + "Gym Class": { + "description": "Physical education and sports activities", + "schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM", + "max_participants": 30, + "participants": [ + "john@mergington.edu", + "olivia@mergington.edu" + ] + }, + "Soccer Team": { + "description": "Practice and compete in interschool soccer matches", + "schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": [ + "alex@mergington.edu", + "mia@mergington.edu" + ] + }, + "Swimming Club": { + "description": "Swimming lessons and competitive training", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 15, + "participants": [ + "lucas@mergington.edu" + ] + }, + "Art Studio": { + "description": "Explore painting, drawing, and mixed media art", + "schedule": "Mondays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": [ + "isabella@mergington.edu", + "ava@mergington.edu" + ] + }, + "Drama Club": { + "description": "Acting, theater production, and performance arts", + "schedule": "Thursdays, 4:00 PM - 6:00 PM", + "max_participants": 20, + "participants": [ + "noah@mergington.edu" + ] + }, + "Debate Team": { + "description": "Develop critical thinking and public speaking skills", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": [ + "liam@mergington.edu", + "charlotte@mergington.edu" + ] + }, + "Science Olympiad": { + "description": "Compete in science and engineering challenges", + "schedule": "Fridays, 3:30 PM - 5:30 PM", + "max_participants": 15, + "participants": [ + "ethan@mergington.edu" + ] + } +} \ No newline at end of file diff --git a/src/app.py b/src/app.py index 4ebb1d9..a3441be 100644 --- a/src/app.py +++ b/src/app.py @@ -5,11 +5,14 @@ for extracurricular activities at Mergington High School. """ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Query from fastapi.staticfiles import StaticFiles from fastapi.responses import RedirectResponse + import os from pathlib import Path +import json +from threading import Lock app = FastAPI(title="Mergington High School API", description="API for viewing and signing up for extracurricular activities") @@ -19,27 +22,20 @@ app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent, "static")), name="static") -# In-memory activity database -activities = { - "Chess Club": { - "description": "Learn strategies and compete in chess tournaments", - "schedule": "Fridays, 3:30 PM - 5:00 PM", - "max_participants": 12, - "participants": ["michael@mergington.edu", "daniel@mergington.edu"] - }, - "Programming Class": { - "description": "Learn programming fundamentals and build software projects", - "schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM", - "max_participants": 20, - "participants": ["emma@mergington.edu", "sophia@mergington.edu"] - }, - "Gym Class": { - "description": "Physical education and sports activities", - "schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM", - "max_participants": 30, - "participants": ["john@mergington.edu", "olivia@mergington.edu"] - } -} + +# Persistent activities store +DATA_FILE = os.path.join(Path(__file__).parent, "activities.json") +_activities_lock = Lock() + +def load_activities(): + with _activities_lock: + with open(DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + +def save_activities(activities): + with _activities_lock: + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(activities, f, indent=2) @app.get("/") @@ -49,19 +45,33 @@ def root(): @app.get("/activities") def get_activities(): - return activities + return load_activities() @app.post("/activities/{activity_name}/signup") def signup_for_activity(activity_name: str, email: str): """Sign up a student for an activity""" - # Validate activity exists + activities = load_activities() if activity_name not in activities: raise HTTPException(status_code=404, detail="Activity not found") - - # Get the specific activity activity = activities[activity_name] - - # Add student + if email in activity["participants"]: + raise HTTPException(status_code=400, detail="Student already signed up for this activity") activity["participants"].append(email) + save_activities(activities) return {"message": f"Signed up {email} for {activity_name}"} + + +# Unregister a participant from an activity +@app.delete("/activities/{activity_name}/unregister") +def unregister_from_activity(activity_name: str, email: str = Query(...)): + """Unregister a student from an activity""" + activities = load_activities() + 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=400, detail="Student is not registered for this activity") + activity["participants"].remove(email) + save_activities(activities) + return {"message": f"Unregistered {email} from {activity_name}"} diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..508cfa9 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -20,13 +20,63 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; + // Participants section + let participantsHTML = ""; + if (details.participants.length > 0) { + participantsHTML = ` +
+ Participants: + +
+ `; + } else { + participantsHTML = ` +
+ No participants yet +
+ `; + } + + activityCard.innerHTML = `

${name}

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+ ${participantsHTML} `; + // Add event listeners for delete icons after rendering + setTimeout(() => { + activityCard.querySelectorAll('.delete-participant').forEach((icon) => { + icon.addEventListener('click', async (e) => { + const activity = icon.getAttribute('data-activity'); + const email = icon.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('Failed to remove participant.'); + } + }); + }); + }, 0); + activitiesList.appendChild(activityCard); // Add option to select dropdown @@ -62,6 +112,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + fetchActivities(); // Refresh activities list after successful signup } 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..38d96ba 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -142,3 +142,68 @@ footer { padding: 20px; color: #666; } + +.participants-section { + margin-top: 12px; + padding: 10px; + background-color: #e3eafc; + border-radius: 4px; +} + +.participants-section strong { + color: #1a237e; + font-size: 15px; +} + +.participants-list { + margin: 8px 0 0 18px; + padding-left: 0; + list-style-type: none; +} + +.no-bullets { + list-style-type: none !important; + margin-left: 0 !important; + padding-left: 0 !important; +} + +.participant-item { + color: #3949ab; + font-size: 15px; + margin-bottom: 2px; + display: flex; + align-items: center; + gap: 8px; +} + +.delete-participant { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: #e53935; + color: #fff; + border-radius: 50%; + font-size: 16px; + cursor: pointer; + margin-left: 8px; + border: none; + transition: background 0.2s, box-shadow 0.2s; + box-shadow: 0 1px 3px rgba(229,57,53,0.08); + line-height: 1; +} +.delete-participant:hover { + background: #b71c1c; + box-shadow: 0 2px 6px rgba(229,57,53,0.18); +} + +.no-participants { + background-color: #f5f5f5; + color: #888; + font-style: italic; + padding: 8px 10px; + border-radius: 4px; + margin-top: 10px; + text-align: left; +} diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..cc08309 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,37 @@ +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 = "pytestuser@mergington.edu" + activity = "Chess Club" + + # Ensure not already signed up + client.delete(f"/activities/{activity}/unregister", params={"email": test_email}) + + # Sign up + response = client.post(f"/activities/{activity}/signup?email={test_email}") + assert response.status_code == 200 + assert f"Signed up {test_email}" in response.json()["message"] + + # Check participant is in list + activities = client.get("/activities").json() + assert test_email in activities[activity]["participants"] + + # Unregister + response = client.delete(f"/activities/{activity}/unregister", params={"email": test_email}) + assert response.status_code == 200 + assert f"Unregistered {test_email}" in response.json()["message"] + + # Check participant is removed + activities = client.get("/activities").json() + assert test_email not in activities[activity]["participants"]