Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# For testing
pytest
httpx
pytest-asyncio
fastapi
uvicorn
22 changes: 22 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"""
Expand Down
50 changes: 50 additions & 0 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div class="participants-section">
<strong>Participants:</strong>
<ul class="participants-list">
${details.participants.map(email => `
<li class="participant-item" style="list-style:none;display:flex;align-items:center;gap:6px;">
<span>${email}</span>
<button class="delete-participant-btn" title="Remove participant" data-activity="${name}" data-email="${email}" style="background:none;border:none;color:#c62828;cursor:pointer;font-size:18px;line-height:1;">&#10006;</button>
</li>
`).join("")}
</ul>
</div>
`;
} else {
participantsSection = `
<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>
${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;
Expand Down Expand Up @@ -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";
Expand Down
52 changes: 52 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
40 changes: 40 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -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})