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
Binary file added .coverage
Binary file not shown.
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fastapi
uvicorn
httpx
watchfiles
watchfiles
pytest
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"]
},
"Basketball Team": {
"description": "Competitive basketball training and tournaments",
"schedule": "Mondays and Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 15,
"participants": ["james@mergington.edu"]
},
"Tennis Club": {
"description": "Learn tennis skills and compete in matches",
"schedule": "Tuesdays and Saturdays, 3:30 PM - 5:00 PM",
"max_participants": 12,
"participants": ["isabella@mergington.edu"]
},
"Art Studio": {
"description": "Explore painting, drawing, and sculpture techniques",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": ["lucas@mergington.edu", "ava@mergington.edu"]
},
"Drama Club": {
"description": "Theatre arts, script writing, and stage performances",
"schedule": "Fridays, 4:00 PM - 5:30 PM",
"max_participants": 25,
"participants": ["noah@mergington.edu"]
},
"Debate Team": {
"description": "Develop public speaking and critical thinking skills",
"schedule": "Mondays and Wednesdays, 3:30 PM - 4:30 PM",
"max_participants": 16,
"participants": ["mia@mergington.edu", "ethan@mergington.edu"]
},
"Science Club": {
"description": "Conduct experiments and explore scientific concepts",
"schedule": "Tuesdays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["charlotte@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
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}"}


@app.delete("/activities/{activity_name}/participants/{email}")
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")

activity = activities[activity_name]

# Check if participant is registered
if email not in activity["participants"]:
raise HTTPException(status_code=404, detail="Participant not found")

