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
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"uvicorn": true
}
}
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": "Join the varsity soccer team and compete in league matches",
"schedule": "Mondays and Wednesdays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["liam@mergington.edu", "noah@mergington.edu"]
},
"Basketball Team": {
"description": "Practice basketball skills and play competitive games",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 15,
"participants": ["ava@mergington.edu", "isabella@mergington.edu"]
},
"Drama Club": {
"description": "Perform in theatrical productions and develop acting skills",
"schedule": "Thursdays, 3:30 PM - 5:30 PM",
"max_participants": 25,
"participants": ["mia@mergington.edu", "charlotte@mergington.edu"]
},
"Art Club": {
"description": "Explore various art mediums including painting, drawing, and sculpture",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": ["ethan@mergington.edu", "amelia@mergington.edu"]
},
"Debate Team": {
"description": "Develop critical thinking and public speaking skills through competitive debates",
"schedule": "Tuesdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["james@mergington.edu", "harper@mergington.edu"]
},
"Science Club": {
"description": "Conduct experiments and explore scientific concepts through hands-on projects",
"schedule": "Fridays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["benjamin@mergington.edu", "evelyn@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}/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
if email not in activity["participants"]:
raise HTTPException(status_code=400, detail="Student not signed up for this activity")

# Remove student
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
37 changes: 37 additions & 0 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ document.addEventListener("DOMContentLoaded", () => {

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

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

// Populate activities list
Object.entries(activities).forEach(([name, details]) => {
Expand All @@ -25,10 +28,41 @@ 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">
<strong>Participants:</strong>
${details.participants.length > 0
? `<ul class="participants-list">
${details.participants.map(email => `<li>${email} <button class="delete-btn" data-activity="${name}" data-email="${email}">×</button></li>`).join('')}
</ul>`
: `<p class="no-participants">No participants yet. Be the first to sign up!</p>`
}
</div>
`;

activitiesList.appendChild(activityCard);

// Add event listeners for delete buttons
activityCard.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const activity = btn.dataset.activity;
const email = btn.dataset.email;
try {
const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
fetchActivities(); // Refresh activities list
} else {
alert(result.detail || 'An error occurred');
}
} catch (error) {
alert('Failed to unregister. Please try again.');
console.error('Error unregistering:', error);
}
});
});

// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
Expand Down Expand Up @@ -62,6 +96,9 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();

// Refresh activities list to show updated participants
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
48 changes: 48 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,54 @@ section h3 {
margin-bottom: 8px;
}

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

.participants-section strong {
display: block;
margin-bottom: 8px;
color: #1a237e;
}

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

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

.delete-btn {
background: none;
border: none;
color: #d32f2f;
cursor: pointer;
font-size: 16px;
font-weight: bold;
padding: 0 5px;
}

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

.no-participants {
font-style: italic;
color: #999;
margin: 0;
font-size: 14px;
}

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

@pytest.mark.asyncio
async def test_get_activities():
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.get("/activities")
assert response.status_code == 200
data = response.json()
assert "Chess Club" in data
assert "participants" in data["Chess Club"]

@pytest.mark.asyncio
async def test_root_redirect():
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.get("/")
assert response.status_code == 307 # Redirect
assert response.headers["location"] == "/static/index.html"

@pytest.mark.asyncio
async def test_signup_success():
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.post("/activities/Chess%20Club/signup?email=test@mergington.edu")
assert response.status_code == 200
data = response.json()
assert "Signed up" in data["message"]

@pytest.mark.asyncio
async def test_signup_already_signed():
async with AsyncClient(app=app, base_url="http://testserver") as client:
# First signup
await client.post("/activities/Chess%20Club/signup?email=test2@mergington.edu")
# Second
response = await client.post("/activities/Chess%20Club/signup?email=test2@mergington.edu")
assert response.status_code == 400
data = response.json()
assert "already signed up" in data["detail"]

@pytest.mark.asyncio
async def test_signup_activity_not_found():
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.post("/activities/NonExistent/signup?email=test@mergington.edu")
assert response.status_code == 404
data = response.json()
assert "Activity not found" in data["detail"]

@pytest.mark.asyncio
async def test_unregister_success():
async with AsyncClient(app=app, base_url="http://testserver") as client:
# Signup first
await client.post("/activities/Chess%20Club/signup?email=unreg@mergington.edu")
# Unregister
response = await client.delete("/activities/Chess%20Club/unregister?email=unreg@mergington.edu")
assert response.status_code == 200
data = response.json()
assert "Unregistered" in data["message"]

@pytest.mark.asyncio
async def test_unregister_not_signed():
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.delete("/activities/Chess%20Club/unregister?email=notsigned@mergington.edu")
assert response.status_code == 400
data = response.json()
assert "not signed up" in data["detail"]

@pytest.mark.asyncio
async def test_unregister_activity_not_found():
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.delete("/activities/NonExistent/unregister?email=test@mergington.edu")
assert response.status_code == 404
data = response.json()
assert "Activity not found" in data["detail"]