Skip to content
Open
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": ["[email protected]", "[email protected]"]
},
"Programming Class": {
"description": "Learn programming fundamentals and build software projects",
"schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM",
"max_participants": 20,
"participants": ["[email protected]", "[email protected]"]
},
"Gym Class": {
"description": "Physical education and sports activities",
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["[email protected]", "[email protected]"]
},
"Soccer Team": {
"description": "Competitive soccer training and interschool matches",
"schedule": "Mondays, Wednesdays, 4:00 PM - 6:00 PM",
"max_participants": 22,
"participants": ["[email protected]", "[email protected]"]
},
"Basketball Club": {
"description": "Skills practice, scrimmages, and local tournaments",
"schedule": "Tuesdays and Thursdays, 5:00 PM - 7:00 PM",
"max_participants": 18,
"participants": ["[email protected]", "[email protected]"]
},
"Art Studio": {
"description": "Painting, drawing, and creative workshops for all levels",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["[email protected]", "[email protected]"]
},
"Drama Club": {
"description": "Acting, stagecraft, and production of school plays",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["[email protected]", "[email protected]"]
},
"Debate Team": {
"description": "Practice debate formats, public speaking, and competitions",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["[email protected]", "[email protected]"]
},
"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": ["[email protected]", "[email protected]"]
}
}


@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": ["[email protected]", "[email protected]"]
},
"Soccer Team": {
"description": "Competitive soccer training and interschool matches",
"schedule": "Mondays, Wednesdays, 4:00 PM - 6:00 PM",
"max_participants": 22,
"participants": ["[email protected]", "[email protected]"]
},
"Basketball Club": {
"description": "Skills practice, scrimmages, and local tournaments",
"schedule": "Tuesdays and Thursdays, 5:00 PM - 7:00 PM",
"max_participants": 18,
"participants": ["[email protected]", "[email protected]"]
},
"Art Studio": {
"description": "Painting, drawing, and creative workshops for all levels",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["[email protected]", "[email protected]"]
},
"Drama Club": {
"description": "Acting, stagecraft, and production of school plays",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["[email protected]", "[email protected]"]
},
"Debate Team": {
"description": "Practice debate formats, public speaking, and competitions",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["[email protected]", "[email protected]"]
},
"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": ["[email protected]", "[email protected]"]
}
}

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 = "[email protected]"
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 = "[email protected]"
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