Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e2a8186
Add extracurricular activities and signup validation to the API
pmca31 Feb 10, 2026
8b30a06
Add participants list and styling to activity cards
pmca31 Feb 10, 2026
b93df53
Add unregister functionality and update participant list display
pmca31 Feb 10, 2026
43d8aa3
Initial plan
Copilot Feb 10, 2026
c0aae0c
Initial plan
Copilot Feb 10, 2026
7ec80ae
Update tests/test_app.py
pmca31 Feb 10, 2026
6859f13
Initial plan
Copilot Feb 10, 2026
e45d7f0
Update tests/test_app.py
pmca31 Feb 10, 2026
64f5317
Fix XSS vulnerability by using DOM methods instead of innerHTML
Copilot Feb 10, 2026
a8c3b5c
Fix: Move unregister endpoint after activities dictionary definition
Copilot Feb 10, 2026
ce4844f
Remove data attributes and use closures for better security
Copilot Feb 10, 2026
5ea4b0f
Add accessibility improvements to delete icon
Copilot Feb 10, 2026
96fd9f5
Add email validation for empty and whitespace-only values
Copilot Feb 10, 2026
95ff5b7
Simplify email validation logic per code review feedback
Copilot Feb 10, 2026
26a9418
Merge pull request #4 from pmca31/copilot/sub-pr-2-again
pmca31 Feb 10, 2026
157cd33
Merge pull request #3 from pmca31/copilot/sub-pr-2
pmca31 Feb 10, 2026
b3e03d4
Merge branch 'accelerate-with-copilot' into copilot/sub-pr-2-another-one
pmca31 Feb 10, 2026
450fba0
Merge pull request #5 from pmca31/copilot/sub-pr-2-another-one
pmca31 Feb 10, 2026
1c31d8a
Initial plan
Copilot Feb 10, 2026
f886c59
Change unregister_from_activity from async to sync function
Copilot Feb 10, 2026
c5e5fc4
Merge pull request #6 from pmca31/copilot/sub-pr-2-yet-again
pmca31 Feb 10, 2026
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 @@
pytest
httpx
pytest-cov
fastapi
uvicorn
67 changes: 65 additions & 2 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
# ...existing code...

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "...existing code..." on line 3 appears to be a placeholder or merge artifact that should be removed. This comment doesn't provide any meaningful information and clutters the code.

Suggested change
# ...existing code...

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate imports detected. Lines 1-2 import FastAPI, HTTPException, Request, and JSONResponse, but lines 11-15 re-import FastAPI, HTTPException along with other modules. The imports at lines 1-2 should be removed, and if Request or JSONResponse are needed, they should be added to the import block at lines 11-15.

