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
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
10 changes: 9 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A super simple FastAPI application that allows students to view and sign up for
1. Install the dependencies:

```
pip install fastapi uvicorn
pip install -r requirements.txt
```

2. Run the application:
Expand All @@ -25,6 +25,14 @@ A super simple FastAPI application that allows students to view and sign up for
- API documentation: http://localhost:8000/docs
- Alternative documentation: http://localhost:8000/redoc

## Run Tests

From the repository root, run:

```
pytest -q
```

## API Endpoints

| Method | Endpoint | Description |
Expand Down
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 with team training and games",
"schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM",
"max_participants": 15,
"participants": ["james@mergington.edu", "alex@mergington.edu"]
},
"Tennis Club": {
"description": "Tennis practice and friendly matches",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:00 PM",
"max_participants": 16,
"participants": ["lucas@mergington.edu", "nina@mergington.edu"]
},
"Art Studio": {
"description": "Drawing, painting, and visual art techniques",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": ["isabella@mergington.edu", "sophia@mergington.edu"]
},
"Music Band": {
"description": "Learn instruments and perform in school concerts",
"schedule": "Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 25,
"participants": ["lucas@mergington.edu", "mia@mergington.edu"]
},
"Debate Club": {
"description": "Develop argumentation and public speaking skills",
"schedule": "Mondays and Fridays, 3:30 PM - 4:30 PM",
"max_participants": 14,
"participants": ["ryan@mergington.edu", "jessica@mergington.edu"]
},
"Science Club": {
"description": "Explore scientific experiments and research projects",
"schedule": "Wednesdays, 4:00 PM - 5:00 PM",
"max_participants": 20,
"participants": ["nathan@mergington.edu", "ava@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")
def unregister_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=404, detail="Student is not signed up for this activity")

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

// Clear loading message
activitiesList.innerHTML = "";
activitySelect.innerHTML = '<option value="">-- Select an activity --</option>';

// 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 sortedParticipants = [...details.participants].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" })
);
const participantsList = sortedParticipants
.map(
(participant) => `
<li class="participant-item">
<span class="participant-email">${participant}</span>
<button
type="button"
class="participant-delete"
data-activity="${name}"
data-email="${participant}"
aria-label="Unregister ${participant} from ${name}"
title="Unregister participant"
>
<span aria-hidden="true">&#128465;</span>
</button>
</li>
`
)
.join("");
Comment on lines +23 to +44

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-section">
<p class="participants-title">Participants</p>
${sortedParticipants.length > 0
? `<ul class="participants-list">${participantsList}</ul>`
: '<p class="participants-empty">No participants yet.</p>'}
</div>
`;

activitiesList.appendChild(activityCard);
Expand Down Expand Up @@ -73,6 +102,10 @@ document.addEventListener("DOMContentLoaded", () => {
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);

if (response.ok) {
await fetchActivities();
}
} catch (error) {
messageDiv.textContent = "Failed to sign up. Please try again.";
messageDiv.className = "error";
Expand All @@ -81,6 +114,52 @@ document.addEventListener("DOMContentLoaded", () => {
}
});

activitiesList.addEventListener("click", async (event) => {
const deleteButton = event.target.closest(".participant-delete");

if (!deleteButton) {
return;
}

const activityName = deleteButton.dataset.activity;
const participantEmail = deleteButton.dataset.email;

if (!activityName || !participantEmail) {
return;
}

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

const result = await response.json();

if (response.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
await fetchActivities();
} else {
messageDiv.textContent = result.detail || "Could not unregister participant.";
messageDiv.className = "error";
}

messageDiv.classList.remove("hidden");

setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
} catch (error) {
messageDiv.textContent = "Failed to unregister participant. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
console.error("Error unregistering participant:", error);
}
});

// Initialize app
fetchActivities();
});
61 changes: 61 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,67 @@ section h3 {
margin-bottom: 8px;
}

.participants-section {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed #cfd8dc;
}

.participants-title {
font-weight: bold;
color: #1a237e;
margin-bottom: 6px;
}

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

.participants-list li {
margin-bottom: 6px;
}

.participant-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 6px 8px;
background-color: #eef3f8;
border: 1px solid #d7e2ee;
border-radius: 8px;
}

.participant-email {
font-size: 14px;
}

.participant-delete {
border: none;
background: transparent;
color: #b71c1c;
padding: 2px 6px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
line-height: 1;
transition: background-color 0.2s, color 0.2s;
}

.participant-delete:hover {
background-color: #ffebee;
color: #8e0000;
}

.participants-empty {
margin-bottom: 0;
color: #607d8b;
font-style: italic;
}

.form-group {
margin-bottom: 15px;
}
Expand Down
20 changes: 20 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from copy import deepcopy

import pytest
from fastapi.testclient import TestClient

from src.app import app
import src.app as app_module


@pytest.fixture
def client():
return TestClient(app)
Comment thread
JesperGr marked this conversation as resolved.


@pytest.fixture(autouse=True)
def reset_activities_state():
original_activities = deepcopy(app_module.activities)
yield
app_module.activities.clear()
app_module.activities.update(original_activities)
Loading
Loading