Improve student activity registration system#2
Conversation
… mejorar la interfaz de usuario
…de participantes en actividades
There was a problem hiding this comment.
Pull request overview
Adds participant-management capabilities to the activities app, including an API/UI flow to unregister students, richer participant display in the UI, and supporting automated tests.
Changes:
- Added a DELETE
/activities/{activity_name}/participantsendpoint and duplicate-signup validation. - Updated the frontend to render participants per activity and allow removing them via the new endpoint, with refreshed messaging.
- Introduced pytest-based endpoint/validation tests and a fixture to reset in-memory activity state between tests.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/app.py |
Expands the in-memory activities list and adds/updates API logic for signup validation and participant removal. |
src/static/app.js |
Renders participant lists with remove buttons; wires UI actions to the DELETE endpoint; centralizes user messaging. |
src/static/styles.css |
Styles participant section/list and remove button to improve readability/UX. |
tests/conftest.py |
Adds TestClient fixture and autouse state restore for test isolation. |
tests/test_endpoints.py |
Adds endpoint-level tests for listing activities, signing up, and unregistering. |
tests/test_validations.py |
Adds validation tests for unknown activities, duplicate signups, and invalid removals. |
requirements.txt |
Adds pytest to support the new test suite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const participantsMarkup = details.participants.length | ||
| ? `<ul class="participant-list">${details.participants | ||
| .map( | ||
| (participant) => | ||
| `<li class="participant-item"> | ||
| <span class="participant-email">${participant}</span> | ||
| <button | ||
| type="button" | ||
| class="participant-remove" | ||
| data-activity="${name}" | ||
| data-email="${participant}" | ||
| aria-label="Remove ${participant} from ${name}" | ||
| title="Unregister participant" | ||
| >×</button> | ||
| </li>` | ||
| ) | ||
| .join("")}</ul>` | ||
| : '<p class="participant-empty">No participants yet. Be the first to sign up.</p>'; | ||
|
|
||
| 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"> | ||
| <p class="participants-title">Participants</p> | ||
| ${participantsMarkup} | ||
| </div> | ||
| `; | ||
|
|
There was a problem hiding this comment.
activityCard.innerHTML interpolates server-provided fields (including details.participants, which contains user-supplied email values) directly into HTML. Because /signup accepts an arbitrary string for email, a malicious value can be persisted and rendered, resulting in DOM XSS. Build the card/participants list using DOM APIs (createElement + textContent) or apply proper escaping/sanitization before inserting into innerHTML/data-* attributes.
| const participantsMarkup = details.participants.length | |
| ? `<ul class="participant-list">${details.participants | |
| .map( | |
| (participant) => | |
| `<li class="participant-item"> | |
| <span class="participant-email">${participant}</span> | |
| <button | |
| type="button" | |
| class="participant-remove" | |
| data-activity="${name}" | |
| data-email="${participant}" | |
| aria-label="Remove ${participant} from ${name}" | |
| title="Unregister participant" | |
| >×</button> | |
| </li>` | |
| ) | |
| .join("")}</ul>` | |
| : '<p class="participant-empty">No participants yet. Be the first to sign up.</p>'; | |
| 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"> | |
| <p class="participants-title">Participants</p> | |
| ${participantsMarkup} | |
| </div> | |
| `; | |
| // Title | |
| const titleEl = document.createElement("h4"); | |
| titleEl.textContent = name; | |
| activityCard.appendChild(titleEl); | |
| // Description | |
| const descriptionEl = document.createElement("p"); | |
| descriptionEl.textContent = details.description; | |
| activityCard.appendChild(descriptionEl); | |
| // Schedule | |
| const scheduleEl = document.createElement("p"); | |
| const scheduleStrong = document.createElement("strong"); | |
| scheduleStrong.textContent = "Schedule:"; | |
| scheduleEl.appendChild(scheduleStrong); | |
| scheduleEl.appendChild(document.createTextNode(" " + details.schedule)); | |
| activityCard.appendChild(scheduleEl); | |
| // Availability | |
| const availabilityEl = document.createElement("p"); | |
| const availabilityStrong = document.createElement("strong"); | |
| availabilityStrong.textContent = "Availability:"; | |
| availabilityEl.appendChild(availabilityStrong); | |
| availabilityEl.appendChild( | |
| document.createTextNode(" " + spotsLeft + " spots left") | |
| ); | |
| activityCard.appendChild(availabilityEl); | |
| // Participants section | |
| const participantsSection = document.createElement("div"); | |
| participantsSection.className = "participants-section"; | |
| const participantsTitle = document.createElement("p"); | |
| participantsTitle.className = "participants-title"; | |
| participantsTitle.textContent = "Participants"; | |
| participantsSection.appendChild(participantsTitle); | |
| if (details.participants.length) { | |
| const listEl = document.createElement("ul"); | |
| listEl.className = "participant-list"; | |
| details.participants.forEach((participant) => { | |
| const itemEl = document.createElement("li"); | |
| itemEl.className = "participant-item"; | |
| const emailSpan = document.createElement("span"); | |
| emailSpan.className = "participant-email"; | |
| emailSpan.textContent = participant; | |
| itemEl.appendChild(emailSpan); | |
| const removeButton = document.createElement("button"); | |
| removeButton.type = "button"; | |
| removeButton.className = "participant-remove"; | |
| removeButton.dataset.activity = name; | |
| removeButton.dataset.email = participant; | |
| removeButton.setAttribute( | |
| "aria-label", | |
| `Remove ${participant} from ${name}` | |
| ); | |
| removeButton.title = "Unregister participant"; | |
| removeButton.textContent = "×"; | |
| itemEl.appendChild(removeButton); | |
| listEl.appendChild(itemEl); | |
| }); | |
| participantsSection.appendChild(listEl); | |
| } else { | |
| const emptyEl = document.createElement("p"); | |
| emptyEl.className = "participant-empty"; | |
| emptyEl.textContent = | |
| "No participants yet. Be the first to sign up."; | |
| participantsSection.appendChild(emptyEl); | |
| } | |
| activityCard.appendChild(participantsSection); |
|
|
||
| function showMessage(message, type) { | ||
| messageDiv.textContent = message; | ||
| messageDiv.className = type; | ||
| messageDiv.classList.remove("hidden"); | ||
|
|
||
| setTimeout(() => { | ||
| messageDiv.classList.add("hidden"); |
There was a problem hiding this comment.
showMessage schedules a new setTimeout each time it is called, but doesn’t cancel any prior timeout. If multiple messages are shown within 5 seconds, an earlier timeout can hide the newer message unexpectedly. Track the timeout id and clearTimeout it before scheduling a new hide.
| function showMessage(message, type) { | |
| messageDiv.textContent = message; | |
| messageDiv.className = type; | |
| messageDiv.classList.remove("hidden"); | |
| setTimeout(() => { | |
| messageDiv.classList.add("hidden"); | |
| let messageHideTimeoutId = null; | |
| function showMessage(message, type) { | |
| // Cancel any previously scheduled hide to avoid hiding a newer message | |
| if (messageHideTimeoutId !== null) { | |
| clearTimeout(messageHideTimeoutId); | |
| messageHideTimeoutId = null; | |
| } | |
| messageDiv.textContent = message; | |
| messageDiv.className = type; | |
| messageDiv.classList.remove("hidden"); | |
| messageHideTimeoutId = setTimeout(() => { | |
| messageDiv.classList.add("hidden"); | |
| messageHideTimeoutId = null; |
|
|
||
| function showMessage(message, type) { | ||
| messageDiv.textContent = message; | ||
| messageDiv.className = type; |
There was a problem hiding this comment.
messageDiv.className = type replaces all classes on the message element. This makes it easy to accidentally drop baseline styling classes (e.g., a shared message class) or future utility classes. Prefer using classList to toggle success/error while preserving any existing base classes.
| messageDiv.className = type; | |
| // Remove any previous status classes while preserving baseline classes | |
| messageDiv.classList.remove("success", "error"); | |
| if (type) { | |
| messageDiv.classList.add(type); | |
| } |
|
|
||
| @pytest.fixture() | ||
| def client(): | ||
| return TestClient(app) |
There was a problem hiding this comment.
The client fixture returns a TestClient instance without closing it. Prefer a yield-style fixture (or with TestClient(app) as client: ...) so FastAPI lifespan startup/shutdown handlers run deterministically and resources/sockets are released between tests.
| return TestClient(app) | |
| with TestClient(app) as client: | |
| yield client |
| # 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 adds the student without enforcing max_participants, so activities can be overbooked and the UI’s "spots left" can become negative/inaccurate. Add a capacity check before appending and return a 400 error when the activity is full.
| # Validate activity is not over capacity (if a max is defined) | |
| if "max_participants" in activity and len(activity["participants"]) >= activity["max_participants"]: | |
| raise HTTPException(status_code=400, detail="Activity is full") |
This pull request adds participant management features for school activities, including the ability to register and unregister students via the API and user interface. It also introduces UI enhancements for displaying and managing participants, and adds comprehensive backend and validation tests to ensure correct behavior.
Backend enhancements:
activitiesdata structure insrc/app.py, each with descriptions, schedules, participant limits, and initial participants./activities/{activity_name}/participantsto allow unregistering a participant from an activity, with appropriate validation and error handling.Frontend/UI improvements:
src/static/app.jsto display a list of participants for each activity, including a remove button for each participant, and to handle participant removal via the new API endpoint. Improved feedback messages and error handling for user actions. [1] [2]src/static/styles.cssto visually distinguish the participants section, participant list, and remove button for a better user experience.Testing and validation:
tests/test_endpoints.pyto verify activity listing, successful signup, and successful participant removal.tests/test_validations.pyto ensure proper error responses for duplicate signups, unknown activities, and attempts to remove non-participants.tests/conftest.pyto restore the activities state between tests, ensuring test isolation.