-
Notifications
You must be signed in to change notification settings - Fork 0
Improve student activity registration system #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| fastapi | ||
| uvicorn | ||
| httpx | ||
| watchfiles | ||
| watchfiles | ||
| pytest |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,27 +4,63 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const signupForm = document.getElementById("signup-form"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const messageDiv = document.getElementById("message"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function showMessage(message, type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageDiv.textContent = message; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageDiv.className = type; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageDiv.className = type; | |
| // Remove any previous status classes while preserving baseline classes | |
| messageDiv.classList.remove("success", "error"); | |
| if (type) { | |
| messageDiv.classList.add(type); | |
| } |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||||
| import copy | ||||||||
|
|
||||||||
| import pytest | ||||||||
| from fastapi.testclient import TestClient | ||||||||
|
|
||||||||
| from src.app import activities, app | ||||||||
|
|
||||||||
|
|
||||||||
| @pytest.fixture() | ||||||||
| def client(): | ||||||||
| return TestClient(app) | ||||||||
|
||||||||
| return TestClient(app) | |
| with TestClient(app) as client: | |
| yield client |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| from src.app import activities | ||
|
|
||
|
|
||
| def test_get_activities_returns_expected_structure(client): | ||
| # Arrange | ||
|
|
||
| # Act | ||
| response = client.get("/activities") | ||
|
|
||
| # Assert | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert isinstance(data, dict) | ||
| assert "Chess Club" in data | ||
| assert { | ||
| "description", | ||
| "schedule", | ||
| "max_participants", | ||
| "participants", | ||
| }.issubset(data["Chess Club"].keys()) | ||
|
|
||
|
|
||
| def test_signup_adds_participant_successfully(client): | ||
| # Arrange | ||
| activity_name = "Chess Club" | ||
| email = "new.student@mergington.edu" | ||
|
|
||
| # Act | ||
| response = client.post( | ||
| f"/activities/{activity_name}/signup", | ||
| params={"email": email}, | ||
| ) | ||
|
|
||
| # Assert | ||
| assert response.status_code == 200 | ||
| assert response.json()["message"] == f"Signed up {email} for {activity_name}" | ||
| assert email in activities[activity_name]["participants"] | ||
|
|
||
|
|
||
| def test_delete_unregisters_participant_successfully(client): | ||
| # Arrange | ||
| activity_name = "Chess Club" | ||
| email = "remove.me@mergington.edu" | ||
| activities[activity_name]["participants"].append(email) | ||
|
|
||
| # Act | ||
| response = client.delete( | ||
| f"/activities/{activity_name}/participants", | ||
| params={"email": email}, | ||
| ) | ||
|
|
||
| # Assert | ||
| assert response.status_code == 200 | ||
| assert response.json()["message"] == f"Removed {email} from {activity_name}" | ||
| assert email not in activities[activity_name]["participants"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
signup_for_activityadds the student without enforcingmax_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.