Improve student activity registration system#2
Conversation
This reverts commit 3645f34.
There was a problem hiding this comment.
Pull request overview
This PR expands the Mergington High School activities app by adding more activities, introducing an unregister API endpoint, updating the frontend to display/manage participants, and adding API endpoint tests.
Changes:
- Add new activities and add backend support for unregistering students (DELETE signup endpoint) plus duplicate-signup validation.
- Enhance the frontend to render participants per activity and allow removing participants from the UI.
- Introduce a pytest-based test suite covering core API behaviors.
Reviewed changes
Copilot reviewed 4 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/app.py |
Adds more activities, prevents duplicate signup, and adds an unregister endpoint. |
src/static/app.js |
Renders participants + remove buttons; wires up DELETE requests and refreshes activity data after changes. |
src/static/styles.css |
Styles the new participants section and remove button. |
tests/test_app.py |
Adds automated tests for listing, signup (success/duplicate/not found), and unregister flows. |
requirements.txt |
Adds pytest to dependencies. |
Comments suppressed due to low confidence (1)
src/static/app.js:52
fetchActivities()repopulates the<select id="activity">by appending options but never clears existing options. Now thatfetchActivities()is called after signup/removal, the dropdown will accumulate duplicate entries. Clear the select (preserving the placeholder option) before appending new options.
activitiesList.innerHTML = "";
// Populate activities list
Object.entries(activities).forEach(([name, details]) => {
const activityCard = document.createElement("div");
activityCard.className = "activity-card";
const spotsLeft = details.max_participants - details.participants.length;
const participantsList = details.participants.length
? details.participants.map(email =>
`<li>
<span class="participant-email">${email}</span>
<button class="remove-btn" data-activity="${name}" data-email="${email}" title="Remove participant">✕</button>
</li>`
).join("")
: "<li class='no-participants'>No participants yet</li>";
activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
<div class="participants-section">
<strong>Participants:</strong>
<ul class="participants-list">
${participantsList}
</ul>
</div>
`;
activitiesList.appendChild(activityCard);
// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
option.textContent = name;
activitySelect.appendChild(option);
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Validate student is not already signed up | ||
| if email in activity["participants"]: | ||
| raise HTTPException(status_code=400, detail="Student already signed up") | ||
|
|
There was a problem hiding this comment.
signup_for_activity appends the student without enforcing max_participants, so activities can be overbooked (and the frontend will show negative spots left). Add a capacity check before appending and return an appropriate 4xx error when the activity is full.
| # Validate activity capacity | |
| if len(activity["participants"]) >= activity["max_participants"]: | |
| raise HTTPException(status_code=400, detail="Activity is full") |
| const participantsList = details.participants.length | ||
| ? details.participants.map(email => | ||
| `<li> | ||
| <span class="participant-email">${email}</span> | ||
| <button class="remove-btn" data-activity="${name}" data-email="${email}" title="Remove participant">✕</button> | ||
| </li>` | ||
| ).join("") | ||
| : "<li class='no-participants'>No participants yet</li>"; | ||
|
|
||
| activityCard.innerHTML = ` | ||
| <h4>${name}</h4> | ||
| <p>${details.description}</p> | ||
| <p><strong>Schedule:</strong> ${details.schedule}</p> | ||
| <p><strong>Availability:</strong> ${spotsLeft} spots left</p> | ||
| <div class="participants-section"> | ||
| <strong>Participants:</strong> | ||
| <ul class="participants-list"> | ||
| ${participantsList} | ||
| </ul> | ||
| </div> | ||
| `; | ||
|
|
There was a problem hiding this comment.
The UI is built via innerHTML with interpolated name, details.description, and especially email (user-controlled via signup). This allows HTML/attribute injection (XSS) if an email contains markup/quotes. Prefer creating DOM nodes and setting textContent, or sanitize/escape values before inserting into HTML.
| const participantsList = details.participants.length | |
| ? details.participants.map(email => | |
| `<li> | |
| <span class="participant-email">${email}</span> | |
| <button class="remove-btn" data-activity="${name}" data-email="${email}" title="Remove participant">✕</button> | |
| </li>` | |
| ).join("") | |
| : "<li class='no-participants'>No participants yet</li>"; | |
| activityCard.innerHTML = ` | |
| <h4>${name}</h4> | |
| <p>${details.description}</p> | |
| <p><strong>Schedule:</strong> ${details.schedule}</p> | |
| <p><strong>Availability:</strong> ${spotsLeft} spots left</p> | |
| <div class="participants-section"> | |
| <strong>Participants:</strong> | |
| <ul class="participants-list"> | |
| ${participantsList} | |
| </ul> | |
| </div> | |
| `; | |
| // Build activity card content safely using DOM APIs to avoid XSS | |
| const titleEl = document.createElement("h4"); | |
| titleEl.textContent = name; | |
| const descEl = document.createElement("p"); | |
| descEl.textContent = details.description; | |
| const scheduleEl = document.createElement("p"); | |
| const scheduleStrong = document.createElement("strong"); | |
| scheduleStrong.textContent = "Schedule:"; | |
| scheduleEl.appendChild(scheduleStrong); | |
| scheduleEl.appendChild(document.createTextNode(" " + details.schedule)); | |
| const availabilityEl = document.createElement("p"); | |
| const availabilityStrong = document.createElement("strong"); | |
| availabilityStrong.textContent = "Availability:"; | |
| availabilityEl.appendChild(availabilityStrong); | |
| availabilityEl.appendChild(document.createTextNode(" " + spotsLeft + " spots left")); | |
| const participantsSection = document.createElement("div"); | |
| participantsSection.className = "participants-section"; | |
| const participantsLabel = document.createElement("strong"); | |
| participantsLabel.textContent = "Participants:"; | |
| participantsSection.appendChild(participantsLabel); | |
| const participantsUl = document.createElement("ul"); | |
| participantsUl.className = "participants-list"; | |
| if (details.participants.length) { | |
| details.participants.forEach(email => { | |
| const li = document.createElement("li"); | |
| const emailSpan = document.createElement("span"); | |
| emailSpan.className = "participant-email"; | |
| emailSpan.textContent = email; | |
| const removeBtn = document.createElement("button"); | |
| removeBtn.className = "remove-btn"; | |
| removeBtn.dataset.activity = name; | |
| removeBtn.dataset.email = email; | |
| removeBtn.title = "Remove participant"; | |
| removeBtn.textContent = "✕"; | |
| li.appendChild(emailSpan); | |
| li.appendChild(removeBtn); | |
| participantsUl.appendChild(li); | |
| }); | |
| } else { | |
| const li = document.createElement("li"); | |
| li.className = "no-participants"; | |
| li.textContent = "No participants yet"; | |
| participantsUl.appendChild(li); | |
| } | |
| participantsSection.appendChild(participantsUl); | |
| activityCard.appendChild(titleEl); | |
| activityCard.appendChild(descEl); | |
| activityCard.appendChild(scheduleEl); | |
| activityCard.appendChild(availabilityEl); | |
| activityCard.appendChild(participantsSection); |
| ? details.participants.map(email => | ||
| `<li> | ||
| <span class="participant-email">${email}</span> | ||
| <button class="remove-btn" data-activity="${name}" data-email="${email}" title="Remove participant">✕</button> |
There was a problem hiding this comment.
The remove button is icon-only and relies on title for labeling, which is not a reliable accessible name. Add an aria-label (and/or visually hidden text) so screen readers announce what the button does (ideally including the participant email).
| <button class="remove-btn" data-activity="${name}" data-email="${email}" title="Remove participant">✕</button> | |
| <button class="remove-btn" data-activity="${name}" data-email="${email}" title="Remove participant" aria-label="Remove participant ${email}">✕</button> |
| background-color: #ffebee; | ||
| color: #b71c1c; | ||
| } | ||
|
|
There was a problem hiding this comment.
.remove-btn has hover styling but no explicit focus styling. Add a :focus-visible (or :focus) style so keyboard users can see which remove button is focused.
| .remove-btn:focus-visible, | |
| .remove-btn:focus { | |
| background-color: #ffebee; | |
| color: #b71c1c; | |
| outline: 2px solid #b71c1c; | |
| outline-offset: 2px; | |
| } |
This pull request adds major new features and improvements to the Mergington High School activities application. The main changes include expanding the list of available activities, implementing the ability for students to unregister from activities, enhancing the frontend to display and manage participants, and introducing comprehensive automated tests for the API endpoints.
Backend features and API improvements:
activitiesdata structure insrc/app.py, including Soccer Team, Basketball Team, Art Club, Drama Club, Debate Club, and Math Olympiad, each with their own schedule, participant list, and limits./activities/{activity_name}/signupwith DELETE), including validation and error handling. Also added validation to prevent duplicate signups.Frontend enhancements:
src/static/app.js) to display the list of participants for each activity, including a "remove" button next to each participant.src/static/styles.css.Testing:
tests/test_app.py) covering activity listing, signup (including duplicate and nonexistent activity cases), and unregistering (including error cases), ensuring robust API behavior.