diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..167fe26 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "uvicorn": true + } +} \ No newline at end of file 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..3fc84ee 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"] + }, + "Soccer Team": { + "description": "Join the varsity soccer team and compete in league matches", + "schedule": "Mondays and Wednesdays, 4:00 PM - 6:00 PM", + "max_participants": 25, + "participants": ["liam@mergington.edu", "noah@mergington.edu"] + }, + "Basketball Team": { + "description": "Practice basketball skills and play competitive games", + "schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM", + "max_participants": 15, + "participants": ["ava@mergington.edu", "isabella@mergington.edu"] + }, + "Drama Club": { + "description": "Perform in theatrical productions and develop acting skills", + "schedule": "Thursdays, 3:30 PM - 5:30 PM", + "max_participants": 25, + "participants": ["mia@mergington.edu", "charlotte@mergington.edu"] + }, + "Art Club": { + "description": "Explore various art mediums including painting, drawing, and sculpture", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["ethan@mergington.edu", "amelia@mergington.edu"] + }, + "Debate Team": { + "description": "Develop critical thinking and public speaking skills through competitive debates", + "schedule": "Tuesdays, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": ["james@mergington.edu", "harper@mergington.edu"] + }, + "Science Club": { + "description": "Conduct experiments and explore scientific concepts through hands-on projects", + "schedule": "Fridays, 3:30 PM - 5:00 PM", + "max_participants": 20, + "participants": ["benjamin@mergington.edu", "evelyn@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..b781e29 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -12,6 +12,9 @@ document.addEventListener("DOMContentLoaded", () => { // Clear loading message activitiesList.innerHTML = ""; + + // Clear and reset activity dropdown + activitySelect.innerHTML = ''; // Populate activities list Object.entries(activities).forEach(([name, details]) => { @@ -25,10 +28,41 @@ document.addEventListener("DOMContentLoaded", () => {

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+
+ Participants: + ${details.participants.length > 0 + ? `` + : `

No participants yet. Be the first to sign up!

` + } +
`; activitiesList.appendChild(activityCard); + // Add event listeners for delete buttons + activityCard.querySelectorAll('.delete-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const activity = btn.dataset.activity; + const email = btn.dataset.email; + try { + const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, { + method: 'DELETE' + }); + const result = await response.json(); + if (response.ok) { + fetchActivities(); // Refresh activities list + } else { + alert(result.detail || 'An error occurred'); + } + } catch (error) { + alert('Failed to unregister. Please try again.'); + console.error('Error unregistering:', error); + } + }); + }); + // Add option to select dropdown const option = document.createElement("option"); option.value = name; @@ -62,6 +96,9 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + + // Refresh activities list to show updated participants + 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..93cf129 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,54 @@ section h3 { margin-bottom: 8px; } +.participants-section { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e0e0e0; +} + +.participants-section strong { + display: block; + margin-bottom: 8px; + color: #1a237e; +} + +.participants-list { + list-style-type: none; + padding-left: 0; + margin: 0; +} + +.participants-list li { + padding: 3px 0; + color: #555; + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.delete-btn { + background: none; + border: none; + color: #d32f2f; + cursor: pointer; + font-size: 16px; + font-weight: bold; + padding: 0 5px; +} + +.delete-btn:hover { + color: #b71c1c; +} + +.no-participants { + font-style: italic; + color: #999; + margin: 0; + font-size: 14px; +} + .form-group { margin-bottom: 15px; } diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..e85c128 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,73 @@ +import pytest +from httpx import AsyncClient +from src.app import app + +@pytest.mark.asyncio +async def test_get_activities(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.get("/activities") + assert response.status_code == 200 + data = response.json() + assert "Chess Club" in data + assert "participants" in data["Chess Club"] + +@pytest.mark.asyncio +async def test_root_redirect(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.get("/") + assert response.status_code == 307 # Redirect + assert response.headers["location"] == "/static/index.html" + +@pytest.mark.asyncio +async def test_signup_success(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.post("/activities/Chess%20Club/signup?email=test@mergington.edu") + assert response.status_code == 200 + data = response.json() + assert "Signed up" in data["message"] + +@pytest.mark.asyncio +async def test_signup_already_signed(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + # First signup + await client.post("/activities/Chess%20Club/signup?email=test2@mergington.edu") + # Second + response = await client.post("/activities/Chess%20Club/signup?email=test2@mergington.edu") + assert response.status_code == 400 + data = response.json() + assert "already signed up" in data["detail"] + +@pytest.mark.asyncio +async def test_signup_activity_not_found(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.post("/activities/NonExistent/signup?email=test@mergington.edu") + assert response.status_code == 404 + data = response.json() + assert "Activity not found" in data["detail"] + +@pytest.mark.asyncio +async def test_unregister_success(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + # Signup first + await client.post("/activities/Chess%20Club/signup?email=unreg@mergington.edu") + # Unregister + response = await client.delete("/activities/Chess%20Club/unregister?email=unreg@mergington.edu") + assert response.status_code == 200 + data = response.json() + assert "Unregistered" in data["message"] + +@pytest.mark.asyncio +async def test_unregister_not_signed(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.delete("/activities/Chess%20Club/unregister?email=notsigned@mergington.edu") + assert response.status_code == 400 + data = response.json() + assert "not signed up" in data["detail"] + +@pytest.mark.asyncio +async def test_unregister_activity_not_found(): + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.delete("/activities/NonExistent/unregister?email=test@mergington.edu") + assert response.status_code == 404 + data = response.json() + assert "Activity not found" in data["detail"] \ No newline at end of file