From 007aebb4b53abec60bf05760cb550a0bc943d0cc Mon Sep 17 00:00:00 2001 From: mihaelaoprea Date: Fri, 24 Apr 2026 11:19:37 +0000 Subject: [PATCH 1/3] Voeg extra buitenschoolse activiteiten toe en valideer inschrijvingen --- src/app.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/app.py b/src/app.py index 4ebb1d9..6d7790e 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 training and tournaments", + "schedule": "Mondays and Thursdays, 4:00 PM - 5:30 PM", + "max_participants": 15, + "participants": ["james@mergington.edu"] + }, + "Tennis Club": { + "description": "Learn tennis skills and compete in matches", + "schedule": "Tuesdays and Saturdays, 3:30 PM - 5:00 PM", + "max_participants": 12, + "participants": ["isabella@mergington.edu"] + }, + "Art Studio": { + "description": "Explore painting, drawing, and sculpture techniques", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 18, + "participants": ["lucas@mergington.edu", "ava@mergington.edu"] + }, + "Drama Club": { + "description": "Theatre arts, script writing, and stage performances", + "schedule": "Fridays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": ["noah@mergington.edu"] + }, + "Debate Team": { + "description": "Develop public speaking and critical thinking skills", + "schedule": "Mondays and Wednesdays, 3:30 PM - 4:30 PM", + "max_participants": 16, + "participants": ["mia@mergington.edu", "ethan@mergington.edu"] + }, + "Science Club": { + "description": "Conduct experiments and explore scientific concepts", + "schedule": "Tuesdays, 3:30 PM - 5:00 PM", + "max_participants": 20, + "participants": ["charlotte@mergington.edu"] } } @@ -62,6 +98,11 @@ 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 ba24e518c14a93c8105539c382c4c7e9f88d7aea Mon Sep 17 00:00:00 2001 From: mihaelaoprea Date: Fri, 24 Apr 2026 11:29:47 +0000 Subject: [PATCH 2/3] Voeg functionaliteit toe om studenten af te melden voor activiteiten en verbeter de gebruikersinterface voor deelnemerslijst --- src/app.py | 18 ++++++++++++++ src/static/app.js | 44 ++++++++++++++++++++++++++++++++++ src/static/styles.css | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/src/app.py b/src/app.py index 6d7790e..ff2bd4a 100644 --- a/src/app.py +++ b/src/app.py @@ -106,3 +106,21 @@ 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] + + # Check if participant is registered + if email not in activity["participants"]: + raise HTTPException(status_code=404, detail="Participant not found") + + # Remove participant + 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..9a30179 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -25,10 +25,53 @@ document.addEventListener("DOMContentLoaded", () => {

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+
+

Participants (${details.participants.length}/${details.max_participants}):

+
    + ${details.participants.map(p => `
  • ${p}
  • `).join('')} +
