-
Notifications
You must be signed in to change notification settings - Fork 0
Improve student activity registration system #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| fastapi | ||
| uvicorn | ||
| pytest | ||
| httpx |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") | ||
|
Comment on lines
+58
to
+59
|
||
| 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}"} | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 = ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="participants-section"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <strong>Participants:</strong> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ul class="participants-list no-bullets"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${details.participants | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (p) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `<li class="participant-item">${p} <span class="delete-participant" title="Remove participant" data-activity="${name}" data-email="${p}">🗑️</span></li>` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join("")} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| participantsHTML = ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="participants-section no-participants"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <em>No participants yet</em> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| activityCard.innerHTML = ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h4>${name}</h4> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>${details.description}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p><strong>Schedule:</strong> ${details.schedule}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p><strong>Availability:</strong> ${spotsLeft} spots left</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+78
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | |
| 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.'); | |
| } | |
| }); | |
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
load_activities()function should handle the case where the data file doesn't exist or contains invalid JSON. Without error handling, the application will crash on startup ifactivities.jsonis missing or corrupted. Consider adding a try-except block to return default data or create the file if it doesn't exist.