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: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
fastapi
uvicorn
pytest
httpx
pytest-cov
68 changes: 67 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
# Sports related activities
"Soccer Team": {
"description": "Join the school soccer team and compete in local leagues",
"schedule": "Wednesdays and Fridays, 4:00 PM - 5:30 PM",
"max_participants": 18,
"participants": ["lucas@mergington.edu", "mia@mergington.edu"]
},
"Basketball Club": {
"description": "Practice basketball skills and play friendly matches",
"schedule": "Tuesdays, 5:00 PM - 6:30 PM",
"max_participants": 15,
"participants": ["liam@mergington.edu", "ava@mergington.edu"]
},
# Artistic activities
"Art Club": {
"description": "Explore painting, drawing, and other visual arts",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["charlotte@mergington.edu", "amelia@mergington.edu"]
},
"Drama Society": {
"description": "Participate in theater productions and acting workshops",
"schedule": "Mondays, 4:00 PM - 5:30 PM",
"max_participants": 20,
"participants": ["jack@mergington.edu", "ella@mergington.edu"]
},
# Intellectual activities
"Math Olympiad": {
"description": "Prepare for math competitions and solve challenging problems",
"schedule": "Wednesdays, 3:30 PM - 4:30 PM",
"max_participants": 10,
"participants": ["noah@mergington.edu", "isabella@mergington.edu"]
},
"Science Club": {
"description": "Conduct experiments and explore scientific concepts",
"schedule": "Fridays, 2:00 PM - 3:30 PM",
"max_participants": 14,
"participants": ["benjamin@mergington.edu", "grace@mergington.edu"]
}
}

Expand All @@ -61,7 +100,34 @@ def signup_for_activity(activity_name: str, email: str):

# Get the specific activity
activity = activities[activity_name]


# Validate activity is not at capacity
if len(activity["participants"]) >= activity["max_participants"]:
raise HTTPException(status_code=400, detail="Activity is at full capacity")

# 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 registered
if email not in activity["participants"]:
raise HTTPException(status_code=400, detail="Student is not registered for this activity")

# Remove student
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
87 changes: 78 additions & 9 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,57 @@ document.addEventListener("DOMContentLoaded", () => {
const messageDiv = document.getElementById("message");

// Function to fetch activities from API
async function fetchActivities() {
async function fetchActivities(refreshDropdown = true) {
try {
const response = await fetch("/activities");
const activities = await response.json();

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

// Only clear and repopulate dropdown on initial load
if (refreshDropdown) {
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
? `<div class="participants-list">${details.participants.map(email => `
<div class="participant-item">
<span class="participant-email">${email}</span>
<button class="delete-btn" onclick="unregisterParticipant('${name}', '${email}')" title="Remove participant">
</button>
</div>
`).join('')}</div>`
: '<p class="no-participants">No participants yet</p>';

activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Description:</strong> ${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
<p><strong>Capacity:</strong> ${details.participants.length}/${details.max_participants}</p>
<div class="participants-section">
<h5>Current Participants:</h5>
${participantsList}
</div>
`;

activitiesList.appendChild(activityCard);

// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
option.textContent = name;
activitySelect.appendChild(option);
// Only add options to dropdown on initial load
if (refreshDropdown) {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
activitySelect.appendChild(option);
}
});
} catch (error) {
activitiesList.innerHTML = "<p>Failed to load activities. Please try again later.</p>";
Expand All @@ -50,9 +72,13 @@ document.addEventListener("DOMContentLoaded", () => {

try {
const response = await fetch(
`/activities/${encodeURIComponent(activity)}/signup?email=${encodeURIComponent(email)}`,
`/activities/${encodeURIComponent(activity)}/signup`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `email=${encodeURIComponent(email)}`,
}
);

Expand All @@ -62,6 +88,7 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
fetchActivities(false); // Refresh activities list but keep dropdown intact
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand All @@ -84,3 +111,45 @@ document.addEventListener("DOMContentLoaded", () => {
// Initialize app
fetchActivities();
});

// Function to unregister a participant from an activity
async function unregisterParticipant(activityName, email) {
const messageDiv = document.getElementById("message");

try {
const response = await fetch(
`/activities/${encodeURIComponent(activityName)}/unregister`,
{
method: "DELETE",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `email=${encodeURIComponent(email)}`,
}
);

const result = await response.json();

if (response.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
// Refresh the activities list to show updated participants
await fetchActivities(false); // Don't refresh dropdown, just update the display
} 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 unregister participant. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
console.error("Error unregistering participant:", error);
}
}
80 changes: 80 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,86 @@ section h3 {
margin-bottom: 8px;
}

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

.participants-section h5 {
margin-bottom: 8px;
color: #1a237e;
font-size: 14px;
font-weight: bold;
}

.participants-list {
margin: 0;
padding: 0;
}

.participant-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #555;
}

.participant-item:last-child {
border-bottom: none;
}

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

.delete-btn {
background-color: #ff4444;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin-left: 8px;
}

.delete-btn:hover {
background-color: #cc0000;
}

.participants-list li {
padding: 4px 0;
padding-left: 16px;
position: relative;
font-size: 14px;
color: #555;
}

.participants-list li:before {
content: "•";
color: #1a237e;
font-weight: bold;
position: absolute;
left: 0;
}

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

.form-group {
margin-bottom: 15px;
}
Expand Down
52 changes: 52 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Tests for Mergington High School Activities API

This directory contains comprehensive tests for the FastAPI application.

## Running Tests

To run all tests:
```bash
pytest tests/ -v
```

To run tests with coverage:
```bash
pytest tests/ --cov=src --cov-report=term-missing
```

## Test Structure

- `conftest.py` - Test configuration and fixtures
- `test_api.py` - API endpoint tests

## Test Coverage

The tests cover:

### Root Endpoint
- Redirect functionality to static files

### Activities Endpoint
- Retrieving all activities
- Data structure validation
- Activity details verification

### Signup Endpoint
- Successful signups
- Duplicate signup prevention
- Capacity validation
- Non-existent activity handling
- Multiple activity signups

### Unregister Endpoint
- Successful unregistration
- Non-participant unregistration
- Non-existent activity handling
- Complete signup/unregister workflow

### Edge Cases
- Activity names with spaces
- Various email formats
- Case sensitivity testing

All tests achieve 100% code coverage of the FastAPI application.
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test package for Mergington High School Activities API
Loading