# Remove participant
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
44 changes: 44 additions & 0 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,53 @@ document.addEventListener("DOMContentLoaded", () => {
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
<div class="participants-section">
<p><strong>Participants (${details.participants.length}/${details.max_participants}):</strong></p>
<ul class="participants-list">
${details.participants.map(p => `<li><span class="participant-email">${p}</span><button class="delete-participant-btn" data-email="${p}" data-activity="${name}" type="button">×</button></li>`).join('')}
</ul>
</div>
`;

activitiesList.appendChild(activityCard);

// Add delete event listeners to participant buttons
const deleteButtons = activityCard.querySelectorAll(".delete-participant-btn");
deleteButtons.forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
const email = btn.getAttribute("data-email");
const activity = btn.getAttribute("data-activity");

try {
const response = await fetch(
`/activities/${encodeURIComponent(activity)}/participants/${encodeURIComponent(email)}`,
{ method: "DELETE" }
);

if (response.ok) {
// Refresh activities list after successful deletion
fetchActivities();
messageDiv.textContent = `Unregistered ${email} from ${activity}`;
messageDiv.className = "success";
} else {
const result = await response.json();
messageDiv.textContent = result.detail || "Failed to unregister";
messageDiv.className = "error";
}
messageDiv.classList.remove("hidden");
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
} catch (error) {
messageDiv.textContent = "Failed to unregister. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
console.error("Error unregistering:", error);
}
});
});

// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
Expand Down Expand Up @@ -62,6 +105,7 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
55 changes: 55 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,61 @@ section h3 {
margin-bottom: 8px;
}

.participants-section {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}

.participants-section p {
margin-bottom: 10px;
color: #555;
font-size: 14px;
}

.participants-list {
list-style: none;
margin-left: 0;
font-size: 13px;
color: #666;
}

.participants-list li {
margin-bottom: 5px;
padding: 4px 8px;
background-color: #f0f7ff;
border-radius: 3px;
border-left: 3px solid #0066cc;
display: flex;
justify-content: space-between;
align-items: center;
}

.participant-email {
flex-grow: 1;
}

.delete-participant-btn {
background: none;
border: none;
color: #c62828;
font-size: 20px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
margin-left: 8px;
transition: color 0.2s, transform 0.1s;
}

.delete-participant-btn:hover {
color: #d32f2f;
transform: scale(1.2);
}

.delete-participant-btn:active {
transform: scale(0.95);
}

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


@pytest.fixture
def client():
"""Test client fixture for FastAPI app testing."""
return TestClient(app)
60 changes: 60 additions & 0 deletions tests/test_activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Tests for GET /activities endpoint."""


def test_get_activities_returns_all_activities(client):
"""Test that GET /activities returns all activities with correct structure."""
# Arrange: No specific setup needed as activities are predefined in app

# Act: Make GET request to /activities
response = client.get("/activities")

# Assert: Response status is 200
assert response.status_code == 200

# Assert: Response contains all 9 activities
activities = response.json()
assert len(activities) == 9

# Assert: Each activity has the expected structure
expected_activities = [
"Chess Club", "Programming Class", "Gym Class", "Basketball Team",
"Tennis Club", "Art Studio", "Drama Club", "Debate Team", "Science Club"
]
assert set(activities.keys()) == set(expected_activities)

# Assert: Each activity has required fields
for activity_name, activity_data in activities.items():
assert "description" in activity_data
assert "schedule" in activity_data
assert "max_participants" in activity_data
assert "participants" in activity_data
assert isinstance(activity_data["participants"], list)


def test_get_activities_participant_counts(client):
"""Test that participant counts are correct for each activity."""
# Arrange: No specific setup needed

# Act: Make GET request to /activities
response = client.get("/activities")

# Assert: Response is successful
assert response.status_code == 200

activities = response.json()

# Assert: Specific participant counts match expected values
expected_counts = {
"Chess Club": 2, # michael@mergington.edu, daniel@mergington.edu
"Programming Class": 2, # emma@mergington.edu, sophia@mergington.edu
"Gym Class": 2, # john@mergington.edu, olivia@mergington.edu
"Basketball Team": 1, # james@mergington.edu
"Tennis Club": 1, # isabella@mergington.edu
"Art Studio": 2, # lucas@mergington.edu, ava@mergington.edu
"Drama Club": 1, # noah@mergington.edu
"Debate Team": 2, # mia@mergington.edu, ethan@mergington.edu
"Science Club": 1, # charlotte@mergington.edu
}

for activity_name, expected_count in expected_counts.items():
assert len(activities[activity_name]["participants"]) == expected_count
64 changes: 64 additions & 0 deletions tests/test_signup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Tests for POST /activities/{activity_name}/signup endpoint."""


def test_signup_successful(client):
"""Test successful signup for an activity."""
# Arrange: Choose an existing activity and a new email
activity_name = "Chess Club"
email = "newstudent@mergington.edu"

# Act: Make POST request to signup endpoint
response = client.post(f"/activities/{activity_name}/signup?email={email}")

# Assert: Response status is 200
assert response.status_code == 200

# Assert: Response contains success message
result = response.json()
assert "message" in result
assert f"Signed up {email} for {activity_name}" in result["message"]

# Assert: Participant was added to the activity
activities_response = client.get("/activities")
activities = activities_response.json()
assert email in activities[activity_name]["participants"]


def test_signup_non_existent_activity(client):
"""Test signup for a non-existent activity returns 404."""
# Arrange: Use an invalid activity name
invalid_activity = "NonExistentActivity"
email = "student@mergington.edu"

# Act: Make POST request to signup endpoint
response = client.post(f"/activities/{invalid_activity}/signup?email={email}")

# Assert: Response status is 404
assert response.status_code == 404

# Assert: Response contains appropriate error message
result = response.json()
assert "detail" in result
assert "Activity not found" in result["detail"]


def test_signup_duplicate_registration(client):
"""Test that duplicate signup for same activity returns 400."""
# Arrange: First, sign up a student
activity_name = "Programming Class"
email = "duplicatestudent@mergington.edu"

# Sign up once (should succeed)
first_response = client.post(f"/activities/{activity_name}/signup?email={email}")
assert first_response.status_code == 200

# Act: Attempt to sign up the same student again
second_response = client.post(f"/activities/{activity_name}/signup?email={email}")

# Assert: Second signup returns 400
assert second_response.status_code == 400

# Assert: Response contains appropriate error message
result = second_response.json()
assert "detail" in result
assert "Student already signed up" in result["detail"]
Loading
Loading