diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index a7d52183f..22830cb07 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -20,6 +20,7 @@ from app.models.eventRsvpLog import EventRsvpLog from app.models.attachmentUpload import AttachmentUpload from app.models.bonnerCohort import BonnerCohort +from app.models.eventCohort import EventCohort from app.models.certification import Certification from app.models.user import User from app.models.term import Term @@ -30,7 +31,7 @@ from app.logic.createLogs import createActivityLog from app.logic.certification import getCertRequirements, updateCertRequirements from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget -from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRecurringEventsData, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId +from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRecurringEventsData, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId, inviteCohortsToEvent, updateEventCohorts from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp from app.logic.minor import getMinorInterest from app.logic.fileHandler import FileHandler @@ -131,11 +132,11 @@ def createEvent(templateid, programid): validationErrorMessage = "Failed to save event." if savedEvents: - rsvpcohorts = request.form.getlist("cohorts[]") - for year in rsvpcohorts: - rsvpForBonnerCohort(int(year), savedEvents[0].id) - addBonnerCohortToRsvpLog(int(year), savedEvents[0].id) - + rsvpCohorts = request.form.getlist("cohorts[]") + if rsvpCohorts: + success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts) + if not success: + flash(message, 'warning') noun = ((eventData.get('isRecurring') or eventData.get('isMultipleOffering')) and "Events" or "Event") # pluralize flash(f"{noun} successfully created!", 'success') @@ -177,7 +178,14 @@ def createEvent(templateid, programid): requirements, bonnerCohorts = [], [] if eventData['program'] is not None and eventData['program'].isBonnerScholars: requirements = getCertRequirements(Certification.BONNER) - bonnerCohorts = getBonnerCohorts(limit=5) + rawBonnerCohorts = getBonnerCohorts(limit=5) + bonnerCohorts = {} + + for year, cohort in rawBonnerCohorts.items(): + if cohort: + bonnerCohorts[year] = cohort + + return render_template(f"/events/{template.templateFile}", template = template, eventData = eventData, @@ -259,6 +267,10 @@ def eventDisplay(eventId): # Validate given URL try: event = Event.get_by_id(eventId) + invitedCohorts = list(EventCohort.select().where( + EventCohort.event == event + )) + invitedYears = [str(cohort.year) for cohort in invitedCohorts] except DoesNotExist as e: print(f"Unknown event: {eventId}") abort(404) @@ -293,11 +305,8 @@ def eventDisplay(eventId): if savedEvents: - rsvpcohorts = request.form.getlist("cohorts[]") - for year in rsvpcohorts: - rsvpForBonnerCohort(int(year), event.id) - addBonnerCohortToRsvpLog(int(year), event.id) - + rsvpCohorts = request.form.getlist("cohorts[]") + updateEventCohorts(savedEvents[0], rsvpCohorts) flash("Event successfully updated!", "success") return redirect(url_for("admin.eventDisplay", eventId = event.id)) else: @@ -314,7 +323,19 @@ def eventDisplay(eventId): if eventData['program'] and eventData['program'].isBonnerScholars: requirements = getCertRequirements(Certification.BONNER) - bonnerCohorts = getBonnerCohorts(limit=5) + rawBonnerCohorts = getBonnerCohorts(limit=5) + bonnerCohorts = {} + + for year, cohort in rawBonnerCohorts.items(): + if cohort: + bonnerCohorts[year] = cohort + + invitedCohorts = list(EventCohort.select().where( + EventCohort.event_id == eventId, + )) + invitedYears = [str(cohort.year) for cohort in invitedCohorts] + else: + requirements, bonnerCohorts, invitedYears = [], [], [] rule = request.url_rule @@ -322,10 +343,11 @@ def eventDisplay(eventId): if 'edit' in rule.rule: return render_template("events/createEvent.html", eventData = eventData, - futureTerms=futureTerms, + futureTerms = futureTerms, event = event, requirements = requirements, bonnerCohorts = bonnerCohorts, + invitedYears = invitedYears, userHasRSVPed = userHasRSVPed, isProgramManager = isProgramManager, filepaths = filepaths) @@ -350,6 +372,8 @@ def eventDisplay(eventId): currentEventRsvpAmount = getEventRsvpCount(event.id) userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term) + + return render_template("events/eventView.html", eventData=eventData, @@ -361,6 +385,7 @@ def eventDisplay(eventId): filepaths=filepaths, image=image, pageViewsCount=pageViewsCount, + invitedYears=invitedYears, eventCountdown=eventCountdown ) diff --git a/app/logic/bonner.py b/app/logic/bonner.py index 1fe11c5bd..dfd64780b 100644 --- a/app/logic/bonner.py +++ b/app/logic/bonner.py @@ -8,6 +8,7 @@ from app.models.bonnerCohort import BonnerCohort from app.models.eventRsvp import EventRsvp from app.models.user import User +from app.models.eventCohort import EventCohort from app.logic.createLogs import createRsvpLog def makeBonnerXls(): @@ -90,4 +91,4 @@ def addBonnerCohortToRsvpLog(year, event): .where(BonnerCohort.year == year)) for bonner in bonnerCohort: fullName = bonner.fullName - createRsvpLog(eventId=event, content=f"Added {fullName} to RSVP list.") \ No newline at end of file + createRsvpLog(eventId=event, content=f"Added {fullName} to RSVP list.") \ No newline at end of file diff --git a/app/logic/events.py b/app/logic/events.py index 8e0ad643c..dd2b34aca 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -17,7 +17,9 @@ from app.models.requirementMatch import RequirementMatch from app.models.certificationRequirement import CertificationRequirement from app.models.eventViews import EventView +from app.models.eventCohort import EventCohort +from app.logic.bonner import rsvpForBonnerCohort, addBonnerCohortToRsvpLog from app.logic.createLogs import createActivityLog, createRsvpLog from app.logic.utils import format24HourTime from app.logic.fileHandler import FileHandler @@ -706,3 +708,66 @@ def copyRsvpToNewEvent(priorEvent, newEvent): numRsvps = len(rsvpInfo) if numRsvps: createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}") + + +def inviteCohortsToEvent(event, cohortYears): + """ + Invites cohorts to a newly created event by associating the cohorts directly. + """ + invitedCohorts = [] + try: + for year in cohortYears: + year = int(year) + EventCohort.get_or_create( + event=event, + year=year, + defaults={'invited_at': datetime.now()} + ) + + addBonnerCohortToRsvpLog(year, event.id) + rsvpForBonnerCohort(year, event.id) + invitedCohorts.append(year) + + if invitedCohorts: + cohortList = ', '.join(map(str, invitedCohorts)) + createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}") + + return True, "Cohorts successfully added to new event", invitedCohorts + + except Exception as e: + print(f"Error inviting cohorts to new event: {e}") + return False, f"Error adding cohorts to new event: {e}", [] + +def updateEventCohorts(event, cohortYears): + """ + Updates the cohorts for an existing event by adding new ones and removing outdated ones. + """ + invitedCohorts = [] + try: + precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event)) + precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts] + + yearsToAdd = [year for year in cohortYears if year not in precedentInvitedYears] + + for year in yearsToAdd: + EventCohort.get_or_create( + event=event, + year=year, + defaults={'invited_at': datetime.now()} + ) + + addBonnerCohortToRsvpLog(year, event.id) + rsvpForBonnerCohort(year, event.id) + invitedCohorts.append(year) + + if yearsToAdd: + cohortList = ', '.join(map(str, invitedCohorts)) + createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}") + + return True, "Cohorts successfully updated for event", invitedCohorts + + except Exception as e: + print(f"Error updating cohorts for event: {e}") + return False, f"Error updating cohorts for event: {e}", [] + + diff --git a/app/models/eventCohort.py b/app/models/eventCohort.py new file mode 100644 index 000000000..e60a02db7 --- /dev/null +++ b/app/models/eventCohort.py @@ -0,0 +1,13 @@ +from datetime import datetime +from app.models import* +from app.models.event import Event +from app.models.bonnerCohort import BonnerCohort + +class EventCohort(baseModel): + event = ForeignKeyField(Event) + year = IntegerField() + invited_at = DateTimeField(default=datetime.now) + + class Meta: + indexes = ( (('event', 'year'), True), ) + diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index 6381cb6ca..7f4557f15 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -332,11 +332,12 @@ function formatDate(originalDate) { */ $(document).ready(function() { //makes sure bonners toggle will stay on between event pages - if (window.location.pathname == '/event/' + $('#newEventID').val() + '/edit') { - if ($("#checkBonners")) { - $("#checkBonners").prop('checked', true); + if (window.location.pathname == '/event/' + $('#newEventID').val() + '/edit') { + if ($("#checkBonners")) { + $("#checkBonners").prop('checked', true); + } } -} + // Initialize datepicker with proper options $.datepicker.setDefaults({ dateFormat: 'yy/mm/dd', // Ensures compatibility across browsers diff --git a/app/templates/events/createEvent.html b/app/templates/events/createEvent.html index 1dbd75c28..6baed091a 100644 --- a/app/templates/events/createEvent.html +++ b/app/templates/events/createEvent.html @@ -235,11 +235,17 @@