+
`; activitiesList.appendChild(activityCard); + // Add delete event listeners to participant buttons + const deleteButtons = activityCard.querySelectorAll(".delete-participant-btn"); + deleteButtons.forEach(btn => { + btn.addEventListener("click", async (e) => { + e.preventDefault(); + const email = btn.getAttribute("data-email"); + const activity = btn.getAttribute("data-activity"); + + try { + const response = await fetch( + `/activities/${encodeURIComponent(activity)}/participants/${encodeURIComponent(email)}`, + { method: "DELETE" } + ); + + if (response.ok) { + // Refresh activities list after successful deletion + fetchActivities(); + messageDiv.textContent = `Unregistered ${email} from ${activity}`; + messageDiv.className = "success"; + } else { + const result = await response.json(); + messageDiv.textContent = result.detail || "Failed to unregister"; + messageDiv.className = "error"; + } + messageDiv.classList.remove("hidden"); + setTimeout(() => { + messageDiv.classList.add("hidden"); + }, 5000); + } catch (error) { + messageDiv.textContent = "Failed to unregister. Please try again."; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + console.error("Error unregistering:", error); + } + }); + }); + // Add option to select dropdown const option = document.createElement("option"); option.value = name; @@ -62,6 +105,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + fetchActivities(); } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; diff --git a/src/static/styles.css b/src/static/styles.css index a533b32..5fbe2b8 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,61 @@ section h3 { margin-bottom: 8px; } +.participants-section { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e0e0e0; +} + +.participants-section p { + margin-bottom: 10px; + color: #555; + font-size: 14px; +} + +.participants-list { + list-style: none; + margin-left: 0; + font-size: 13px; + color: #666; +} + +.participants-list li { + margin-bottom: 5px; + padding: 4px 8px; + background-color: #f0f7ff; + border-radius: 3px; + border-left: 3px solid #0066cc; + display: flex; + justify-content: space-between; + align-items: center; +} + +.participant-email { + flex-grow: 1; +} + +.delete-participant-btn { + background: none; + border: none; + color: #c62828; + font-size: 20px; + line-height: 1; + cursor: pointer; + padding: 0 4px; + margin-left: 8px; + transition: color 0.2s, transform 0.1s; +} + +.delete-participant-btn:hover { + color: #d32f2f; + transform: scale(1.2); +} + +.delete-participant-btn:active { + transform: scale(0.95); +} + .form-group { margin-bottom: 15px; } From 21e53d6653ed88d8c06f630eb283f16121d2a355 Mon Sep 17 00:00:00 2001 From: mihaelaoprea Date: Fri, 24 Apr 2026 11:44:41 +0000 Subject: [PATCH 3/3] Add tests for activities management endpoints - Create tests package with __init__.py - Implement conftest.py for test client fixture - Add tests for GET /activities endpoint to verify structure and participant counts - Add tests for POST /activities/{activity_name}/signup endpoint to check successful signup, handling of non-existent activities, and duplicate registrations - Add tests for DELETE /activities/{activity_name}/participants/{email} endpoint to ensure successful unregistration, handling of non-existent activities, and non-existent participants --- .coverage | Bin 0 -> 53248 bytes requirements.txt | 3 +- tests/__init__.py | 1 + tests/conftest.py | 9 +++++ tests/test_activities.py | 60 ++++++++++++++++++++++++++++++++++ tests/test_signup.py | 64 ++++++++++++++++++++++++++++++++++++ tests/test_unregister.py | 69 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 .coverage create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_activities.py create mode 100644 tests/test_signup.py create mode 100644 tests/test_unregister.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..4e68f72259c8b247547df41e4cad1d2ae3ffcfa7 GIT binary patch literal 53248 zcmeI)U2EG`7zc34PVL%pP(~L_7D~>|NJ4FAri{How`>cntgv-U$96OKSU$G3+L9y5 zc^Q=9uF!pi4fRrqfZvaT|^;L&t52u&t8R{XjK%&7xzp zYH`9!l%Mx%PJ?oVszpWge5Wh@LvceLmZAjl)k1X;>H}1Wa#}8@5bJUF4dJV%@|A0= zKu59eG&b@ZKmU}^7SEkC@2IFw(zoe0KT#VxFsjPsCKY6ReuJa0`_i@BDkup#tfB2u zoCl$#!^IHBXYLLDf(AF9TQ6)Z7YvJu{RZK$m4T%qeaXMC+Ool)i! za>mKy%uNc;^%B-mM!qyPwmG3KNA;a( z&V-y>ROjlu&Td}~8^0t*IZIJBT(2a!64V3UoX-^BJ7*fyE7d`;+o6|EHQ5in=y{DA zzgDG(!tK}2W{az<=AGTBqcBgc?u5Z`CpXb)hKHwg9X>z3-|#4z>?AyjIvwFPqank& zg>D?`&EsQcGr^EP4>aM$2`SPS+S@o-ib2#nUVXB!MLi}^G(c!BNRl=CuFXRw;5yje zp7Lc&RpPUHT6obNiMmcTmnm+qPU||uE>Nouck_egOt!ePV&2L{-6(FY_(yi43neXi z!Zezk>M9xZ{bV9}(j=MW7|oK&7>UnNb;{XHacgB-b>eijYP@rHke$mGmzK3XOOrm| z(-R@{kx|H>tHbi1?9gn|la5bAmgZo+0timnkd#=~y?z+=Uw#^OPhNXQ@#+Fb&l7t} zwGkz!;r*o-1P(2$yle&UCr8CvTLwG}1#6=2c^xHPy*5ye%K=@}2}Bgb@m1-=Q5nG@ zm(ki=i(*O^7oO&KXwW6&i*~wQ={Dl^ikD$sD$V^oIQcqy5}}M@jRs{vL-a_QNulvs zp0SehD?KvdQoFLDhdVdQSc&^>b*wnO8ht856*^rdI7t*t)UozuAZ%aJ$#^Y|0wb~Z z(WrDWpV66Q4%M~USW7>t?{E{>s{F|BlF#x>xxuQLEnc`_4*Ky3){|2$pjQH2nbb9Y znwscYM^279*%vfR(@UiB&c)zkhWu1tfoDYB>~&3-q>5i4k|;+@gnmy=;3ur{jg2fp2fh zUXQ+Hfj|E@t=|p0VS@k!AOHafKmY;|fB*y_009UEHk3`~POOVG$Pu zAOHafKmY;|fB*y_009U<;JFB-&9t$gKmUJhSdX7enJ5GT5P$##AOHafKmY;|fB*y_ z0D&13NT(Na`t$#XhV^j9f!Bh(2tWV=5P$##AOHafKmY;|fB*!ZMSws5$NT?h5ky`HKmY;|fB*y_009U<00Izz m!0Zd~|NrCt|LnIDB7^`0AOHafKmY;|fB*y_009U*i@^V$hHS0? literal 0 HcmV?d00001 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..739954c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3bbdb25 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from fastapi.testclient import TestClient +from src.app import app + + +@pytest.fixture +def client(): + """Test client fixture for FastAPI app testing.""" + return TestClient(app) \ No newline at end of file diff --git a/tests/test_activities.py b/tests/test_activities.py new file mode 100644 index 0000000..6a8dac2 --- /dev/null +++ b/tests/test_activities.py @@ -0,0 +1,60 @@ +"""Tests for GET /activities endpoint.""" + + +def test_get_activities_returns_all_activities(client): + """Test that GET /activities returns all activities with correct structure.""" + # Arrange: No specific setup needed as activities are predefined in app + + # Act: Make GET request to /activities + response = client.get("/activities") + + # Assert: Response status is 200 + assert response.status_code == 200 + + # Assert: Response contains all 9 activities + activities = response.json() + assert len(activities) == 9 + + # Assert: Each activity has the expected structure + expected_activities = [ + "Chess Club", "Programming Class", "Gym Class", "Basketball Team", + "Tennis Club", "Art Studio", "Drama Club", "Debate Team", "Science Club" + ] + assert set(activities.keys()) == set(expected_activities) + + # Assert: Each activity has required fields + for activity_name, activity_data in activities.items(): + assert "description" in activity_data + assert "schedule" in activity_data + assert "max_participants" in activity_data + assert "participants" in activity_data + assert isinstance(activity_data["participants"], list) + + +def test_get_activities_participant_counts(client): + """Test that participant counts are correct for each activity.""" + # Arrange: No specific setup needed + + # Act: Make GET request to /activities + response = client.get("/activities") + + # Assert: Response is successful + assert response.status_code == 200 + + activities = response.json() + + # Assert: Specific participant counts match expected values + expected_counts = { + "Chess Club": 2, # michael@mergington.edu, daniel@mergington.edu + "Programming Class": 2, # emma@mergington.edu, sophia@mergington.edu + "Gym Class": 2, # john@mergington.edu, olivia@mergington.edu + "Basketball Team": 1, # james@mergington.edu + "Tennis Club": 1, # isabella@mergington.edu + "Art Studio": 2, # lucas@mergington.edu, ava@mergington.edu + "Drama Club": 1, # noah@mergington.edu + "Debate Team": 2, # mia@mergington.edu, ethan@mergington.edu + "Science Club": 1, # charlotte@mergington.edu + } + + for activity_name, expected_count in expected_counts.items(): + assert len(activities[activity_name]["participants"]) == expected_count \ No newline at end of file diff --git a/tests/test_signup.py b/tests/test_signup.py new file mode 100644 index 0000000..9e8e054 --- /dev/null +++ b/tests/test_signup.py @@ -0,0 +1,64 @@ +"""Tests for POST /activities/{activity_name}/signup endpoint.""" + + +def test_signup_successful(client): + """Test successful signup for an activity.""" + # Arrange: Choose an existing activity and a new email + activity_name = "Chess Club" + email = "newstudent@mergington.edu" + + # Act: Make POST request to signup endpoint + response = client.post(f"/activities/{activity_name}/signup?email={email}") + + # Assert: Response status is 200 + assert response.status_code == 200 + + # Assert: Response contains success message + result = response.json() + assert "message" in result + assert f"Signed up {email} for {activity_name}" in result["message"] + + # Assert: Participant was added to the activity + activities_response = client.get("/activities") + activities = activities_response.json() + assert email in activities[activity_name]["participants"] + + +def test_signup_non_existent_activity(client): + """Test signup for a non-existent activity returns 404.""" + # Arrange: Use an invalid activity name + invalid_activity = "NonExistentActivity" + email = "student@mergington.edu" + + # Act: Make POST request to signup endpoint + response = client.post(f"/activities/{invalid_activity}/signup?email={email}") + + # Assert: Response status is 404 + assert response.status_code == 404 + + # Assert: Response contains appropriate error message + result = response.json() + assert "detail" in result + assert "Activity not found" in result["detail"] + + +def test_signup_duplicate_registration(client): + """Test that duplicate signup for same activity returns 400.""" + # Arrange: First, sign up a student + activity_name = "Programming Class" + email = "duplicatestudent@mergington.edu" + + # Sign up once (should succeed) + first_response = client.post(f"/activities/{activity_name}/signup?email={email}") + assert first_response.status_code == 200 + + # Act: Attempt to sign up the same student again + second_response = client.post(f"/activities/{activity_name}/signup?email={email}") + + # Assert: Second signup returns 400 + assert second_response.status_code == 400 + + # Assert: Response contains appropriate error message + result = second_response.json() + assert "detail" in result + assert "Student already signed up" in result["detail"] \ No newline at end of file diff --git a/tests/test_unregister.py b/tests/test_unregister.py new file mode 100644 index 0000000..48a0832 --- /dev/null +++ b/tests/test_unregister.py @@ -0,0 +1,69 @@ +"""Tests for DELETE /activities/{activity_name}/participants/{email} endpoint.""" + + +def test_unregister_successful(client): + """Test successful unregister from an activity.""" + # Arrange: First sign up a participant, then unregister them + activity_name = "Gym Class" + email = "tempstudent@mergington.edu" + + # Sign up the participant + signup_response = client.post(f"/activities/{activity_name}/signup?email={email}") + assert signup_response.status_code == 200 + + # Verify they are signed up + activities_response = client.get("/activities") + activities = activities_response.json() + assert email in activities[activity_name]["participants"] + + # Act: Make DELETE request to unregister endpoint + response = client.delete(f"/activities/{activity_name}/participants/{email}") + + # Assert: Response status is 200 + assert response.status_code == 200 + + # Assert: Response contains success message + result = response.json() + assert "message" in result + assert f"Unregistered {email} from {activity_name}" in result["message"] + + # Assert: Participant was removed from the activity + activities_response = client.get("/activities") + activities = activities_response.json() + assert email not in activities[activity_name]["participants"] + + +def test_unregister_non_existent_activity(client): + """Test unregister from a non-existent activity returns 404.""" + # Arrange: Use an invalid activity name + invalid_activity = "NonExistentActivity" + email = "student@mergington.edu" + + # Act: Make DELETE request to unregister endpoint + response = client.delete(f"/activities/{invalid_activity}/participants/{email}") + + # Assert: Response status is 404 + assert response.status_code == 404 + + # Assert: Response contains appropriate error message + result = response.json() + assert "detail" in result + assert "Activity not found" in result["detail"] + + +def test_unregister_non_existent_participant(client): + """Test unregister of a participant not signed up returns 404.""" + # Arrange: Use valid activity but email not registered + activity_name = "Tennis Club" + unregistered_email = "notsignedup@mergington.edu" + + # Act: Make DELETE request to unregister endpoint + response = client.delete(f"/activities/{activity_name}/participants/{unregistered_email}") + + # Assert: Response status is 404 + assert response.status_code == 404 + + # Assert: Response contains appropriate error message + result = response.json() + assert "detail" in result + assert "Participant not found" in result["detail"] \ No newline at end of file