From f5fbba2871cc1a0a1c73524d0446bab15ffbc67f Mon Sep 17 00:00:00 2001 From: Karina Date: Thu, 2 Apr 2026 17:28:12 -0400 Subject: [PATCH 1/3] Added a report for graduating seniors with volunteer hours in 4 unique semesters --- app/logic/volunteerSpreadsheet.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/app/logic/volunteerSpreadsheet.py b/app/logic/volunteerSpreadsheet.py index 6a03ca6ac..b3f1a46d4 100644 --- a/app/logic/volunteerSpreadsheet.py +++ b/app/logic/volunteerSpreadsheet.py @@ -208,6 +208,32 @@ def termParticipation(term): return dict(programParticipationDict) +def graduatingSeniorsVolunteerHours(academicYear): + columns = ["Full Name", "Email", "B-Number", "Unique Volunteer Semesters", "Total Volunteer Hours"] + + currentSeniors = (EventParticipant + .select(EventParticipant.user_id) + .join(User).switch(EventParticipant) + .join(Event) + .join(Term) + .where(Term.academicYear == academicYear, User.rawClassLevel.in_(["Senior", "Graduating"]), Event.isService == True, + Event.deletionDate == None, Event.isCanceled == False)) + + query = (EventParticipant + .select(fn.CONCAT(User.firstName, ' ', User.lastName), + fn.CONCAT(User.username, '@berea.edu'), + User.bnumber, + fn.COUNT(fn.DISTINCT(Event.term)).alias("semester_count"), + fn.SUM(EventParticipant.hoursEarned).alias("total_hours")) + .join(User).switch(EventParticipant) + .join(Event) + .where(Event.isService == True, Event.deletionDate == None, Event.isCanceled == False, EventParticipant.user_id.in_(currentSeniors)) + .group_by(User.bnumber) + .having(fn.COUNT(fn.DISTINCT(Event.term)) >= 4) + .order_by(SQL("semester_count").desc())) + + return (columns, query.tuples()) + def removeNullParticipants(participantList): return list(filter(lambda participant: participant, participantList)) @@ -273,6 +299,7 @@ def createSpreadsheet(academicYear): makeDataXls("Unique Volunteers", getUniqueVolunteers(academicYear), workbook, sheetDesc=f"All students who participated in at least one service event during {academicYear}.") makeDataXls("Only All Volunteer Training", onlyCompletedAllVolunteer(academicYear), workbook, sheetDesc="Students who participated in an All Volunteer Training, but did not participate in any service events.") makeDataXls("Retention Rate By Semester", getRetentionRate(academicYear), workbook, sheetDesc="The percentage of students who participated in service events in the fall semester who also participated in a service event in the spring semester. Does not currently account for fall graduations.") + makeDataXls("Graduating Seniors", graduatingSeniorsVolunteerHours(academicYear), workbook, sheetDesc="Graduating seniors who have earned any number of service hours for at least 4 unique semesters.") fallTerm = getFallTerm(academicYear) springTerm = getSpringTerm(academicYear) From 36dda65b92af854a58c541f1b8b5ca913b6809d7 Mon Sep 17 00:00:00 2001 From: Karina Date: Tue, 7 Apr 2026 15:37:08 -0400 Subject: [PATCH 2/3] Added tests to grad seniors reports spreadsheet --- tests/code/test_spreadsheet.py | 94 +++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/tests/code/test_spreadsheet.py b/tests/code/test_spreadsheet.py index 9f20be372..cde087a04 100644 --- a/tests/code/test_spreadsheet.py +++ b/tests/code/test_spreadsheet.py @@ -366,4 +366,96 @@ def test_getUniqueVolunteers(fixture_info): ('doej2', 'Jane Doe', 'B888828'), ('testt', 'Test Tester', 'B55555')] - +@pytest.mark.integration +def test_graduatingSeniorsVolunteerHours(fixture_info): + columns, rows = graduatingSeniorsVolunteerHours("2024-2025-test") + assert columns == ["Full Name", "Email", "B-Number", "Unique Volunteer Semesters", "Total Volunteer Hours"] + + assert list(rows) == [] + + term5 = Term.create(description='Fall 2021', academicYear='2021-2022-test') + term6 = Term.create(description='Spring 2022', academicYear='2021-2022-test') + term7 = Term.create(description='Fall 2022', academicYear='2022-2023-test') + + program5 = Program.create(programName='Program5') + + event5 = Event.create(name='Event5', term=term5, program=program5, startDate=date(2021, 9, 1), + isCanceled=False, deletionDate=None, isService=True) + event6 = Event.create(name='Event6', term=term6, program=program5, startDate=date(2022, 2, 1), + isCanceled=False, deletionDate=None, isService=True) + event7 = Event.create(name='Event7', term=term7, program=program5, startDate=date(2022, 9, 1), + isCanceled=False, deletionDate=None, isService=True) + + # Give Bob 3 more unique semesters of service (he already has 1 from before - term2/2024-2025) + EventParticipant.create(user=fixture_info['user3'], event=event5, hoursEarned=2) + EventParticipant.create(user=fixture_info['user3'], event=event6, hoursEarned=3) + EventParticipant.create(user=fixture_info['user3'], event=event7, hoursEarned=4) + + # Bob now has 4 unique semesters total, and is a Senior in 2024-2025-test, so his info should appear + columns, rows = graduatingSeniorsVolunteerHours("2024-2025-test") + result = list(rows) + assert len(result) == 1 + assert result[0] == ("Bob Builder", "builderb@berea.edu", "B00700932", 4, 9.0) + + # Bob should NOT appear when querying a year where he is not a Senior (Bob is Senior only in 2024-2025) + columns, rows = graduatingSeniorsVolunteerHours("2023-2024-test") + assert list(rows) == [] + + # non-senior students should never appear even with enough semesters + extraTerm = Term.create(description='Spring 2021', academicYear='2020-2021-test') + extraTerm2 = Term.create(description='Fall 2020', academicYear='2020-2021-test') + extraTerm3 = Term.create(description='Spring 2020', academicYear='2019-2020-test') + + event8 = Event.create(name='Event8', term=extraTerm, program=program5, startDate=date(2021, 2, 1), + isCanceled=False, deletionDate=None, isService=True) + event9 = Event.create(name='Event9', term=extraTerm2, program=program5, startDate=date(2020, 9, 1), + isCanceled=False, deletionDate=None, isService=True) + event10 = Event.create(name='Event10', term=extraTerm3, program=program5, startDate=date(2020, 2, 1), + isCanceled=False, deletionDate=None, isService=True) + + # give John (Sophomore) 4 unique semesters, so it should never appear + EventParticipant.create(user=fixture_info['user1'], event=event8, hoursEarned=1) + EventParticipant.create(user=fixture_info['user1'], event=event9, hoursEarned=1) + EventParticipant.create(user=fixture_info['user1'], event=event10, hoursEarned=1) + # John already has term1 (2023-2024-test) from fixture, so now has 4 unique semesters + columns, rows = graduatingSeniorsVolunteerHours("2023-2024-test") + assert list(rows) == [] + + # Test "Graduating" class level works the same as "Senior" + graduatingUser = User.create(username="smithj", firstName="James", lastName="Smith", + bnumber="B999999", major="Math", rawClassLevel="Graduating") + + gradTerm1 = Term.create(description='Fall 2023 Grad', academicYear='2023-2024-test') + gradTerm2 = Term.create(description='Spring 2023 Grad', academicYear='2022-2023-test') + gradTerm3 = Term.create(description='Fall 2022 Grad', academicYear='2022-2023-test') + gradTerm4 = Term.create(description='Spring 2022 Grad', academicYear='2021-2022-test') + + gevent1 = Event.create(name='GEvent1', term=gradTerm1, program=program5, startDate=date(2023, 9, 5), + isCanceled=False, deletionDate=None, isService=True) + gevent2 = Event.create(name='GEvent2', term=gradTerm2, program=program5, startDate=date(2023, 2, 5), + isCanceled=False, deletionDate=None, isService=True) + gevent3 = Event.create(name='GEvent3', term=gradTerm3, program=program5, startDate=date(2022, 9, 5), + isCanceled=False, deletionDate=None, isService=True) + gevent4 = Event.create(name='GEvent4', term=gradTerm4, program=program5, startDate=date(2022, 2, 5), + isCanceled=False, deletionDate=None, isService=True) + + EventParticipant.create(user=graduatingUser, event=gevent1, hoursEarned=5) + EventParticipant.create(user=graduatingUser, event=gevent2, hoursEarned=5) + EventParticipant.create(user=graduatingUser, event=gevent3, hoursEarned=5) + EventParticipant.create(user=graduatingUser, event=gevent4, hoursEarned=5) + + columns, rows = graduatingSeniorsVolunteerHours("2023-2024-test") + result = list(rows) + assert len(result) == 1 + assert result[0] == ("James Smith", "smithj@berea.edu", "B999999", 4, 20.0) + + # non-service events should not be counted + nonServiceTerm = Term.create(description='Fall 2019', academicYear='2019-2020-test') + nonServiceEvent = Event.create(name='NonServiceEvent', term=nonServiceTerm, program=program5, + startDate=date(2019, 9, 1), isCanceled=False, deletionDate=None, isService=False) + EventParticipant.create(user=fixture_info['user3'], event=nonServiceEvent, hoursEarned=5) + + # Bob still has exactly 4 semesters with volunteer hours, the non-service event participation should not push the count up + columns, rows = graduatingSeniorsVolunteerHours("2024-2025-test") + result = list(rows) + assert result[0][3] == 4 From 0471bc26f16b7155785d91790643df20f71d7b4e Mon Sep 17 00:00:00 2001 From: Karina Date: Fri, 10 Apr 2026 17:05:31 -0400 Subject: [PATCH 3/3] Fixed tests failing --- app/logic/volunteerSpreadsheet.py | 41 ++++++++++++++++++------------- tests/code/test_spreadsheet.py | 37 ++++++++++++++++------------ 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/app/logic/volunteerSpreadsheet.py b/app/logic/volunteerSpreadsheet.py index b3f1a46d4..a194a2d82 100644 --- a/app/logic/volunteerSpreadsheet.py +++ b/app/logic/volunteerSpreadsheet.py @@ -212,25 +212,32 @@ def graduatingSeniorsVolunteerHours(academicYear): columns = ["Full Name", "Email", "B-Number", "Unique Volunteer Semesters", "Total Volunteer Hours"] currentSeniors = (EventParticipant - .select(EventParticipant.user_id) - .join(User).switch(EventParticipant) - .join(Event) - .join(Term) - .where(Term.academicYear == academicYear, User.rawClassLevel.in_(["Senior", "Graduating"]), Event.isService == True, - Event.deletionDate == None, Event.isCanceled == False)) + .select(EventParticipant.user_id) + .join(User).switch(EventParticipant) + .join(Event) + .join(Term) + .where(Term.academicYear == academicYear, + User.rawClassLevel.in_(["Senior", "Graduating"]), + Event.isService == True, + Event.deletionDate == None, + Event.isCanceled == False) + .tuples()) query = (EventParticipant - .select(fn.CONCAT(User.firstName, ' ', User.lastName), - fn.CONCAT(User.username, '@berea.edu'), - User.bnumber, - fn.COUNT(fn.DISTINCT(Event.term)).alias("semester_count"), - fn.SUM(EventParticipant.hoursEarned).alias("total_hours")) - .join(User).switch(EventParticipant) - .join(Event) - .where(Event.isService == True, Event.deletionDate == None, Event.isCanceled == False, EventParticipant.user_id.in_(currentSeniors)) - .group_by(User.bnumber) - .having(fn.COUNT(fn.DISTINCT(Event.term)) >= 4) - .order_by(SQL("semester_count").desc())) + .select(fn.CONCAT(User.firstName, ' ', User.lastName), + fn.CONCAT(User.username, '@berea.edu'), + User.bnumber, + fn.COUNT(fn.DISTINCT(Event.term)).alias("semester_count"), + fn.SUM(EventParticipant.hoursEarned).alias("total_hours")) + .join(User).switch(EventParticipant) + .join(Event) + .where(Event.isService == True, + Event.deletionDate == None, + Event.isCanceled == False, + EventParticipant.user_id.in_(currentSeniors)) + .group_by(User.bnumber) + .having(fn.COUNT(fn.DISTINCT(Event.term)) >= 4) + .order_by(SQL("semester_count").desc())) return (columns, query.tuples()) diff --git a/tests/code/test_spreadsheet.py b/tests/code/test_spreadsheet.py index cde087a04..c8444e198 100644 --- a/tests/code/test_spreadsheet.py +++ b/tests/code/test_spreadsheet.py @@ -370,15 +370,19 @@ def test_getUniqueVolunteers(fixture_info): def test_graduatingSeniorsVolunteerHours(fixture_info): columns, rows = graduatingSeniorsVolunteerHours("2024-2025-test") assert columns == ["Full Name", "Email", "B-Number", "Unique Volunteer Semesters", "Total Volunteer Hours"] - assert list(rows) == [] - term5 = Term.create(description='Fall 2021', academicYear='2021-2022-test') - term6 = Term.create(description='Spring 2022', academicYear='2021-2022-test') - term7 = Term.create(description='Fall 2022', academicYear='2022-2023-test') + # Create all terms with -test suffix so we don't touch real data + term_bob_current = Term.create(description='Fall 2024 Test', academicYear='2024-2025-test') + term5 = Term.create(description='Fall 2021 Test', academicYear='2021-2022-test') + term6 = Term.create(description='Spring 2022 Test', academicYear='2021-2022-test') + term7 = Term.create(description='Fall 2022 Test', academicYear='2022-2023-test') program5 = Program.create(programName='Program5') + # Bob needs a service event in 2024-2025-test so currentSeniors subquery finds him + eventBobCurrent = Event.create(name='EventBobCurrent', term=term_bob_current, program=program5, + startDate=date(2024, 9, 1), isCanceled=False, deletionDate=None, isService=True) event5 = Event.create(name='Event5', term=term5, program=program5, startDate=date(2021, 9, 1), isCanceled=False, deletionDate=None, isService=True) event6 = Event.create(name='Event6', term=term6, program=program5, startDate=date(2022, 2, 1), @@ -386,25 +390,26 @@ def test_graduatingSeniorsVolunteerHours(fixture_info): event7 = Event.create(name='Event7', term=term7, program=program5, startDate=date(2022, 9, 1), isCanceled=False, deletionDate=None, isService=True) - # Give Bob 3 more unique semesters of service (he already has 1 from before - term2/2024-2025) + # Give Bob 4 unique semesters of service including one in 2024-2025-test + EventParticipant.create(user=fixture_info['user3'], event=eventBobCurrent, hoursEarned=2) EventParticipant.create(user=fixture_info['user3'], event=event5, hoursEarned=2) EventParticipant.create(user=fixture_info['user3'], event=event6, hoursEarned=3) EventParticipant.create(user=fixture_info['user3'], event=event7, hoursEarned=4) - # Bob now has 4 unique semesters total, and is a Senior in 2024-2025-test, so his info should appear + # Bob now has 4 unique semesters total, and is a Senior in 2024-2025-test columns, rows = graduatingSeniorsVolunteerHours("2024-2025-test") result = list(rows) assert len(result) == 1 - assert result[0] == ("Bob Builder", "builderb@berea.edu", "B00700932", 4, 9.0) + assert result[0] == ("Bob Builder", "builderb@berea.edu", "B00700932", 4, 11.0) - # Bob should NOT appear when querying a year where he is not a Senior (Bob is Senior only in 2024-2025) + # Bob should NOT appear when querying a year where he is not a Senior columns, rows = graduatingSeniorsVolunteerHours("2023-2024-test") assert list(rows) == [] # non-senior students should never appear even with enough semesters - extraTerm = Term.create(description='Spring 2021', academicYear='2020-2021-test') - extraTerm2 = Term.create(description='Fall 2020', academicYear='2020-2021-test') - extraTerm3 = Term.create(description='Spring 2020', academicYear='2019-2020-test') + extraTerm = Term.create(description='Spring 2021 Test', academicYear='2020-2021-test') + extraTerm2 = Term.create(description='Fall 2020 Test', academicYear='2020-2021-test') + extraTerm3 = Term.create(description='Spring 2020 Test', academicYear='2019-2020-test') event8 = Event.create(name='Event8', term=extraTerm, program=program5, startDate=date(2021, 2, 1), isCanceled=False, deletionDate=None, isService=True) @@ -413,11 +418,11 @@ def test_graduatingSeniorsVolunteerHours(fixture_info): event10 = Event.create(name='Event10', term=extraTerm3, program=program5, startDate=date(2020, 2, 1), isCanceled=False, deletionDate=None, isService=True) - # give John (Sophomore) 4 unique semesters, so it should never appear + # Give John (Sophomore) 4 unique semesters — should never appear EventParticipant.create(user=fixture_info['user1'], event=event8, hoursEarned=1) EventParticipant.create(user=fixture_info['user1'], event=event9, hoursEarned=1) EventParticipant.create(user=fixture_info['user1'], event=event10, hoursEarned=1) - # John already has term1 (2023-2024-test) from fixture, so now has 4 unique semesters + # John already has term5/term6/term7 plus one more = 4, but is Sophomore so should not appear columns, rows = graduatingSeniorsVolunteerHours("2023-2024-test") assert list(rows) == [] @@ -450,12 +455,12 @@ def test_graduatingSeniorsVolunteerHours(fixture_info): assert result[0] == ("James Smith", "smithj@berea.edu", "B999999", 4, 20.0) # non-service events should not be counted - nonServiceTerm = Term.create(description='Fall 2019', academicYear='2019-2020-test') + nonServiceTerm = Term.create(description='Fall 2019 Test', academicYear='2019-2020-test') nonServiceEvent = Event.create(name='NonServiceEvent', term=nonServiceTerm, program=program5, startDate=date(2019, 9, 1), isCanceled=False, deletionDate=None, isService=False) EventParticipant.create(user=fixture_info['user3'], event=nonServiceEvent, hoursEarned=5) - # Bob still has exactly 4 semesters with volunteer hours, the non-service event participation should not push the count up + # Bob still has exactly 4 service semesters, the non-service event should not push the count up columns, rows = graduatingSeniorsVolunteerHours("2024-2025-test") result = list(rows) - assert result[0][3] == 4 + assert result[0][3] == 4 \ No newline at end of file