{{page_title}}

- {% for year,students in bonnerCohorts.items()|reverse %} + {% for year, students in bonnerCohorts.items() | reverse %}
- -
{% endfor %} diff --git a/app/templates/sidebar.html b/app/templates/sidebar.html index a1f2f7a37..7209c437d 100644 --- a/app/templates/sidebar.html +++ b/app/templates/sidebar.html @@ -33,7 +33,7 @@ {% if g.current_user.isAdmin %}
  • - Create Event + Create Event
  • diff --git a/database/migrate_db.sh b/database/migrate_db.sh index 7a4804a03..571dbd9a8 100755 --- a/database/migrate_db.sh +++ b/database/migrate_db.sh @@ -70,6 +70,7 @@ pem add app.models.eventRsvpLog.EventRsvpLog pem add app.models.celtsLabor.CeltsLabor pem add app.models.communityEngagementRequest.CommunityEngagementRequest pem add app.models.individualRequirement.IndividualRequirement +pem add app.models.eventCohort.EventCohort pem watch diff --git a/tests/code/test_bonner.py b/tests/code/test_bonner.py index d7ef55743..ee9fa8f1b 100644 --- a/tests/code/test_bonner.py +++ b/tests/code/test_bonner.py @@ -1,18 +1,24 @@ import pytest -from datetime import date -from peewee import IntegrityError +from flask import g +from app import app +from datetime import date, datetime, timedelta +from peewee import IntegrityError, fn from app.models.user import User from app.models import mainDB from app.models.bonnerCohort import BonnerCohort from app.models.eventRsvp import EventRsvp +from app.models.eventRsvpLog import EventRsvpLog +from app.models.event import Event +from app.models.program import Program -from app.logic.bonner import getBonnerCohorts, rsvpForBonnerCohort +from app.logic.bonner import getBonnerCohorts, rsvpForBonnerCohort, addBonnerCohortToRsvpLog @pytest.mark.integration def test_getBonnerCohorts(): with mainDB.atomic() as transaction: + # reset pre-determined bonner cohorts BonnerCohort.delete().execute() @@ -89,5 +95,74 @@ def test_bonnerRsvp(): assert EventRsvp.select().where(EventRsvp.event == event_id).count() == 3 transaction.rollback() + +@pytest.mark.integration +def test_addBonnerCohortToRsvpLog(): + with mainDB.atomic() as transaction: + with app.app_context(): + g.current_user = "heggens" + + # reset pre-determined bonner cohorts + BonnerCohort.delete().execute() + + currentYear = 2024 + + # create BonnerCohort entries for a set of valid users for the given year. + BonnerCohort.create(user="ramsayb2", year=currentYear) + BonnerCohort.create(user="qasema", year=currentYear) + BonnerCohort.create(user="neillz", year=currentYear) + BonnerCohort.create(user="khatts", year=currentYear) + + testDate = datetime.strptime("2025-01-19 05:00","%Y-%m-%d %H:%M") + + # Create a test event associated with a Bonner Scholars program + programEvent = Program.create(id = 15, + programName = "Bonner Scholars", + isStudentLed = False, + isBonnerScholars = True, + contactEmail = "test@email", + contactName = "testName") + + event = Event.create(name = "Upcoming Bonner Scholars Event", + term = 4, + description = "Test upcoming bonner event.", + location = "Stephenson Building", + startDate = testDate, + endDate = testDate + timedelta(days=1), + program = programEvent) + + addBonnerCohortToRsvpLog(currentYear, event) + + users = { + 'ramsayb2' : 'Brian Ramsay', + 'khatts' : 'Sreynit Khatt', + 'qasema' : 'Ala Qasem', + 'neillz' : 'Zach Neill' + } + + # Assert that the RSVP log contains entries for all valid users in the BonnerCohort. + for name in users.values(): + content = f"Added {name} to RSVP list." + assert EventRsvpLog.select().where(EventRsvpLog.event == event, EventRsvpLog.rsvpLogContent == content).exists() + + invalidUsers = { + 'michels' : 'Stevenson Michel', + 'blesoef' : 'Finn Bledsoe', + 'makindeo' : 'Oluwagbayi Makinde', + 'zawn' : 'Nyan Zaw' + } + + # Assert that no RSVP log entries are created for invalid users. + for name in invalidUsers.values(): + content = f"Added {name} to RSVP list." + assert not EventRsvpLog.select().where(EventRsvpLog.event == event, EventRsvpLog.rsvpLogContent == content).exists() + + # Verify that the total number of RSVP log entries matches the number of valid cohort users. + assert EventRsvpLog.select().where(EventRsvpLog.event == event).count() == 4 + assert not EventRsvpLog.select().where(EventRsvpLog.event == event).count() == 7 + + transaction.rollback() + + diff --git a/tests/code/test_events.py b/tests/code/test_events.py index 19535bbe3..c6363e480 100644 --- a/tests/code/test_events.py +++ b/tests/code/test_events.py @@ -22,8 +22,10 @@ from app.models.interest import Interest from app.models.eventRsvp import EventRsvp from app.models.note import Note +from app.models.bonnerCohort import BonnerCohort +from app.models.eventCohort import EventCohort -from app.logic.events import preprocessEventData, validateNewEventData, getRecurringEventsData +from app.logic.events import preprocessEventData, validateNewEventData, getRecurringEventsData, inviteCohortsToEvent, updateEventCohorts from app.logic.events import attemptSaveEvent, attemptSaveMultipleOfferings, saveEventToDb, cancelEvent, deleteEvent, getParticipatedEventsForUser from app.logic.events import calculateNewrecurringId, getPreviousRecurringEventData, getUpcomingEventsForUser, calculateNewMultipleOfferingId, getPreviousMultipleOfferingEventData from app.logic.events import deleteEventAndAllFollowing, deleteAllRecurringEvents, getEventRsvpCountsForTerm, getEventRsvpCount, getCountdownToEvent, copyRsvpToNewEvent @@ -1372,6 +1374,23 @@ def test_copyRsvpToNewEvent(): priorEvent = Event.create(name = "Req and Limit", + term = 2, + description = "Event that requries RSVP and has an RSVP limit set.", + timeStart = "6:00 pm", + timeEnd = "9:00 pm", + location = "The Moon", + isRsvpRequired = 1, + startDate = "2022-12-19", + endDate = "2022-12-19", + program = 9) + + priorEvent.save() + EventRsvp.create(user = "neillz", + event = priorEvent).save() + EventRsvp.create(user = "partont", + event = priorEvent).save() + + newEvent = Event.create(name = "Req and Limit", term = 2, description = "Event that requries RSVP and has an RSVP limit set.", timeStart = "6:00 pm", @@ -1381,23 +1400,6 @@ def test_copyRsvpToNewEvent(): startDate = "2022-12-19", endDate = "2022-12-19", program = 9) - - priorEvent.save() - EventRsvp.create(user = "neillz", - event = priorEvent).save() - EventRsvp.create(user = "partont", - event = priorEvent).save() - - newEvent = Event.create(name = "Req and Limit", - term = 2, - description = "Event that requries RSVP and has an RSVP limit set.", - timeStart = "6:00 pm", - timeEnd = "9:00 pm", - location = "The Moon", - isRsvpRequired = 1, - startDate = "2022-12-19", - endDate = "2022-12-19", - program = 9) newEvent.save() assert len(EventRsvp.select().where(EventRsvp.event_id == priorEvent)) == 2 @@ -1407,3 +1409,101 @@ def test_copyRsvpToNewEvent(): assert len(EventRsvp.select().where(EventRsvp.event_id == newEvent)) == 2 transaction.rollback() + + +@pytest.mark.integration +def test_inviteCohortsToEvent(): + """ + This function creates a Bonner Scholar program, attaches an event to this program, + and creates invited cohorts for this event. + """ + with mainDB.atomic() as transaction: + with app.app_context(): + g.current_user = "heggens" + + testDate = datetime.strptime("2025-08-01 05:00","%Y-%m-%d %H:%M") + programEvent = Program.create(id = 13, + programName = "Bonner Scholars", + isStudentLed = False, + isBonnerScholars = True, + contactEmail = "test@email", + contactName = "testName") + + event = Event.create(name = "Upcoming Bonner Scholars Event", + term = 2, + description = "Test upcoming bonner event.", + location = "Stephenson Building", + startDate = testDate, + endDate = testDate + timedelta(days=1), + program = programEvent) + + cohortYears = ["2020", "2021", "2024"] + success, message, invitedCohorts = inviteCohortsToEvent(event, cohortYears) + + assert success is True + assert message == "Cohorts successfully added to new event" + assert invitedCohorts == [2020, 2021, 2024] + transaction.rollback() + +@pytest.mark.integration +def test_updateEventCohorts(): + """ + This function creates a Bonner Scholar program, attaches an event to this program, + creates invited cohorts for this event, + and updates the cohorts attached to this event. + """ + with mainDB.atomic() as transaction: + with app.app_context(): + g.current_user = "heggens" + + testDate = datetime.strptime("2025-10-01 05:00","%Y-%m-%d %H:%M") + programEvent = Program.create(id = 13, + programName = "Bonner Scholars", + isStudentLed = False, + isBonnerScholars = True, + contactEmail = "test@email", + contactName = "testName") + + event = Event.create(name = "Upcoming Bonner Scholars event", + term = 2, + description = "Test upcoming bonner event.", + location = "MAC Building", + startDate = testDate, + endDate = testDate + timedelta(days=1), + program = programEvent) + + bonnerCohort1 = BonnerCohort.create(year = "2021", + user = "heggens") + + bonnerCohort2 = BonnerCohort.create(year = "2020", + user = "khatts") + + bonnerCohort3 = BonnerCohort.create(year = "2024", + user = "mupotsal") + + cohortYear1 = bonnerCohort1.year + cohortYear2 = bonnerCohort2.year + cohortYear3 = bonnerCohort3.year + + EventCohort.create(event = event, + invited_at = datetime.now(), + year = cohortYear1) + + EventCohort.create(event = event, + invited_at = datetime.now(), + year = cohortYear2) + + EventCohort.create(event = event, + invited_at = datetime.now(), + year = cohortYear3) + + cohortYears = ["2020", "2022", "2023"] + + success, message, updatedCohorts = updateEventCohorts(event, cohortYears) + + assert success is True + assert message == "Cohorts successfully updated for event" + assert updatedCohorts == [2022, 2023] + + transaction.rollback() +