From a7f53c75d494a9293199fd3350b684f16745af9d Mon Sep 17 00:00:00 2001 From: Karina Date: Thu, 2 Apr 2026 17:28:12 -0400 Subject: [PATCH 1/2] 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 255cbc97b..51b4d94e0 100644 --- a/app/logic/volunteerSpreadsheet.py +++ b/app/logic/volunteerSpreadsheet.py @@ -206,6 +206,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)) @@ -271,6 +297,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 d873bd3cdf8158642bebeb8520bcd63b45930e97 Mon Sep 17 00:00:00 2001 From: Karina Date: Tue, 7 Apr 2026 15:37:08 -0400 Subject: [PATCH 2/2] Added tests to grad seniors reports spreadsheet --- tests/code/test_spreadsheet.py | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/code/test_spreadsheet.py b/tests/code/test_spreadsheet.py index d66139c33..6f9d189fd 100644 --- a/tests/code/test_spreadsheet.py +++ b/tests/code/test_spreadsheet.py @@ -684,4 +684,96 @@ def test_getUniqueVolunteers(fixture_info): ]) +@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