Skip to content
Closed
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
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
fastapi
typer
pytest
requests
uvicorn
160 changes: 160 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,123 @@

"""
High School Management System API

A super simple FastAPI application that allows students to view and sign up
for extracurricular activities at Mergington High School.
"""

from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
import os
from pathlib import Path

app = FastAPI(title="Mergington High School API",
description="API for viewing and signing up for extracurricular activities")

# Mount the static files directory
current_dir = Path(__file__).parent
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"]
},
"Soccer Team": {
"description": "Competitive soccer training and interschool matches",
"schedule": "Mondays, Wednesdays, 4:00 PM - 6:00 PM",
"max_participants": 22,
"participants": ["liam@mergington.edu", "noah@mergington.edu"]
},
"Basketball Club": {
"description": "Skills practice, scrimmages, and local tournaments",
"schedule": "Tuesdays and Thursdays, 5:00 PM - 7:00 PM",
"max_participants": 18,
"participants": ["isabella@mergington.edu", "lucas@mergington.edu"]
},
"Art Studio": {
"description": "Painting, drawing, and creative workshops for all levels",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["mia@mergington.edu", "amelia@mergington.edu"]
},
"Drama Club": {
"description": "Acting, stagecraft, and production of school plays",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["sophia.k@mergington.edu", "ethan@mergington.edu"]
},
"Debate Team": {
"description": "Practice debate formats, public speaking, and competitions",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["ava@mergington.edu", "william@mergington.edu"]
},
"Robotics Club": {
"description": "Design, build, and program robots for challenges and fairs",
"schedule": "Mondays and Thursdays, 3:30 PM - 5:30 PM",
"max_participants": 20,
"participants": ["harper@mergington.edu", "jack@mergington.edu"]
}
}


@app.get("/")
def root():
return RedirectResponse(url="/static/index.html")


@app.get("/activities")
def get_activities():
return 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
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 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}"}

# Unregister endpoint (now correctly placed)
@app.post("/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=400, detail="Student not registered for this activity")
activity["participants"].remove(email)
return {"message": f"Removed {email} from {activity_name}"}
"""
High School Management System API

Expand Down Expand Up @@ -38,6 +158,42 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
"Soccer Team": {
"description": "Competitive soccer training and interschool matches",
"schedule": "Mondays, Wednesdays, 4:00 PM - 6:00 PM",
"max_participants": 22,
"participants": ["liam@mergington.edu", "noah@mergington.edu"]
},
"Basketball Club": {
"description": "Skills practice, scrimmages, and local tournaments",
"schedule": "Tuesdays and Thursdays, 5:00 PM - 7:00 PM",
"max_participants": 18,
"participants": ["isabella@mergington.edu", "lucas@mergington.edu"]
},
"Art Studio": {
"description": "Painting, drawing, and creative workshops for all levels",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["mia@mergington.edu", "amelia@mergington.edu"]
},
"Drama Club": {
"description": "Acting, stagecraft, and production of school plays",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["sophia.k@mergington.edu", "ethan@mergington.edu"]
},
"Debate Team": {
"description": "Practice debate formats, public speaking, and competitions",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["ava@mergington.edu", "william@mergington.edu"]
},
"Robotics Club": {
"description": "Design, build, and program robots for challenges and fairs",
"schedule": "Mondays and Thursdays, 3:30 PM - 5:30 PM",
"max_participants": 20,
"participants": ["harper@mergington.edu", "jack@mergington.edu"]
}
}

Expand All @@ -62,6 +218,10 @@ 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}"}
72 changes: 66 additions & 6 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,80 @@ document.addEventListener("DOMContentLoaded", () => {

const spotsLeft = details.max_participants - details.participants.length;

// Build participants list HTML with delete icon and no bullets
let participantsHTML = "";
if (details.participants.length > 0) {
participantsHTML = `
<div class="participants-section">
<span class="participants-title">Participants:</span>
<ul class="participants-list no-bullets">
${details.participants.map(p => `
<li class="participant-item">
<span class="participant-email">${p}</span>
<span class="delete-icon" title="Remove participant" data-activity="${name}" data-email="${p}">&#128465;</span>
</li>
`).join("")}
</ul>
</div>
`;
} else {
participantsHTML = `
<div class="participants-section">
<span class="participants-title">Participants:</span>
<p class="participants-none">No participants yet.</p>
</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}
`;

activitiesList.appendChild(activityCard);
activitiesList.appendChild(activityCard);

// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
option.textContent = name;
activitySelect.appendChild(option);
// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
option.textContent = name;
activitySelect.appendChild(option);
// Event delegation for delete icon click
document.getElementById("activities-list").addEventListener("click", async (event) => {
if (event.target.classList.contains("delete-icon")) {
const activity = event.target.getAttribute("data-activity");
const email = event.target.getAttribute("data-email");
if (confirm(`Remove ${email} from ${activity}?`)) {
try {
const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, {
method: "POST"
});
const result = await response.json();
if (response.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
}
messageDiv.classList.remove("hidden");
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
} catch (error) {
messageDiv.textContent = "Failed to remove participant. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
}
}
}
});
});
} catch (error) {
activitiesList.innerHTML = "<p>Failed to load activities. Please try again later.</p>";
Expand Down
33 changes: 33 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ section h3 {
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
position: relative;
}

.activity-card h4 {
Expand Down Expand Up @@ -142,3 +143,35 @@ footer {
padding: 20px;
color: #666;
}

.participants-section {
margin-top: 12px;
padding: 10px;
background: #eef4fa;
border-radius: 4px;
border: 1px solid #dde6f3;
}

.participants-title {
font-weight: bold;
color: #1a237e;
display: block;
margin-bottom: 6px;
}

.participants-list {
margin-left: 18px;
margin-bottom: 0;
color: #333;
}

.participants-list li {
margin-bottom: 4px;
list-style-type: disc;
}

.participants-none {
color: #888;
font-style: italic;
margin-left: 4px;
}
39 changes: 39 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 "Chess Club" in data
assert "Programming Class" in data


def test_signup_for_activity():
email = "newstudent@mergington.edu"
activity = "Chess Club"
# Ensure not already signed up
client.post(f"/activities/{activity}/unregister?email={email}")
response = client.post(f"/activities/{activity}/signup?email={email}")
assert response.status_code == 200
assert response.json()["message"] == f"Signed up {email} for {activity}"
# Try signing up again (should fail)
response = client.post(f"/activities/{activity}/signup?email={email}")
assert response.status_code == 400


def test_unregister_from_activity():
email = "newstudent@mergington.edu"
activity = "Chess Club"
# Ensure signed up
client.post(f"/activities/{activity}/signup?email={email}")
response = client.post(f"/activities/{activity}/unregister?email={email}")
assert response.status_code == 200
assert response.json()["message"] == f"Removed {email} from {activity}"
# Try unregistering again (should fail)
response = client.post(f"/activities/{activity}/unregister?email={email}")
assert response.status_code == 400