From a7f53c75d494a9293199fd3350b684f16745af9d Mon Sep 17 00:00:00 2001 From: Karina Date: Thu, 2 Apr 2026 17:28:12 -0400 Subject: [PATCH 1/9] 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/9] 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 From e1760383716d986d9fc474f84342f35bf8c31532 Mon Sep 17 00:00:00 2001 From: zawn Date: Thu, 23 Apr 2026 16:55:33 -0400 Subject: [PATCH 3/9] The participanttype is done --- app/logic/events.py | 14 ++++++++------ app/templates/main/userProfile.html | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/logic/events.py b/app/logic/events.py index 822d85632..45a8af83b 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -1,5 +1,5 @@ from flask import url_for, g, session -from peewee import DoesNotExist, fn, JOIN +from peewee import DoesNotExist, fn, JOIN, Case, Value from dateutil import parser from datetime import timedelta, date, datetime from dateutil.relativedelta import relativedelta @@ -399,20 +399,22 @@ def getParticipatedEventsForUser(user): :return: A list of Event objects """ - participatedEvents = (Event.select(Event, Program.programName) + participatedEvents = (Event.select(Event, Program.programName, Case(None, ((Event.isLaborOnly, "Labor"),(Event.name.contains("Labor"), "Labor")), "Attendee").alias("participatedType")) .join(Program, JOIN.LEFT_OUTER).switch() .join(EventParticipant) .where(EventParticipant.user == user, Event.isAllVolunteerTraining == False, Event.deletionDate == None) .order_by(Event.startDate, Event.name)) - - allVolunteer = (Event.select(Event, "") + allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType")) .join(EventParticipant) .where(Event.isAllVolunteerTraining == True, EventParticipant.user == user)) + for v in allVolunteer: + print("SSSSSS", v) union = participatedEvents.union_all(allVolunteer) - unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.programName, union.c.startDate, union.c.name).order_by(union.c.startDate, union.c.name).execute()) - + unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType).order_by(union.c.startDate, union.c.name).execute()) + for events in unionParticipationWithVolunteer: + print(events.__dict__, "lele") return unionParticipationWithVolunteer def validateNewEventData(data): diff --git a/app/templates/main/userProfile.html b/app/templates/main/userProfile.html index e29db4895..49fa59764 100644 --- a/app/templates/main/userProfile.html +++ b/app/templates/main/userProfile.html @@ -167,12 +167,14 @@

Program Event Name + Participation Type Event Date {% for event in participatedEvents %} {{event.programName}} {{event.name}} + {{event.participatedType}} {{event.startDate.strftime('%m/%d/%Y')}} {% endfor %} From d499663869bcadabede0da9341066bf8c5621688 Mon Sep 17 00:00:00 2001 From: zawn Date: Mon, 27 Apr 2026 12:58:08 -0400 Subject: [PATCH 4/9] remove print --- app/logic/events.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/logic/events.py b/app/logic/events.py index 45a8af83b..55cb48896 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -409,12 +409,8 @@ def getParticipatedEventsForUser(user): .join(EventParticipant) .where(Event.isAllVolunteerTraining == True, EventParticipant.user == user)) - for v in allVolunteer: - print("SSSSSS", v) union = participatedEvents.union_all(allVolunteer) unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType).order_by(union.c.startDate, union.c.name).execute()) - for events in unionParticipationWithVolunteer: - print(events.__dict__, "lele") return unionParticipationWithVolunteer def validateNewEventData(data): From 5c52d370bda44198ac17a75c6f926783ab2edb10 Mon Sep 17 00:00:00 2001 From: zawn Date: Mon, 27 Apr 2026 18:27:17 -0400 Subject: [PATCH 5/9] added considerations on labor, volunteers and fixed the bug on cases where it isService --- app/logic/events.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/logic/events.py b/app/logic/events.py index 55cb48896..274de795c 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -398,8 +398,10 @@ def getParticipatedEventsForUser(user): Used in testing, defaults to the current timestamp. :return: A list of Event objects """ - - participatedEvents = (Event.select(Event, Program.programName, Case(None, ((Event.isLaborOnly, "Labor"),(Event.name.contains("Labor"), "Labor")), "Attendee").alias("participatedType")) + participatedEvents = (Event.select(Event, Program.programName, Case(None, ( + ((Event.isLaborOnly | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"), + ((Event.isLaborOnly | Event.name.contains("Labor")), "Labor"), + (Event.isService, "Volunteer")), "Attendee").alias("participatedType")) .join(Program, JOIN.LEFT_OUTER).switch() .join(EventParticipant) .where(EventParticipant.user == user, From e4cfec6d1b1bbbbcf39076705b8267bea6d9a951 Mon Sep 17 00:00:00 2001 From: bakobagassas Date: Thu, 30 Apr 2026 15:34:17 -0400 Subject: [PATCH 6/9] initializing datatables before registering for order --- app/static/js/volunteerDetails.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/static/js/volunteerDetails.js b/app/static/js/volunteerDetails.js index 1b6192a23..18f0dc650 100644 --- a/app/static/js/volunteerDetails.js +++ b/app/static/js/volunteerDetails.js @@ -5,6 +5,15 @@ $(document).ready(function () { }).get(); users = new Set(users); users = [... users] + + const columnMap = { + phoneSelect: 1, + emailSelect: 2, + statusSelect: 3, + dietRestrictionSelect: 4, + emergencyContactSelect: 5, + insuranceSelect: 6, + }; $("#tableCardToggle").on('click', function () { $("#volunteerInformationCardToPrint").toggle() $("#volunteerInformationTableToPrint_wrapper").toggle() From 6ab49ef28b0c1bd1882f7c207a1196196914420b Mon Sep 17 00:00:00 2001 From: bakobagassas Date: Thu, 30 Apr 2026 15:37:03 -0400 Subject: [PATCH 7/9] Showing and hiding fully with datatables --- app/static/js/volunteerDetails.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/static/js/volunteerDetails.js b/app/static/js/volunteerDetails.js index 18f0dc650..6bffc6e5c 100644 --- a/app/static/js/volunteerDetails.js +++ b/app/static/js/volunteerDetails.js @@ -58,15 +58,18 @@ $(document).ready(function () { function getCheckBoxes() { $(".displayCheckbox").each(function () { let checkboxId = this.id; - if ($('#' + checkboxId).is(':checked')) { + let isChecked = $(this).is(':checked'); + if (checkboxId in columnMap) { + volunteerInfoTable.column(columnMap[checkboxId]).visible(isChecked); + } + if (isChecked) { $("#volunteerInformationCardToPrint ." + checkboxId).show(); } else { $("#volunteerInformationCardToPrint ." + checkboxId).hide(); } - }); + }); hideDuplicateVolunteers() volunteerInfoTable.page('first').draw(false); - } function sortVolunteers() { @@ -78,7 +81,8 @@ $(document).ready(function () { let textB = b.getElementsByClassName('nameSelect')[0].innerText return textA.localeCompare(textB); }); - + let sortedCards = $("#volunteerInformationCardToPrint .sort-here"); + let entriesCards = sortedCards.find(".volunteerInfoEntries"); entriesCards.appendTo(sortedCards); }; From 2e793ff9247b329437e92c7824e23c24b2bdbf98 Mon Sep 17 00:00:00 2001 From: bakobagassas Date: Thu, 30 Apr 2026 16:04:22 -0400 Subject: [PATCH 8/9] rearranged the included infos order --- app/templates/events/volunteerDetails.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/templates/events/volunteerDetails.html b/app/templates/events/volunteerDetails.html index 6464a869c..07d4df486 100644 --- a/app/templates/events/volunteerDetails.html +++ b/app/templates/events/volunteerDetails.html @@ -117,18 +117,18 @@


- -
- -

- -

+ +
+ +
+ +
-
+

From f60631ca8f4c8670102a9881cb75b8b9c4dbb97f Mon Sep 17 00:00:00 2001 From: bakobagassas Date: Fri, 1 May 2026 10:29:00 -0400 Subject: [PATCH 9/9] reverted from changes I accidentally committed to this branch --- app/static/js/volunteerDetails.js | 24 +++++----------------- app/templates/events/volunteerDetails.html | 14 ++++++------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/app/static/js/volunteerDetails.js b/app/static/js/volunteerDetails.js index 6bffc6e5c..436691673 100644 --- a/app/static/js/volunteerDetails.js +++ b/app/static/js/volunteerDetails.js @@ -5,15 +5,6 @@ $(document).ready(function () { }).get(); users = new Set(users); users = [... users] - - const columnMap = { - phoneSelect: 1, - emailSelect: 2, - statusSelect: 3, - dietRestrictionSelect: 4, - emergencyContactSelect: 5, - insuranceSelect: 6, - }; $("#tableCardToggle").on('click', function () { $("#volunteerInformationCardToPrint").toggle() $("#volunteerInformationTableToPrint_wrapper").toggle() @@ -58,31 +49,26 @@ $(document).ready(function () { function getCheckBoxes() { $(".displayCheckbox").each(function () { let checkboxId = this.id; - let isChecked = $(this).is(':checked'); - if (checkboxId in columnMap) { - volunteerInfoTable.column(columnMap[checkboxId]).visible(isChecked); - } - if (isChecked) { + if ($('#' + checkboxId).is(':checked')) { $("#volunteerInformationCardToPrint ." + checkboxId).show(); } else { $("#volunteerInformationCardToPrint ." + checkboxId).hide(); } - }); + }); hideDuplicateVolunteers() volunteerInfoTable.page('first').draw(false); + } function sortVolunteers() { - let sortedTable = $("#volunteerInformationTableToPrint_wrapper"); - let entriesTable = sortedTable.find(".volunteerInfoEntries"); + entriesTable.sort(function (a, b) { let textA = a.getElementsByClassName('nameSelect')[0].innerText let textB = b.getElementsByClassName('nameSelect')[0].innerText return textA.localeCompare(textB); }); - let sortedCards = $("#volunteerInformationCardToPrint .sort-here"); - let entriesCards = sortedCards.find(".volunteerInfoEntries"); + entriesCards.appendTo(sortedCards); }; diff --git a/app/templates/events/volunteerDetails.html b/app/templates/events/volunteerDetails.html index 07d4df486..6464a869c 100644 --- a/app/templates/events/volunteerDetails.html +++ b/app/templates/events/volunteerDetails.html @@ -117,18 +117,18 @@


- -
- -
- -


+ +
+ +
+ +
-
+