Skip to content
Merged
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
pytest
httpx
uvicorn
58 changes: 58 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["[email protected]", "[email protected]"]
},
# Sports related activities
"Soccer Team": {
"description": "Join the school soccer team and compete in local matches",
"schedule": "Wednesdays, 4:00 PM - 5:30 PM",
"max_participants": 18,
"participants": ["[email protected]", "[email protected]"]
},
"Basketball Club": {
"description": "Practice basketball skills and play friendly games",
"schedule": "Mondays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["[email protected]", "[email protected]"]
},
# Artistic activities
"Art Workshop": {
"description": "Explore painting, drawing, and sculpture techniques",
"schedule": "Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 16,
"participants": ["[email protected]", "[email protected]"]
},
"Drama Club": {
"description": "Act, direct, and produce school plays and performances",
"schedule": "Tuesdays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["[email protected]", "[email protected]"]
},
# Intellectual activities
"Math Olympiad": {
"description": "Prepare for math competitions and solve challenging problems",
"schedule": "Fridays, 4:00 PM - 5:30 PM",
"max_participants": 10,
"participants": ["[email protected]", "[email protected]"]
},
"Science Club": {
"description": "Conduct experiments and explore scientific concepts",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 14,
"participants": ["[email protected]", "[email protected]"]
}
}

Expand All @@ -62,6 +101,25 @@ def signup_for_activity(activity_name: str, email: str):
# Get the specific activity
activity = activities[activity_name]

# Validate student is not already signed up
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The comment has incorrect indentation - it should align with the code it describes rather than being outdented to column 0.

Suggested change
# Validate student is not already signed up
# Validate student is not already signed up

Copilot uses AI. Check for mistakes.
if email in activity["participants"]:
raise HTTPException(status_code=400, detail="Student already signed up for this activity")

# Add student
activity["participants"].append(email)
return {"message": f"Signed up {email} for {activity_name}"}


# Unregister a participant from an activity
from fastapi import Query
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The import statement for Query should be moved to the top of the file with other imports. Having imports in the middle of the file violates Python conventions and can make the code harder to maintain.

Copilot uses AI. Check for mistakes.

@app.delete("/activities/{activity_name}/unregister")
def unregister_from_activity(activity_name: str, email: str = Query(...)):
"""Remove a participant from an activity"""
if activity_name not in activities:
raise HTTPException(status_code=404, detail="Activity not found")
activity = activities[activity_name]
if email not in activity["participants"]:
raise HTTPException(status_code=400, detail="Participant not found in this activity")
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
66 changes: 60 additions & 6 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,69 @@ document.addEventListener("DOMContentLoaded", () => {

const spotsLeft = details.max_participants - details.participants.length;

activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
`;

// Create participants list HTML
let participantsHTML = '';
if (details.participants.length > 0) {
participantsHTML = `
<div class="participants-section">
<strong>Participants:</strong>
<ul class="participants-list">
${details.participants.map(p => `
<li class="participant-item">
<span class="participant-email">${p}</span>
<span class="delete-participant" title="Unregister" data-activity="${name}" data-email="${p}">&#128465;</span>
</li>
`).join('')}
</ul>
</div>
`;
} else {
participantsHTML = `
<div class="participants-section empty">
<strong>Participants:</strong>
<p>No one has signed up yet.</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>
${participantsHTML}
`;

activitiesList.appendChild(activityCard);

// Add event listeners for delete icons
activityCard.querySelectorAll('.delete-participant').forEach(icon => {
icon.addEventListener('click', async (e) => {
const activityName = icon.getAttribute('data-activity');
const email = icon.getAttribute('data-email');
if (confirm(`Unregister ${email} from ${activityName}?`)) {
try {
const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`, {
method: 'DELETE',
});
const result = await response.json();
if (response.ok) {
fetchActivities(); // Refresh list
messageDiv.textContent = result.message;
messageDiv.style.color = 'green';
} else {
messageDiv.textContent = result.detail || 'Failed to unregister.';
messageDiv.style.color = 'red';
}
} catch (err) {
messageDiv.textContent = 'Error unregistering participant.';
messageDiv.style.color = 'red';
}
}
});
});

// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
Expand Down
59 changes: 59 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ section h3 {
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
box-shadow: 0 2px 8px rgba(0, 102, 204, 0.08);
transition: box-shadow 0.2s;
}

.activity-card h4 {
Expand All @@ -74,6 +76,63 @@ section h3 {
margin-bottom: 8px;
}

.participants-section {
margin-top: 12px;
padding: 10px 12px;
background: #e3f2fd;
border-radius: 4px;
border: 1px solid #bbdefb;
}

.participants-section strong {
color: #1976d2;
font-size: 15px;
display: block;
margin-bottom: 6px;
}


.participants-list {
list-style-type: none;
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
}

.participant-item {
display: flex;
align-items: center;
margin-bottom: 4px;
color: #333;
font-size: 14px;
}

.participant-email {
flex: 1;
}

.delete-participant {
cursor: pointer;
color: #d32f2f;
margin-left: 10px;
font-size: 16px;
transition: color 0.2s;
}
.delete-participant:hover {
color: #b71c1c;
text-shadow: 0 0 2px #d32f2f;
}

.participants-section.empty {
background: #fffde7;
border: 1px solid #ffe082;
}
.participants-section.empty p {
color: #bdb76b;
font-style: italic;
margin-bottom: 0;
}

.form-group {
margin-bottom: 15px;
}
Expand Down
61 changes: 61 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import pytest
from fastapi.testclient import TestClient
from src.app import app

client = TestClient(app)

# Test GET /activities
def test_get_activities():
response = client.get("/activities")
assert response.status_code == 200
data = response.json()
assert "Chess Club" in data
assert "Programming Class" in data
assert "Gym Class" in data

# Test POST /activities/{activity_name}/signup
@pytest.mark.parametrize("activity,email", [
("Chess Club", "[email protected]"),
("Programming Class", "[email protected]"),
])
def test_signup_for_activity(activity, email):
response = client.post(f"/activities/{activity}/signup?email={email}")
assert response.status_code == 200
assert f"Signed up {email} for {activity}" in response.json()["message"]

# Test duplicate signup
def test_duplicate_signup():
activity = "Chess Club"
email = "[email protected]"
response = client.post(f"/activities/{activity}/signup?email={email}")
assert response.status_code == 400
assert "already signed up" in response.json()["detail"]

# Test DELETE /activities/{activity_name}/unregister
@pytest.mark.parametrize("activity,email", [
("Chess Club", "[email protected]"),
("Programming Class", "[email protected]"),
])
def test_unregister_from_activity(activity, email):
response = client.delete(f"/activities/{activity}/unregister?email={email}")
assert response.status_code == 200
assert f"Unregistered {email} from {activity}" in response.json()["message"]

# Test unregister non-existent participant
def test_unregister_nonexistent():
activity = "Chess Club"
email = "[email protected]"
response = client.delete(f"/activities/{activity}/unregister?email={email}")
assert response.status_code == 400
assert "Participant not found" in response.json()["detail"]

# Test activity not found
@pytest.mark.parametrize("endpoint", [
"/activities/Unknown/[email protected]",
"/activities/Unknown/[email protected]",
])
def test_activity_not_found(endpoint):
method = "post" if "signup" in endpoint else "delete"
response = getattr(client, method)(endpoint)
assert response.status_code == 404
assert "Activity not found" in response.json()["detail"]