Add participant management features to activities#2
Conversation
Display current participants as a bulleted list on each activity card. Shows participant emails when enrolled, or a friendly message when empty. Includes styled divider and matching school theme colors.
- Add DELETE endpoint to unregister participants from activities - Display delete icon next to each participant - Remove bullet points from participant list - Add confirmation dialog before deletion - Auto-refresh activity list after successful deletion
There was a problem hiding this comment.
Pull request overview
This PR enhances the activities app with participant management capabilities by exposing an unregister API, adding backend validation for duplicate signups, and updating the frontend to display and remove participants from activity cards.
Changes:
- Added backend validation to prevent duplicate signups and introduced a new
DELETE /activities/{activity_name}/unregisterendpoint. - Updated the frontend to render a “Current Participants” section per activity card and allow removing a participant with confirmation.
- Added a pytest-based API test suite and included test dependencies in
requirements.txt.
Reviewed changes
Copilot reviewed 5 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 seeded activities, prevents duplicate signups, and introduces an unregister endpoint. |
src/static/app.js |
Renders participant lists on activity cards, refreshes UI after mutations, and adds delete-with-confirm behavior. |
src/static/styles.css |
Styles the participant section and delete icon for the activity cards. |
tests/test_api.py |
Adds API tests for listing activities, signup flows, and unregister flows. |
tests/__init__.py |
Marks tests as a package (docstring only). |
requirements.txt |
Adds pytest and httpx for running the new test suite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Refresh the activities list | ||
| await fetchActivities(); | ||
|
|
There was a problem hiding this comment.
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.
| // 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} | ||
| `; | ||
|
|
There was a problem hiding this comment.
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.
| // 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); |
| ${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> |
There was a problem hiding this comment.
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.
| <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> |
| # Validate student is not already signed up | ||
| if email in activity["participants"]: | ||
| raise HTTPException(status_code=400, detail="Student is already signed up") | ||
|
|
There was a problem hiding this comment.
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).
| # 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") |
Enhance the API with signup validation for extracurricular activities, a participant section on activity cards, and a feature to delete participants with confirmation. This update improves user experience by displaying current participants and ensuring proper management of enrollments.