Skip to content
Open
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"]
},
"Soccer Team": {
"description": "Competitive soccer team with regular matches and tournaments",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["james@mergington.edu", "liam@mergington.edu"]
},
"Swimming Club": {
"description": "Swim training and competitive meets",
"schedule": "Mondays and Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["ava@mergington.edu", "noah@mergington.edu"]
},
"Art Studio": {
"description": "Painting, drawing, and mixed media art projects",
"schedule": "Thursdays, 3:30 PM - 5:30 PM",
"max_participants": 15,
"participants": ["isabella@mergington.edu", "mia@mergington.edu"]
},
"Drama Club": {
"description": "Theater performances, acting workshops, and stage production",
"schedule": "Tuesdays and Fridays, 3:30 PM - 5:00 PM",
"max_participants": 30,
"participants": ["ethan@mergington.edu", "charlotte@mergington.edu"]
},
"Robotics Team": {
"description": "Build and program robots for regional competitions",
"schedule": "Wednesdays, 3:30 PM - 6:00 PM",
"max_participants": 18,
"participants": ["william@mergington.edu", "benjamin@mergington.edu"]
},
"Debate Club": {
"description": "Develop critical thinking and public speaking through competitive debates",
"schedule": "Mondays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["amelia@mergington.edu", "lucas@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 for the activity
if email in activity["participants"]:
raise HTTPException(status_code=400, detail="Already signed up for this activity")

Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The signup endpoint does not validate whether the activity has reached its maximum capacity before adding a new participant. This allows more participants to sign up than max_participants allows.

Add a validation check:

if len(activity["participants"]) >= activity["max_participants"]:
    raise HTTPException(status_code=400, detail="Activity is full")
Suggested change
# Validate activity has not reached maximum capacity
if len(activity["participants"]) >= activity["max_participants"]:
raise HTTPException(status_code=400, 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}"}
Comment on lines +101 to 107
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The new unregister functionality lacks test coverage for the scenario where an activity reaches maximum capacity. There should be a test to verify that signups are rejected when len(participants) >= max_participants.

Add a test case like:

def test_signup_activity_full(self, reset_activities):
    """Test that signup fails when activity is at capacity"""
    # Fill up Chess Club (max 12)
    # Try to add 13th participant and verify it fails with 400

Copilot uses AI. Check for mistakes.


@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 for the activity
if email not in activity["participants"]:
raise HTTPException(status_code=400, detail="Not signed up for this activity")

# Remove student
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
49 changes: 49 additions & 0 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,35 @@ document.addEventListener("DOMContentLoaded", () => {
// Clear loading message
activitiesList.innerHTML = "";

// Clear existing options from dropdown (except the default option)
while (activitySelect.options.length > 1) {
activitySelect.remove(1);
}

// 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 participantsMarkup = details.participants?.length
? `<ul class="participants-list">
${details.participants.map((p) => `<li>${p} <button class="delete-btn" data-activity="${name}" data-email="${p}" aria-label="Delete ${p}">✕</button></li>`).join("")}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The aria-label attribute also uses unsanitized user input (email), which could lead to attribute injection attacks. An attacker could use an email like test" onclick="alert('xss')" data-x="@test.com to inject malicious attributes.

Ensure proper escaping or use a safer method to construct DOM elements with createElement() and set properties individually rather than using innerHTML.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Participant emails are directly interpolated into the HTML without sanitization, creating a potential XSS vulnerability. If a malicious email address containing HTML/JavaScript is stored (e.g., <script>alert('xss')</script>@test.com), it will be executed when the page is rendered.

Use textContent instead of template literals for user-generated content, or properly escape the email addresses before inserting them into the HTML.

Copilot uses AI. Check for mistakes.
</ul>`
: `<p class="participants-empty">Be the first to join this activity!</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">
<div class="participants-header">
<span class="participants-title">Participants</span>
<span class="participants-count">${details.participants.length}</span>
</div>
${participantsMarkup}
</div>
Comment on lines +27 to +44
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The name variable (activity name) is also directly interpolated into HTML without sanitization in multiple places. While activity names are currently hardcoded in the backend, if this ever becomes user-editable, it would introduce XSS vulnerabilities.

Consider sanitizing or escaping the name variable as well, or use DOM manipulation methods instead of innerHTML.

Copilot uses AI. Check for mistakes.
`;

activitiesList.appendChild(activityCard);
Expand All @@ -35,6 +52,36 @@ document.addEventListener("DOMContentLoaded", () => {
option.textContent = name;
activitySelect.appendChild(option);
});

// Add event listeners for delete buttons using event delegation
activitiesList.addEventListener("click", async (event) => {
if (event.target.classList.contains("delete-btn")) {
const activityName = event.target.dataset.activity;
const email = event.target.dataset.email;

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

if (response.ok) {
// Refresh the activities list
fetchActivities();
} else {
const result = await response.json();
alert(result.detail || "Failed to unregister participant");
}
} catch (error) {
alert("Failed to unregister participant");
console.error("Error unregistering:", error);
}
}
}
});
Comment on lines +56 to +84
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The event listener for delete buttons is added inside fetchActivities(), which means a new listener is attached every time the activities list is refreshed. This creates duplicate listeners that will cause the unregister action to be triggered multiple times.

Move the event listener registration outside of fetchActivities() so it's only registered once when the page loads.

Copilot uses AI. Check for mistakes.
} catch (error) {
activitiesList.innerHTML = "<p>Failed to load activities. Please try again later.</p>";
console.error("Error fetching activities:", error);
Expand Down Expand Up @@ -62,6 +109,8 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
// Refresh the activities list to show the new participant
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
70 changes: 70 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,73 @@ footer {
padding: 20px;
color: #666;
}

.participants {
margin-top: 12px;
padding: 12px;
border-radius: 6px;
background: linear-gradient(135deg, #eef2ff 0%, #f5f7ff 100%);
border: 1px solid #dde1ff;
}

.participants-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}

.participants-title {
font-weight: 700;
color: #1a237e;
}

.participants-count {
min-width: 28px;
padding: 3px 8px;
border-radius: 999px;
background-color: #1a237e;
color: #fff;
font-size: 12px;
text-align: center;
}

.participants-list {
list-style: none;
margin: 0;
padding-left: 0;
color: #37474f;
}

.participants-list li {
margin: 4px 0;
padding: 6px 8px;
border-radius: 4px;
background-color: #fff;
border: 1px solid #e3e6ff;
display: flex;
justify-content: space-between;
align-items: center;
}

.delete-btn {
background-color: #d32f2f;
color: white;
border: none;
padding: 2px 8px;
font-size: 14px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
margin-left: 10px;
}

.delete-btn:hover {
background-color: #b71c1c;
}

.participants-empty {
margin: 0;
color: #546e7a;
font-style: italic;
}
Empty file added tests/__init__.py
Empty file.
86 changes: 86 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import sys
from pathlib import Path

# Add src directory to path so we can import app
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

import pytest
from fastapi.testclient import TestClient
from app import app


@pytest.fixture
def client():
"""Create a test client for the FastAPI app"""
return TestClient(app)


@pytest.fixture
def reset_activities():
"""Reset activities to initial state before each test"""
from app import activities

initial_state = {
"Chess Club": {
"description": "Learn strategies and compete in chess tournaments",
"schedule": "Fridays, 3:30 PM - 5:00 PM",
"max_participants": 12,
"participants": ["michael@mergington.edu", "daniel@mergington.edu"]
},
"Programming Class": {
"description": "Learn programming fundamentals and build software projects",
"schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM",
"max_participants": 20,
"participants": ["emma@mergington.edu", "sophia@mergington.edu"]
},
"Gym Class": {
"description": "Physical education and sports activities",
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
"Soccer Team": {
"description": "Competitive soccer team with regular matches and tournaments",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["james@mergington.edu", "liam@mergington.edu"]
},
"Swimming Club": {
"description": "Swim training and competitive meets",
"schedule": "Mondays and Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["ava@mergington.edu", "noah@mergington.edu"]
},
"Art Studio": {
"description": "Painting, drawing, and mixed media art projects",
"schedule": "Thursdays, 3:30 PM - 5:30 PM",
"max_participants": 15,
"participants": ["isabella@mergington.edu", "mia@mergington.edu"]
},
"Drama Club": {
"description": "Theater performances, acting workshops, and stage production",
"schedule": "Tuesdays and Fridays, 3:30 PM - 5:00 PM",
"max_participants": 30,
"participants": ["ethan@mergington.edu", "charlotte@mergington.edu"]
},
"Robotics Team": {
"description": "Build and program robots for regional competitions",
"schedule": "Wednesdays, 3:30 PM - 6:00 PM",
"max_participants": 18,
"participants": ["william@mergington.edu", "benjamin@mergington.edu"]
},
"Debate Club": {
"description": "Develop critical thinking and public speaking through competitive debates",
"schedule": "Mondays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["amelia@mergington.edu", "lucas@mergington.edu"]
}
}

# Clear and reset
activities.clear()
activities.update(initial_state)
yield
# Reset after test
activities.clear()
activities.update(initial_state)
Loading