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
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
fastapi
uvicorn
pytest
httpx
59 changes: 59 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,42 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
"Basketball Team": {
"description": "Competitive basketball training and inter-school matches",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 15,
"participants": ["james@mergington.edu", "liam@mergington.edu"]
},
"Swimming Club": {
"description": "Swimming techniques and competitive training",
"schedule": "Mondays and Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["ava@mergington.edu", "noah@mergington.edu"]
},
"Drama Club": {
"description": "Acting, theater production, and performing arts",
"schedule": "Wednesdays, 3:30 PM - 5:30 PM",
"max_participants": 25,
"participants": ["isabella@mergington.edu", "ethan@mergington.edu"]
},
"Art Studio": {
"description": "Painting, drawing, and mixed media art projects",
"schedule": "Thursdays, 3:00 PM - 5:00 PM",
"max_participants": 18,
"participants": ["mia@mergington.edu", "lucas@mergington.edu"]
},
"Debate Team": {
"description": "Develop critical thinking and public speaking through debates",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 16,
"participants": ["charlotte@mergington.edu", "william@mergington.edu"]
},
"Science Olympiad": {
"description": "Competitive science events and hands-on experiments",
"schedule": "Tuesdays, 3:30 PM - 5:30 PM",
"max_participants": 20,
"participants": ["amelia@mergington.edu", "benjamin@mergington.edu"]
}
}

Expand All @@ -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 is already signed up")

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

signup_for_activity doesn’t enforce max_participants. As a result, signups can exceed capacity even though the UI displays remaining spots, leading to inconsistent behavior and overbooking. Add a check before appending (e.g., if len(participants) >= max_participants then return a 400/409 with an "Activity is full"-style detail).

Suggested change
# Validate activity capacity
max_participants = activity.get("max_participants")
if max_participants is not None and len(activity["participants"]) >= max_participants:
raise HTTPException(status_code=409, detail="Activity is full")

Copilot uses AI. Check for mistakes.
# 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 is not signed up for this activity")