Copilot uses AI. Check for mistakes.
"""
High School Management System API

Expand All @@ -16,11 +19,46 @@

# Mount the static files directory
current_dir = Path(__file__).parent
app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent,
"static")), name="static")
app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent, "static")), name="static")

# In-memory activity database
activities = {
"Basketball Team": {
"description": "Join the school basketball team for training and competitions",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 15,
"participants": []
},
"Soccer Club": {
"description": "Practice soccer skills and play friendly matches",
"schedule": "Wednesdays, 3:30 PM - 5:30 PM",
"max_participants": 18,
"participants": []
},
"Drama Club": {
"description": "Participate in acting, stage production, and school plays",
"schedule": "Mondays, 4:00 PM - 5:30 PM",
"max_participants": 25,
"participants": []
},
"Art Workshop": {
"description": "Explore painting, drawing, and other visual arts",
"schedule": "Fridays, 2:30 PM - 4:00 PM",
"max_participants": 20,
"participants": []
},
"Math Olympiad": {
"description": "Prepare for math competitions and solve challenging problems",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 10,
"participants": []
},
"Science Club": {
"description": "Conduct experiments and explore scientific concepts",
"schedule": "Wednesdays, 4:00 PM - 5:00 PM",
"max_participants": 15,
"participants": []
},
"Chess Club": {
"description": "Learn strategies and compete in chess tournaments",
"schedule": "Fridays, 3:30 PM - 5:00 PM",
Expand Down Expand Up @@ -55,13 +93,38 @@ def get_activities():
@app.post("/activities/{activity_name}/signup")
def signup_for_activity(activity_name: str, email: str):
"""Sign up a student for an activity"""
# Validate email is not empty or whitespace-only
if not email.strip():
raise HTTPException(status_code=400, detail="Email cannot be empty or whitespace-only")

# 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"]:
Comment thread
pmca31 marked this conversation as resolved.
raise HTTPException(status_code=400, detail="Student already signed up for this activity")
Comment on lines +107 to +109

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing email validation: The endpoint accepts any string as an email parameter without validation. Consider adding email format validation to ensure data quality and prevent potential issues with malformed email addresses.

Copilot uses AI. Check for mistakes.


Comment on lines +110 to +111

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signup endpoint is missing validation to check if the activity is at max capacity before adding a participant. The test_max_participants_limit test expects a 400 error with "Activity is full" when capacity is exceeded, but this validation logic is not implemented in the signup endpoint.

Suggested change
# Validate activity is not at max capacity (if a limit is defined)
if "max_participants" in activity and activity["max_participants"] is not None:
if len(activity["participants"]) >= activity["max_participants"]:
raise HTTPException(status_code=400, detail="Activity is full")

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +111

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary blank line at line 116. This creates inconsistent spacing in the function and should be removed for cleaner code formatting.

Suggested change

Copilot uses AI. Check for mistakes.
# Add student
activity["participants"].append(email)
return {"message": f"Signed up {email} for {activity_name}"}
Comment thread
pmca31 marked this conversation as resolved.
Comment on lines 104 to 114

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signup_for_activity no longer enforces max_participants capacity before appending to participants. This makes it possible to overfill an activity and will break the expected 400 "Activity is full" behavior (see tests). Add a check comparing len(activity["participants"]) against activity["max_participants"] and raise HTTPException(400, "Activity is full") before appending.

Copilot uses AI. Check for mistakes.


@app.post("/activities/{activity_name}/unregister")
def unregister_from_activity(activity_name: str, email: str):
"""Unregister a student from an activity"""
# Validate email is not empty or whitespace-only
if not email.strip():
raise HTTPException(status_code=400, detail="Email cannot be empty or whitespace-only")

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="Participant not found")
activity["participants"].remove(email)
return {"message": f"{email} removed from {activity_name}"}
Comment thread
pmca31 marked this conversation as resolved.
102 changes: 96 additions & 6 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,102 @@ document.addEventListener("DOMContentLoaded", () => {

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

activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
`;
// Create title
const title = document.createElement("h4");
title.textContent = name;
activityCard.appendChild(title);

// Create description
const description = document.createElement("p");
description.textContent = details.description;
activityCard.appendChild(description);

// Create schedule
const schedule = document.createElement("p");
const scheduleLabel = document.createElement("strong");
scheduleLabel.textContent = "Schedule: ";
schedule.appendChild(scheduleLabel);
schedule.appendChild(document.createTextNode(details.schedule));
activityCard.appendChild(schedule);

// Create availability
const availability = document.createElement("p");
const availabilityLabel = document.createElement("strong");
availabilityLabel.textContent = "Availability: ";
availability.appendChild(availabilityLabel);
availability.appendChild(document.createTextNode(`${spotsLeft} spots left`));
activityCard.appendChild(availability);

// Create participants section
const participantsSection = document.createElement("div");
participantsSection.className = "participants-section";

const participantsLabel = document.createElement("strong");
participantsLabel.textContent = "Participants:";
participantsSection.appendChild(participantsLabel);

if (details.participants.length > 0) {
const participantsList = document.createElement("div");
participantsList.className = "participants-list";

details.participants.forEach((email) => {
const participantItem = document.createElement("div");
Comment on lines +58 to +62

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Participants markup uses <div class="participants-list"> / <div class="participant-item">, but the CSS suggests a list with bullets. Consider rendering participants as a <ul> with <li> items (or adjust CSS) so the UI matches the intended styling and improves semantics for assistive tech.

Suggested change
const participantsList = document.createElement("div");
participantsList.className = "participants-list";
details.participants.forEach((email) => {
const participantItem = document.createElement("div");
const participantsList = document.createElement("ul");
participantsList.className = "participants-list";
details.participants.forEach((email) => {
const participantItem = document.createElement("li");

Copilot uses AI. Check for mistakes.
participantItem.className = "participant-item";

const emailSpan = document.createElement("span");
emailSpan.className = "participant-email";
emailSpan.textContent = email;
participantItem.appendChild(emailSpan);

const deleteIcon = document.createElement("span");
deleteIcon.className = "delete-icon";
deleteIcon.title = "Remove participant";
deleteIcon.textContent = "🗑️";
deleteIcon.setAttribute("role", "button");
deleteIcon.setAttribute("aria-label", "Remove participant");
deleteIcon.setAttribute("tabindex", "0");

const handleDelete = async () => {
// Create a safe confirmation message
const confirmMsg = `Remove participant from activity?\n\nParticipant: ${email}\nActivity: ${name}`;
if (!confirm(confirmMsg)) return;
try {
const response = await fetch(`/activities/${encodeURIComponent(name)}/unregister?email=${encodeURIComponent(email)}`, {
method: "POST",
});
if (response.ok) {
fetchActivities();
} else {
const result = await response.json();
alert(result.detail || "Failed to remove participant.");
}
} catch (error) {
alert("Error removing participant.");
}
};

// Store data in closure instead of data attributes for better security
deleteIcon.addEventListener("click", handleDelete);
deleteIcon.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleDelete();
}
});

participantItem.appendChild(deleteIcon);
participantsList.appendChild(participantItem);
});

participantsSection.appendChild(participantsList);
} else {
const noParticipants = document.createElement("p");
noParticipants.className = "participants-none";
noParticipants.textContent = "No participants yet.";
participantsSection.appendChild(noParticipants);
}

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

// Add option to select dropdown
Expand Down Expand Up @@ -62,6 +151,7 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
fetchActivities(); // Refresh activities list
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
32 changes: 32 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,35 @@ footer {
padding: 20px;
color: #666;
}

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

.participants-section strong {
color: #1976d2;
font-size: 15px;
}

.participants-list {
margin-top: 6px;
margin-left: 18px;
list-style-type: disc;

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.participants-list is styled with list-style-type: disc, but the JS creates it as a <div> containing <div> items, so list bullets won’t render. Use semantic <ul>/<li> in the markup (and update CSS selectors accordingly), or remove list-style-related CSS and implement bullets another way.

Suggested change
list-style-type: disc;

Copilot uses AI. Check for mistakes.
}

.participant-item {
color: #333;
font-size: 14px;
padding: 2px 0;
}

.participants-none {
color: #757575;
font-style: italic;
font-size: 14px;
margin-top: 6px;
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delete icon (trash can emoji) added in the JavaScript is missing CSS styling to make it visually distinct and interactive. Consider adding styles for .delete-icon to include cursor pointer, color, hover effects, and proper spacing to improve usability.

Suggested change
}
}
.delete-icon {
cursor: pointer;
margin-left: 8px;
color: #9e9e9e;
font-size: 0.9em;
vertical-align: middle;
transition: color 0.2s ease, transform 0.2s ease;
}
.delete-icon:hover,
.delete-icon:focus {
color: #d32f2f;
transform: scale(1.1);
}

Copilot uses AI. Check for mistakes.
95 changes: 95 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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
assert isinstance(response.json(), dict)

def test_signup_and_unregister():
activity = "Basketball Team"
email = "testuser@example.com"
# Signup
signup_resp = client.post(f"/activities/{activity}/signup?email={email}")
assert signup_resp.status_code == 200 or signup_resp.status_code == 400
# Unregister
unregister_resp = client.post(f"/activities/{activity}/unregister?email={email}")
assert unregister_resp.status_code == 200 or unregister_resp.status_code == 400
Comment on lines +15 to +19

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test accepts both success (200) and error (400) responses without differentiating the expected outcome. This makes the test non-deterministic and unable to catch bugs. The test should clean up state or use unique identifiers to ensure predictable behavior and verify the correct response code.

Copilot uses AI. Check for mistakes.

def test_signup_nonexistent_activity():
email = "ghost@example.com"
resp = client.post("/activities/Nonexistent Activity/signup?email=" + email)
assert resp.status_code == 404
assert resp.json()["detail"] == "Activity not found"


def test_unregister_non_participant():
activity = "Drama Club"
email = "not_signed_up@example.com"
resp = client.post(f"/activities/{activity}/unregister?email={email}")
assert resp.status_code == 400
assert resp.json()["detail"] == "Participant not found"


def test_max_participants_limit():
activity = "Art Workshop"
emails = [f"user{i}@example.com" for i in range(1, 22)]
# Fill up the activity
for email in emails[:20]:
resp = client.post(f"/activities/{activity}/signup?email={email}")
assert resp.status_code == 200 or resp.status_code == 400
# Try to add one more
resp = client.post(f"/activities/{activity}/signup?email={emails[20]}")
assert resp.status_code == 400
assert resp.json()["detail"] == "Activity is full"


def test_static_index():
resp = client.get("/static/index.html")
assert resp.status_code == 200


def test_signup_twice():
activity = "Soccer Club"
email = "twiceuser@example.com"
# First signup
resp1 = client.post(f"/activities/{activity}/signup?email={email}")
assert resp1.status_code == 200
# Second signup should fail
resp2 = client.post(f"/activities/{activity}/signup?email={email}")
assert resp2.status_code == 400
assert resp2.json()["detail"] == "Student already signed up for this activity"
Comment thread
pmca31 marked this conversation as resolved.


def test_signup_empty_email():
activity = "Basketball Team"
# Test with empty email
resp = client.post(f"/activities/{activity}/signup?email=")
assert resp.status_code == 400
assert resp.json()["detail"] == "Email cannot be empty or whitespace-only"


def test_signup_whitespace_email():
activity = "Basketball Team"
# Test with whitespace-only email
resp = client.post(f"/activities/{activity}/signup?email= ")
assert resp.status_code == 400
assert resp.json()["detail"] == "Email cannot be empty or whitespace-only"


def test_unregister_empty_email():
activity = "Drama Club"
# Test with empty email
resp = client.post(f"/activities/{activity}/unregister?email=")
assert resp.status_code == 400
assert resp.json()["detail"] == "Email cannot be empty or whitespace-only"


def test_unregister_whitespace_email():
activity = "Drama Club"
# Test with whitespace-only email
resp = client.post(f"/activities/{activity}/unregister?email= ")
assert resp.status_code == 400
assert resp.json()["detail"] == "Email cannot be empty or whitespace-only"