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
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
fastapi
uvicorn
httpx
watchfiles
watchfiles
pytest
pytest-asyncio
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": "Join our competitive basketball team and play in tournaments",
"schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM",
"max_participants": 15,
"participants": ["alex@mergington.edu"]
},
"Tennis Club": {
"description": "Learn tennis skills and compete in matches",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 16,
"participants": ["lucas@mergington.edu"]
},
"Art Studio": {
"description": "Explore painting, drawing, and other visual arts",
"schedule": "Mondays and Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": ["isabella@mergington.edu", "mia@mergington.edu"]
},
"Music Ensemble": {
"description": "Play instruments and perform in concerts",
"schedule": "Wednesdays and Fridays, 4:00 PM - 5:30 PM",
"max_participants": 25,
"participants": ["noah@mergington.edu"]
},
"Debate Club": {
"description": "Develop public speaking and argumentation skills through competitive debate",
"schedule": "Tuesdays, 4:00 PM - 5:30 PM",
"max_participants": 14,
"participants": ["ava@mergington.edu", "ethan@mergington.edu"]
},
"Science Club": {
"description": "Conduct experiments and explore STEM topics",
"schedule": "Fridays, 4:00 PM - 5:00 PM",
"max_participants": 20,
"participants": ["liam@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}/signup")
def remove_from_activity(activity_name: str, email: str):
"""Remove 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
if email not in activity["participants"]:
raise HTTPException(status_code=400, detail="Student is not signed up for this activity")

# Remove student
activity["participants"].remove(email)
return {"message": f"Removed {email} from {activity_name}"}
59 changes: 58 additions & 1 deletion src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,54 @@ document.addEventListener("DOMContentLoaded", () => {
const signupForm = document.getElementById("signup-form");
const messageDiv = document.getElementById("message");

// Function to delete a participant from an activity
async function deleteParticipant(activityName, email) {
try {
const response = await fetch(
`/activities/${encodeURIComponent(activityName)}/signup?email=${encodeURIComponent(email)}`,
{
method: "DELETE",
}
);

const result = await response.json();

if (response.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
// Refresh activities list
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
}

messageDiv.classList.remove("hidden");

// Hide message after 5 seconds
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
} catch (error) {
messageDiv.textContent = "Failed to remove participant. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
console.error("Error removing participant:", error);
}
}

// Function to fetch activities from API
async function fetchActivities() {
try {
const response = await fetch("/activities");
const response = await fetch(`/activities?t=${Date.now()}`);
const activities = await response.json();

// Clear loading message
activitiesList.innerHTML = "";

// Clear and repopulate activity select dropdown
activitySelect.innerHTML = '<option value="">-- Select an activity --</option>';

// Populate activities list
Object.entries(activities).forEach(([name, details]) => {
const activityCard = document.createElement("div");
Expand All @@ -25,10 +64,26 @@ 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:</strong></p>
<ul class="participants-list">
${details.participants.length > 0 ? details.participants.map(email => `<li>${email} <button class="delete-btn" data-email="${email}" data-activity="${name}" title="Remove participant">×</button></li>`).join('') : '<li>No participants yet</li>'}
</ul>
</div>
`;

activitiesList.appendChild(activityCard);

// Add event listeners for delete buttons
const deleteButtons = activityCard.querySelectorAll('.delete-btn');
deleteButtons.forEach(button => {
button.addEventListener('click', async (event) => {
const email = event.target.dataset.email;
const activity = event.target.dataset.activity;
await deleteParticipant(activity, email);
});
});

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

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

.participants-section p {
margin-bottom: 5px;
font-weight: bold;
color: #1a237e;
}

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

.participants-list li {
margin-bottom: 3px;
color: #555;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}

.delete-btn {
background: none;
border: none;
color: #c62828;
cursor: pointer;
font-size: 18px;
font-weight: bold;
padding: 0 5px;
margin-left: 10px;
}

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

.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 for Mergington High School Activities API
27 changes: 27 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest
from fastapi.testclient import TestClient
from src.app import app


@pytest.fixture
def client():
"""FastAPI test client fixture"""
return TestClient(app)


@pytest.fixture
def sample_activity():
"""Sample activity data for testing"""
return {
"name": "Test Activity",
"description": "A test activity",
"schedule": "Test schedule",
"max_participants": 10,
"participants": []
}


@pytest.fixture
def sample_email():
"""Sample email for testing"""
return "test@example.com"
53 changes: 53 additions & 0 deletions tests/test_activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pytest
from fastapi.testclient import TestClient


class TestRootEndpoint:
"""Test cases for the root endpoint"""

def test_root_redirect(self, client: TestClient):
# Arrange
expected_status_code = 307
expected_location = "/static/index.html"

# Act - don't follow redirects
response = client.get("/", follow_redirects=False)

# Assert
assert response.status_code == expected_status_code
assert response.headers["location"] == expected_location


class TestActivitiesEndpoint:
"""Test cases for the activities endpoint"""

def test_get_activities_success(self, client: TestClient):
# Arrange
expected_status_code = 200

# Act
response = client.get("/activities")

# Assert
assert response.status_code == expected_status_code
data = response.json()
assert isinstance(data, dict)
assert len(data) > 0 # Should have activities

# Check structure of first activity
first_activity = next(iter(data.values()))
required_keys = ["description", "schedule", "max_participants", "participants"]
for key in required_keys:
assert key in first_activity

def test_get_activities_returns_all_activities(self, client: TestClient):
# Arrange
response = client.get("/activities")
data = response.json()

# Act - count activities
activity_count = len(data)

# Assert
assert activity_count > 0
assert isinstance(data, dict)
77 changes: 77 additions & 0 deletions tests/test_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import pytest
from fastapi.testclient import TestClient


class TestDeleteParticipantEndpoint:
"""Test cases for removing participants from activities"""

def test_delete_participant_success(self, client: TestClient):
# Arrange
activity_name = "Chess Club"
email = "daniel@mergington.edu" # Already exists
expected_status_code = 200

# Act
response = client.delete(
f"/activities/{activity_name}/signup",
params={"email": email}
)

# Assert
assert response.status_code == expected_status_code
data = response.json()
assert "message" in data
assert email in data["message"]
assert activity_name in data["message"]

def test_delete_from_nonexistent_activity(self, client: TestClient):
# Arrange
activity_name = "Nonexistent Activity"
email = "test@example.com"
expected_status_code = 404

# Act
response = client.delete(
f"/activities/{activity_name}/signup",
params={"email": email}
)

# Assert
assert response.status_code == expected_status_code
data = response.json()
assert "detail" in data
assert "not found" in data["detail"].lower()

def test_delete_nonexistent_participant(self, client: TestClient):
# Arrange
activity_name = "Chess Club"
email = "nonexistent@example.com"
expected_status_code = 400

# Act
response = client.delete(
f"/activities/{activity_name}/signup",
params={"email": email}
)

# Assert
assert response.status_code == expected_status_code
data = response.json()
assert "detail" in data
assert "not signed up" in data["detail"].lower()

def test_delete_with_url_encoding(self, client: TestClient):
# Arrange
activity_name = "Programming Class"
email = "emma@mergington.edu"
expected_status_code = 200

# Act
response = client.delete(
f"/activities/{activity_name}/signup?email={email}"
)

# Assert
assert response.status_code == expected_status_code
data = response.json()
assert "message" in data
Loading
Loading