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
40 changes: 40 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": "Practice and compete in basketball games",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 15,
"participants": []
},
"Soccer Club": {
"description": "Train and play soccer matches",
"schedule": "Mondays and Wednesdays, 3:00 PM - 5:00 PM",
"max_participants": 22,
"participants": []
},
"Art Club": {
"description": "Explore painting, drawing, and other visual arts",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": []
},
"Drama Club": {
"description": "Act in plays and learn theater skills",
"schedule": "Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 20,
"participants": []
},
"Debate Club": {
"description": "Develop argumentation and public speaking skills",
"schedule": "Mondays, 4:00 PM - 5:30 PM",
"max_participants": 16,
"participants": []
},
"Science Club": {
"description": "Conduct experiments and learn about scientific concepts",
"schedule": "Fridays, 2:00 PM - 4:00 PM",
"max_participants": 25,
"participants": []
}
}

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

# validate the studen is already signed in
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}"}
47 changes: 47 additions & 0 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,67 @@ document.addEventListener("DOMContentLoaded", () => {
// Clear loading message
activitiesList.innerHTML = "";

// Clear activity dropdown options (except the first one)
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 participantsList = details.participants.length > 0
? details.participants.map(p => `
<li class="participant-item">
<span class="participant-name">${p}</span>
<button class="delete-participant" type="button" data-activity="${name}" data-email="${p}">×</button>
</li>
`).join('')
: '<li class="participants-list-empty">No participants yet</li>';

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">
<strong>Participants:</strong>
<ul class="participants-list">
${participantsList}
</ul>
</div>
`;

activitiesList.appendChild(activityCard);

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

try {
const response = await fetch(
`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`,
{
method: 'POST',
}
);

if (response.ok) {
// Refresh activities
fetchActivities();
} else {
console.error('Failed to unregister participant');
}
} catch (error) {
console.error('Error unregistering participant:', error);
}
});
});

// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
Expand Down Expand Up @@ -62,6 +107,8 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
// Refresh activities to show the new participant
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
53 changes: 53 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,59 @@ 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: 10px;
color: #1a237e;
}

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

.participant-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 5px 0;
}

.participant-name {
color: #555;
flex: 1;
}

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

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

.participants-list-empty {
color: #999;
font-style: italic;
margin-bottom: 5px;
}

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

from fastapi.testclient import TestClient

from src.app import activities, app

client = TestClient(app)
original_activities = copy.deepcopy(activities)


def reset_activities():
activities.clear()
activities.update(copy.deepcopy(original_activities))


def test_root_redirects_to_static_index_html():
response = client.get("/", follow_redirects=False)

assert response.status_code == 307
assert response.headers["location"] == "/static/index.html"


def test_get_activities_returns_all_activities():
reset_activities()

response = client.get("/activities")

assert response.status_code == 200
assert response.json() == original_activities


def test_signup_for_activity_adds_participant():
reset_activities()

activity_name = "Basketball Team"
email = "alex@mergington.edu"

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

assert response.status_code == 200
assert response.json() == {"message": f"Signed up {email} for {activity_name}"}
assert email in activities[activity_name]["participants"]


def test_signup_missing_activity_returns_404():
reset_activities()

response = client.post("/activities/Unknown Club/signup", params={"email": "student@mergington.edu"})

assert response.status_code == 404
assert response.json()["detail"] == "Activity not found"


def test_duplicate_signup_returns_400():
reset_activities()

activity_name = "Chess Club"
email = "michael@mergington.edu"

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

assert response.status_code == 400
assert response.json()["detail"] == "Student already signed up for this activity"
Loading