From 2182f14c00b24d76b2087608f50b29573db4a783 Mon Sep 17 00:00:00 2001 From: dotanp <3824200+dotanp@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:43:00 +0000 Subject: [PATCH 1/4] Add extracurricular activities and signup validation to API --- src/app.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 4ebb1d9..d720c4a 100644 --- a/src/app.py +++ b/src/app.py @@ -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 team for interscholastic play", + "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, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": ["ava@mergington.edu"] + }, + "Art Studio": { + "description": "Explore painting, drawing, and mixed media techniques", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["isabella@mergington.edu", "lucas@mergington.edu"] + }, + "Drama Club": { + "description": "Perform in theatrical productions and develop acting skills", + "schedule": "Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": ["noah@mergington.edu"] + }, + "Debate Team": { + "description": "Engage in competitive debate and public speaking", + "schedule": "Mondays and Fridays, 3:30 PM - 4:30 PM", + "max_participants": 14, + "participants": ["mia@mergington.edu", "ethan@mergington.edu"] + }, + "Science Club": { + "description": "Conduct experiments and explore advanced scientific concepts", + "schedule": "Tuesdays, 3:30 PM - 5:00 PM", + "max_participants": 20, + "participants": ["charlotte@mergington.edu"] } } @@ -61,7 +97,9 @@ 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}"} From 94fe9c8b3a699e2d8ca60b049197379da944db70 Mon Sep 17 00:00:00 2001 From: dotanp <3824200+dotanp@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:03:41 +0000 Subject: [PATCH 2/4] Implement participant unregistration feature and enhance UI for activities --- src/app.py | 17 ++++++++ src/static/app.js | 91 ++++++++++++++++++++++++++++++++++++++++++- src/static/index.html | 4 +- src/static/styles.css | 89 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index d720c4a..afa7609 100644 --- a/src/app.py +++ b/src/app.py @@ -103,3 +103,20 @@ def signup_for_activity(activity_name: str, email: str): # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} + + +@app.delete("/activities/{activity_name}/participants/{email}") +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") + + activity = activities[activity_name] + + # Validate student is currently signed up + if email not in activity["participants"]: + raise HTTPException(status_code=404, detail="Student is not signed up for this activity") + + activity["participants"].remove(email) + return {"message": f"Unregistered {email} from {activity_name}"} diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..2ef542f 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -4,27 +4,71 @@ document.addEventListener("DOMContentLoaded", () => { const signupForm = document.getElementById("signup-form"); const messageDiv = document.getElementById("message"); + function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + } + // Function to fetch activities from API async function fetchActivities() { try { - const response = await fetch("/activities"); + const response = await fetch("/activities", { cache: "no-store" }); const activities = await response.json(); // Clear loading message activitiesList.innerHTML = ""; + activitySelect.innerHTML = ''; // 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 participants = Array.isArray(details.participants) ? details.participants : []; + const maxParticipants = details.max_participants || 0; + const spotsLeft = maxParticipants - participants.length; + const encodedActivity = encodeURIComponent(name); + const participantItems = participants.length + ? participants + .map((participant) => { + const encodedParticipant = encodeURIComponent(participant); + const safeParticipant = escapeHtml(participant); + + return ` +
  • + ${safeParticipant} + +
  • + `; + }) + .join("") + : '
  • No students signed up yet
  • '; activityCard.innerHTML = `

    ${name}

    ${details.description}

    Schedule: ${details.schedule}

    Availability: ${spotsLeft} spots left

    +
    +

    Participants

    +

    ${participants.length}/${maxParticipants} students enrolled

    + +
    `; activitiesList.appendChild(activityCard); @@ -62,6 +106,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + await fetchActivities(); } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; @@ -81,6 +126,48 @@ document.addEventListener("DOMContentLoaded", () => { } }); + activitiesList.addEventListener("click", async (event) => { + const button = event.target.closest(".delete-participant-btn"); + if (!button) { + return; + } + + const activity = button.dataset.activity; + const email = button.dataset.email; + + if (!activity || !email) { + return; + } + + try { + const response = await fetch(`/activities/${activity}/participants/${email}`, { + method: "DELETE", + }); + + const result = await response.json(); + + if (response.ok) { + messageDiv.textContent = result.message; + messageDiv.className = "success"; + await fetchActivities(); + } else { + messageDiv.textContent = result.detail || "Failed to 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(); }); diff --git a/src/static/index.html b/src/static/index.html index 3074f6e..67adffd 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -4,7 +4,7 @@ Mergington High School Activities - +
    @@ -45,6 +45,6 @@

    Sign Up for an Activity

    © 2023 Mergington High School

    - + diff --git a/src/static/styles.css b/src/static/styles.css index a533b32..e989d35 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -63,6 +63,8 @@ section h3 { border: 1px solid #ddd; border-radius: 5px; background-color: #f9f9f9; + border-left: 4px solid #3949ab; + box-shadow: 0 4px 12px rgba(26, 35, 126, 0.08); } .activity-card h4 { @@ -74,6 +76,93 @@ section h3 { margin-bottom: 8px; } +.participants-section { + margin-top: 12px; + padding: 12px; + border-radius: 8px; + border: 1px solid #c5cae9; + background: linear-gradient(180deg, #eef1ff 0%, #f7f8ff 100%); + border-left: 5px solid #1a237e; +} + +.participants-title { + margin-bottom: 4px; + color: #1a237e; + font-size: 0.98rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.participants-meta { + margin-bottom: 8px; + color: #3a4673; + font-size: 0.9rem; +} + +.participants-list { + margin: 0; + padding-left: 0; + list-style: none; + display: grid; + gap: 8px; +} + +.participant-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 10px; + border-radius: 6px; + background-color: #ffffff; + border: 1px solid #d7dcf5; +} + +.participant-email { + color: #2f3b63; + line-height: 1.4; + word-break: break-word; + font-size: 0.95rem; +} + +.delete-participant-btn { + border: 1px solid #f3b5b5; + background-color: #fff3f3; + color: #b42323; + width: 30px; + height: 30px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 14px; + padding: 0; + transition: background-color 0.2s, transform 0.15s; +} + +.delete-participant-btn:hover { + background-color: #ffe4e4; + transform: scale(1.05); +} + +.delete-participant-btn:focus-visible { + outline: 2px solid #b42323; + outline-offset: 2px; +} + +.empty-participant { + list-style: none; + margin-left: -1.2rem; + padding: 6px 8px; + border: 1px dashed #9fa8da; + border-radius: 6px; + background-color: #ffffff; + color: #5f6b8b; + font-style: italic; +} + .form-group { margin-bottom: 15px; } From 0fd7c881a4aa967738192903c992ae7344fc87c1 Mon Sep 17 00:00:00 2001 From: dotanp <3824200+dotanp@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:34:37 +0000 Subject: [PATCH 3/4] Add tests for activity signup and unregistration, including fixtures for isolation --- pytest.ini | 2 ++ requirements.txt | 3 +- tests/__init__.py | 0 tests/conftest.py | 24 +++++++++++++++ tests/test_app.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_app.py diff --git a/pytest.ini b/pytest.ini index a635c5c..d7501c2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,4 @@ [pytest] pythonpath = . +testpaths = tests +python_files = test_*.py diff --git a/requirements.txt b/requirements.txt index 5d9efb5..f2821b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ fastapi uvicorn httpx -watchfiles \ No newline at end of file +watchfiles +pytest \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fd4f77e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,24 @@ +import copy + +import pytest +from fastapi.testclient import TestClient + +from src import app as app_module + +INITIAL_ACTIVITIES = copy.deepcopy(app_module.activities) + + +@pytest.fixture(autouse=True) +def reset_activities(): + """Reset in-memory state so tests remain isolated and deterministic.""" + app_module.activities.clear() + app_module.activities.update(copy.deepcopy(INITIAL_ACTIVITIES)) + yield + app_module.activities.clear() + app_module.activities.update(copy.deepcopy(INITIAL_ACTIVITIES)) + + +@pytest.fixture +def client(): + with TestClient(app_module.app) as test_client: + yield test_client diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..3000662 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,75 @@ +def test_root_redirects_to_static_index(client): + response = client.get("/", follow_redirects=False) + + assert response.status_code == 307 + assert response.headers["location"] == "/static/index.html" + + +def test_get_activities_returns_expected_shape(client): + response = client.get("/activities") + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, dict) + assert "Chess Club" in payload + + chess = payload["Chess Club"] + assert set(chess.keys()) == { + "description", + "schedule", + "max_participants", + "participants", + } + assert isinstance(chess["participants"], list) + + +def test_signup_adds_new_participant(client): + new_email = "new.student@mergington.edu" + + response = client.post(f"/activities/Chess Club/signup?email={new_email}") + + assert response.status_code == 200 + assert response.json()["message"] == f"Signed up {new_email} for Chess Club" + + activities_response = client.get("/activities") + participants = activities_response.json()["Chess Club"]["participants"] + assert new_email in participants + + +def test_signup_rejects_duplicate_participant(client): + response = client.post("/activities/Chess Club/signup?email=michael@mergington.edu") + + assert response.status_code == 400 + assert response.json()["detail"] == "Student already signed up for this activity" + + +def test_signup_rejects_unknown_activity(client): + response = client.post("/activities/Unknown Club/signup?email=test@mergington.edu") + + assert response.status_code == 404 + assert response.json()["detail"] == "Activity not found" + + +def test_unregister_removes_participant(client): + response = client.delete("/activities/Chess Club/participants/michael@mergington.edu") + + assert response.status_code == 200 + assert response.json()["message"] == "Unregistered michael@mergington.edu from Chess Club" + + activities_response = client.get("/activities") + participants = activities_response.json()["Chess Club"]["participants"] + assert "michael@mergington.edu" not in participants + + +def test_unregister_rejects_unknown_activity(client): + response = client.delete("/activities/Unknown Club/participants/test@mergington.edu") + + assert response.status_code == 404 + assert response.json()["detail"] == "Activity not found" + + +def test_unregister_rejects_non_enrolled_participant(client): + response = client.delete("/activities/Chess Club/participants/not.enrolled@mergington.edu") + + assert response.status_code == 404 + assert response.json()["detail"] == "Student is not signed up for this activity" From b5bcbc52fd63bc74f7524caaba6214c2dfea9149 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:42:08 +0000 Subject: [PATCH 4/4] =?UTF-8?q?Congratulations!=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fbfb24e..932b822 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,35 @@ -# Getting Started with GitHub Copilot +
    - +# 🎉 Congratulations dotanp! 🎉 -Hey dotanp! + -Mona here. I'm done preparing your exercise. Hope you enjoy! 💚 +### 🌟 You've successfully completed the exercise! 🌟 -Remember, it's self-paced so feel free to take a break! ☕️ +## 🚀 Share Your Success! -[![](https://img.shields.io/badge/Go%20to%20Exercise-%E2%86%92-1f883d?style=for-the-badge&logo=github&labelColor=197935)](https://github.com/dotanp/github-copilot/issues/1) +**Show off your new skills and inspire others!** + + + Share on X + + + Share on Bluesky + + + Share on LinkedIn + + +### 🎯 What's Next? + +**Keep the momentum going!** + +[![](https://img.shields.io/badge/Return%20to%20Exercise-%E2%86%92-1f883d?style=for-the-badge&logo=github&labelColor=197935)](https://github.com/dotanp/github-copilot/issues/1) +[![GitHub Skills](https://img.shields.io/badge/Explore%20GitHub%20Skills-000000?style=for-the-badge&logo=github&logoColor=white)](https://learn.github.com/skills) + +*There's no better way to learn than building things!* 🚀 + +
    ---