# Remove student
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
75 changes: 75 additions & 0 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,37 @@ document.addEventListener("DOMContentLoaded", () => {

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

// Build participants list
let participantsList = '';
if (details.participants.length > 0) {
participantsList = `
<div class="participants-section">
<strong>Current Participants:</strong>
<ul class="participants-list">
${details.participants.map(email => `
<li>
<span class="participant-email">${email}</span>
<span class="delete-icon" data-activity="${name}" data-email="${email}" title="Remove participant">🗑️</span>
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The delete control is a clickable <span> which is not keyboard-focusable and lacks an accessible name beyond the title tooltip. For accessibility, use a semantic <button type="button"> (or add role="button", tabindex="0", and key handlers) and provide an aria-label so screen readers and keyboard users can remove participants.

Suggested change
<span class="delete-icon" data-activity="${name}" data-email="${email}" title="Remove participant">🗑️</span>
<button type="button" class="delete-icon" data-activity="${name}" data-email="${email}" title="Remove participant" aria-label="Remove participant ${email} from ${name}">🗑️</button>

Copilot uses AI. Check for mistakes.
</li>
`).join('')}
</ul>
</div>
`;
} else {
participantsList = `
<div class="participants-section">
<strong>Current Participants:</strong>
<p class="no-participants">No participants yet. Be the first to sign up!</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>
${participantsList}
`;

Comment on lines +23 to 55
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The participants list HTML interpolates email directly into both text and attribute contexts (data-email="...") via innerHTML. Since emails originate from user input, this enables HTML/attribute injection (XSS) if a crafted value contains quotes or markup. Build these nodes with document.createElement + textContent/dataset, or escape/sanitize values before inserting into the template string.

Suggested change
// Build participants list
let participantsList = '';
if (details.participants.length > 0) {
participantsList = `
<div class="participants-section">
<strong>Current Participants:</strong>
<ul class="participants-list">
${details.participants.map(email => `
<li>
<span class="participant-email">${email}</span>
<span class="delete-icon" data-activity="${name}" data-email="${email}" title="Remove participant">🗑️</span>
</li>
`).join('')}
</ul>
</div>
`;
} else {
participantsList = `
<div class="participants-section">
<strong>Current Participants:</strong>
<p class="no-participants">No participants yet. Be the first to sign up!</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>
${participantsList}
`;
// Build activity card content (static parts)
activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
`;
// Build participants section using DOM APIs to avoid injecting untrusted HTML
const participantsSection = document.createElement("div");
participantsSection.className = "participants-section";
const participantsTitle = document.createElement("strong");
participantsTitle.textContent = "Current Participants:";
participantsSection.appendChild(participantsTitle);
if (details.participants.length > 0) {
const participantsUl = document.createElement("ul");
participantsUl.className = "participants-list";
details.participants.forEach((email) => {
const li = document.createElement("li");
const emailSpan = document.createElement("span");
emailSpan.className = "participant-email";
emailSpan.textContent = email;
const deleteSpan = document.createElement("span");
deleteSpan.className = "delete-icon";
deleteSpan.dataset.activity = name;
deleteSpan.dataset.email = email;
deleteSpan.title = "Remove participant";
deleteSpan.textContent = "🗑️";
li.appendChild(emailSpan);
li.appendChild(deleteSpan);
participantsUl.appendChild(li);
});
participantsSection.appendChild(participantsUl);
} else {
const noParticipantsMsg = document.createElement("p");
noParticipantsMsg.className = "no-participants";
noParticipantsMsg.textContent = "No participants yet. Be the first to sign up!";
participantsSection.appendChild(noParticipantsMsg);
}
activityCard.appendChild(participantsSection);

Copilot uses AI. Check for mistakes.
activitiesList.appendChild(activityCard);
Expand Down Expand Up @@ -59,6 +85,9 @@ document.addEventListener("DOMContentLoaded", () => {
const result = await response.json();

if (response.ok) {
// Refresh the activities list
await fetchActivities();

Comment on lines +88 to +90
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

fetchActivities() is now called after successful signup/unregister, but it appends <option>s to the activity <select> without clearing existing options. With the new refresh behavior, this will cause duplicate dropdown entries after each signup/delete. Consider resetting activitySelect (and preserving the selected value) before repopulating it.

Copilot uses AI. Check for mistakes.
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
Expand All @@ -81,6 +110,52 @@ document.addEventListener("DOMContentLoaded", () => {
}
});

// Handle participant deletion
activitiesList.addEventListener("click", async (event) => {
if (event.target.classList.contains("delete-icon")) {
const activityName = event.target.getAttribute("data-activity");
const email = event.target.getAttribute("data-email");

if (!confirm(`Are you sure you want to unregister ${email} from ${activityName}?`)) {
return;
}

try {
const response = await fetch(
`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`,
{
method: "DELETE",
}
);

const result = await response.json();

if (response.ok) {
// Refresh the activities list
await fetchActivities();

messageDiv.textContent = result.message;
messageDiv.className = "success";
} else {
messageDiv.textContent = result.detail || "Failed to unregister participant";
messageDiv.className = "error";
}

messageDiv.classList.remove("hidden");

// Hide message after 5 seconds
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
} catch (error) {
messageDiv.textContent = "Failed to unregister participant. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
console.error("Error unregistering participant:", error);
}
}
});

// Initialize app
fetchActivities();
});
51 changes: 51 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,57 @@ section h3 {
margin-bottom: 8px;
}

.participants-section {
margin-top: 15px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
}

.participants-section strong {
color: #1a237e;
display: block;
margin-bottom: 8px;
}

.participants-list {
list-style-type: none;
margin-left: 0;
color: #555;
}

.participants-list li {
padding: 8px 0;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}

.participant-email {
flex-grow: 1;
}

.delete-icon {
cursor: pointer;
font-size: 16px;
margin-left: 10px;
opacity: 0.6;
transition: opacity 0.2s, transform 0.2s;
user-select: none;
}

.delete-icon:hover {
opacity: 1;
transform: scale(1.2);
}

.no-participants {
color: #999;
font-style: italic;
margin: 5px 0 0 0;
font-size: 14px;
}

.form-group {
margin-bottom: 15px;
}
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for Mergington High School API"""
Loading
Loading