Coverage for app/logic/events.py: 93%
333 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-21 23:52 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-21 23:52 +0000
1from flask import url_for, g, session
2from peewee import DoesNotExist, fn, JOIN
3from dateutil import parser
4from datetime import timedelta, date, datetime
5from dateutil.relativedelta import relativedelta
6from werkzeug.datastructures import MultiDict
7import json
8from app.models import mainDB
9from app.models.user import User
10from app.models.event import Event
11from app.models.eventParticipant import EventParticipant
12from app.models.program import Program
13from app.models.term import Term
14from app.models.programBan import ProgramBan
15from app.models.interest import Interest
16from app.models.eventRsvp import EventRsvp
17from app.models.requirementMatch import RequirementMatch
18from app.models.certificationRequirement import CertificationRequirement
19from app.models.eventViews import EventView
20from app.models.eventCohort import EventCohort
22from app.logic.bonner import rsvpForBonnerCohort, addBonnerCohortToRsvpLog
23from app.logic.createLogs import createActivityLog, createRsvpLog
24from app.logic.utils import format24HourTime
25from app.logic.fileHandler import FileHandler
26from app.logic.certification import updateCertRequirementForEvent
28def cancelEvent(eventId):
29 """
30 Cancels an event.
31 """
32 event = Event.get_or_none(Event.id == eventId)
33 if event:
34 event.isCanceled = True
35 event.save()
37 program = event.program
38 createActivityLog(f"Canceled <a href= \"{url_for('admin.eventDisplay', eventId = event.id)}\" >{event.name}</a> for {program.programName}, which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
40#NEEDS FIXING: process not working properly for repeating events when two events are deleted consecutively
41def deleteEvent(eventId):
42 """
43 Deletes an event, if it is a repeating event, rename all following events
44 to make sure there is no gap in weeks.
46 """
47 event = Event.get_or_none(Event.id == eventId)
49 if event:
50 if event.isRepeating:
51 seriesId = event.seriesId
52 repeatingEvents = list(Event.select().where(Event.seriesId==seriesId).order_by(Event.id)) # orders for tests
53 eventDeleted = False
54 # once the deleted event is detected, change all other names to the previous event's name
55 for repeatingEvent in repeatingEvents:
56 if eventDeleted:
57 Event.update({Event.name:newEventName}).where(Event.id==repeatingEvent.id).execute()
58 newEventName = repeatingEvent.name
60 if repeatingEvent == event:
61 newEventName = repeatingEvent.name
62 eventDeleted = True
64 program = event.program
66 if program:
67 createActivityLog(f"Deleted \"{event.name}\" for {program.programName}, which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
68 else:
69 createActivityLog(f"Deleted a non-program event, \"{event.name}\", which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
71 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where(Event.id == event.id).execute()
73def deleteEventAndAllFollowing(eventId):
74 """
75 Deletes an event in the series and all events after it
77 """
78 event = Event.get_or_none(Event.id == eventId)
79 if event:
80 if event.seriesId:
81 seriesId = event.seriesId
82 eventSeries = list(Event.select(Event.id).where((Event.seriesId == seriesId) & (Event.startDate >= event.startDate)))
83 deletedEventList = [event.id for event in eventSeries]
84 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where((Event.seriesId == seriesId) & (Event.startDate >= event.startDate)).execute()
85 return deletedEventList
87def deleteAllEventsInSeries(eventId):
88 """
89 Deletes all events in a series by getting the first event in the series and calling deleteEventAndAllFollowing().
91 """
92 event = Event.get_or_none(Event.id == eventId)
93 if event:
94 if event.seriesId:
95 seriesId = event.seriesId
96 allSeriesEvents = list(Event.select(Event.id).where(Event.seriesId == seriesId).order_by(Event.startDate))
97 eventId = allSeriesEvents[0].id
98 return deleteEventAndAllFollowing(eventId)
99 else:
100 raise ValueError(f"Event with id {event.id} does not belong to a series (seriesId is None).")
102def attemptSaveMultipleOfferings(eventData, attachmentFiles = None):
103 """
104 Tries to save an event with multiple offerings to the database:
105 Creates separate event data inheriting from the original eventData
106 with the specifics of each offering.
107 Calls attemptSaveEvent on each of the newly created datum
108 If any data is not valid it will return a validation error.
110 Returns:
111 allSavesWereSuccessful : bool | Whether or not all offering saves were successful
112 savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesWereSuccessful is False then this list will be empty.
113 failedSavedOfferings : List[(int, str), ...] | Tuples containing the indicies of failed saved offerings and the associated validation error message.
114 """
115 savedOfferings = []
116 failedSavedOfferings = []
117 allSavesWereSuccessful = True
119 seriesId = calculateNewSeriesId()
121 # Create separate event data inheriting from the original eventData
122 seriesData = eventData.get('seriesData')
123 isRepeating = bool(eventData.get('isRepeating'))
124 with mainDB.atomic() as transaction:
125 for index, event in enumerate(seriesData):
126 eventInfo = eventData.copy()
127 eventInfo.update({
128 'name': event['eventName'],
129 'startDate': event['eventDate'],
130 'timeStart': event['startTime'],
131 'timeEnd': event['endTime'],
132 'location': eventData['location'],
133 'seriesId': seriesId,
134 'isRepeating': bool(isRepeating),
135 })
136 # Try to save each offering
137 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles)
138 if validationErrorMessage:
139 failedSavedOfferings.append((index, validationErrorMessage))
140 allSavesWereSuccessful = False
141 else:
142 savedEvent = savedEvents[0]
143 savedOfferings.append(savedEvent)
144 if not allSavesWereSuccessful:
145 savedOfferings = []
146 transaction.rollback()
148 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings
151def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
152 """
153 Tries to save an event to the database:
154 Checks that the event data is valid and if it is, it continues to save the new
155 event to the database and adds files if there are any.
156 If it is not valid it will return a validation error.
158 Returns:
159 The saved event, created events and an error message if an error occurred.
160 """
162 # Manually set the value of RSVP Limit if it is and empty string since it is
163 # automatically changed from "" to 0
164 if eventData["rsvpLimit"] == "":
165 eventData["rsvpLimit"] = None
167 newEventData = preprocessEventData(eventData)
169 isValid, validationErrorMessage = validateNewEventData(newEventData)
170 if not isValid:
171 return [], validationErrorMessage
173 events = saveEventToDb(newEventData, renewedEvent)
174 if attachmentFiles:
175 for event in events:
176 addFile = FileHandler(attachmentFiles, eventId=event.id)
177 addFile.saveFiles(saveOriginalFile=events[0])
178 return events, ""
181def saveEventToDb(newEventData, renewedEvent = False):
183 if not newEventData.get('valid', False) and not renewedEvent:
184 raise Exception("Unvalidated data passed to saveEventToDb")
186 isNewEvent = ('id' not in newEventData)
188 eventRecords = []
189 with mainDB.atomic():
191 eventData = {
192 "term": newEventData['term'],
193 "name": newEventData['name'],
194 "description": newEventData['description'],
195 "timeStart": newEventData['timeStart'],
196 "timeEnd": newEventData['timeEnd'],
197 "location": newEventData['location'],
198 "isFoodProvided" : newEventData['isFoodProvided'],
199 "isLaborOnly" : newEventData['isLaborOnly'],
200 "isTraining": newEventData['isTraining'],
201 "isEngagement": newEventData['isEngagement'],
202 "isRsvpRequired": newEventData['isRsvpRequired'],
203 "isService": newEventData['isService'],
204 "startDate": newEventData['startDate'],
205 "rsvpLimit": newEventData['rsvpLimit'],
206 "contactEmail": newEventData['contactEmail'],
207 "contactName": newEventData['contactName'],
208 }
210 # The three fields below are only relevant during event creation so we only set/change them when
211 # it is a new event.
212 if isNewEvent:
213 eventData['program'] = newEventData['program']
214 eventData['seriesId'] = newEventData.get('seriesId')
215 eventData['isRepeating'] = bool(newEventData.get('isRepeating'))
216 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
217 eventRecord = Event.create(**eventData)
218 else:
219 eventRecord = Event.get_by_id(newEventData['id'])
220 Event.update(**eventData).where(Event.id == eventRecord).execute()
222 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
223 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
225 eventRecords.append(eventRecord)
226 return eventRecords
228def getVolunteerOpportunities(term):
229 volunteerOpportunities = list(Event.select(Event, Program)
230 .join(Program)
231 .where((Event.term == term) &
232 (Event.deletionDate.is_null(True)) &
233 (Event.isService == True) &
234 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True))
235 )
236 .order_by(Event.startDate, Event.timeStart)
237 .execute())
239 programs = {}
241 for event in volunteerOpportunities:
242 programs.setdefault(event.program, []).append(event)
244 return programs
246def getEngagementEvents(term):
247 engagementEvents = list(Event.select(Event, Program)
248 .join(Program)
249 .where(Event.isEngagement, Event.isLaborOnly == False,
250 Event.term == term, Event.deletionDate == None)
251 .order_by(Event.startDate, Event.timeStart)
252 .execute())
253 return engagementEvents
255def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
256 """
257 Return a count of all upcoming events for each volunteer opportunitiesprogram.
258 """
260 upcomingCount = (
261 Program
262 .select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
263 .join(Event, on=(Program.id == Event.program_id))
264 .where(
265 (Event.term == term) &
266 (Event.deletionDate.is_null(True)) &
267 (Event.isService == True) &
268 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
269 ((Event.startDate > currentTime) |
270 ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime))) &
271 (Event.isCanceled == False)
272 )
273 .group_by(Program.id)
274 )
276 programCountDict = {}
277 for programCount in upcomingCount:
278 programCountDict[programCount.id] = programCount.eventCount
279 return programCountDict
281def getTrainingEvents(term, user):
282 """
283 The allTrainingsEvent query is designed to select and count eventId's after grouping them
284 together by id's of similiar value. The query will then return the event that is associated
285 with the most programs (highest count) by doing this we can ensure that the event being
286 returned is the All Trainings Event.
287 term: expected to be the ID of a term
288 user: expected to be the current user
289 return: a list of all trainings the user can view
290 """
291 trainingQuery = (Event.select(Event).distinct()
292 .join(Program, JOIN.LEFT_OUTER)
293 .where(Event.isTraining == True, Event.isLaborOnly == False,
294 Event.term == term, Event.deletionDate == None)
295 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
297 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
298 if hideBonner:
299 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
301 return list(trainingQuery.execute())
303def getBonnerEvents(term):
304 bonnerScholarsEvents = list(
305 Event.select(Event, Program.id.alias("program_id"))
306 .join(Program)
307 .where(
308 Program.isBonnerScholars,
309 Event.term == term,
310 Event.deletionDate == None
311 )
312 .order_by(Event.startDate, Event.timeStart)
313 .execute()
314 )
315 return bonnerScholarsEvents
317def getCeltsLabor(term):
318 """
319 Labor tab: events explicitly marked as Labor Only.
320 """
321 celtsLabor = list(Event.select()
322 .where(Event.term == term, Event.deletionDate == None, Event.isLaborOnly == True)
323 .order_by(Event.startDate, Event.timeStart, Event.id)
324 .execute())
325 return celtsLabor
327def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
328 """
329 Get the list of upcoming events that the user is interested in as long
330 as they are not banned from the program that the event is a part of.
331 :param user: a username or User object
332 :param asOf: The date to use when determining future and past events.
333 Used in testing, defaults to the current timestamp.
334 :return: A list of Event objects
335 """
337 events = (Event.select().distinct()
338 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
339 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
340 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
341 .where(Event.deletionDate == None, Event.startDate >= asOf,
342 (Interest.user == user) | (EventRsvp.user == user),
343 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
345 if program:
346 events = events.where(Event.program == program)
348 events = events.order_by(Event.startDate, Event.timeStart)
350 eventsList = []
351 seriesEventsList = []
353 # removes all events in series except for the next upcoming one
354 for event in events:
355 if event.seriesId:
356 if not event.isCanceled:
357 if event.seriesId not in seriesEventsList:
358 eventsList.append(event)
359 seriesEventsList.append(event.seriesId)
360 else:
361 if not event.isCanceled:
362 eventsList.append(event)
364 return eventsList
366def getParticipatedEventsForUser(user):
367 """
368 Get all the events a user has participated in.
369 :param user: a username or User object
370 :param asOf: The date to use when determining future and past events.
371 Used in testing, defaults to the current timestamp.
372 :return: A list of Event objects
373 """
375 participatedEvents = (Event.select(Event, Program.programName)
376 .join(Program, JOIN.LEFT_OUTER).switch()
377 .join(EventParticipant)
378 .where(EventParticipant.user == user,
379 Event.isAllVolunteerTraining == False, Event.deletionDate == None)
380 .order_by(Event.startDate, Event.name))
382 allVolunteer = (Event.select(Event, "")
383 .join(EventParticipant)
384 .where(Event.isAllVolunteerTraining == True,
385 EventParticipant.user == user))
386 union = participatedEvents.union_all(allVolunteer)
387 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())
389 return unionParticipationWithVolunteer
391def validateNewEventData(data):
392 """
393 Confirm that the provided data is valid for an event.
395 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
397 Returns 3 values: (boolean success, the validation error message, the data object)
398 """
400 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['isLaborOnly']]:
401 return (False, "Raw form data passed to validate method. Preprocess first.")
403 if data['timeEnd'] <= data['timeStart']:
404 return (False, "Event end time must be after start time.")
406 # Validation if we are inserting a new event
407 if 'id' not in data:
409 sameEventList = list((Event.select().where((Event.name == data['name']) &
410 (Event.location == data['location']) &
411 (Event.startDate == data['startDate']) &
412 (Event.timeStart == data['timeStart'])).execute()))
414 sameEventListCopy = sameEventList.copy()
416 for event in sameEventListCopy:
417 if event.isCanceled or (event.seriesId and event.isRepeating):
418 sameEventList.remove(event)
420 try:
421 Term.get_by_id(data['term'])
422 except DoesNotExist as e:
423 return (False, f"Not a valid term: {data['term']}")
424 if sameEventList:
425 return (False, "This event already exists")
427 data['valid'] = True
428 return (True, "All inputs are valid.")
430def calculateNewSeriesId():
431 """
432 Gets the max series ID so that new seriesId can be assigned.
433 """
434 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
435 if maxSeriesId:
436 return maxSeriesId + 1
437 return 1
439def getPreviousSeriesEventData(seriesId):
440 """
441 Joins the User db table and Event Participant db table so that we can get the information of a participant if they attended an event.
443 """
444 previousEventVolunteers = (User.select(User).distinct()
445 .join(EventParticipant)
446 .join(Event)
447 .where(Event.seriesId==seriesId))
448 return previousEventVolunteers
450def getRepeatingEventsData(eventData):
451 """
452 Calculate the events to create based on a repeating event start and end date. Takes a
453 dictionary of event data.
455 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
457 Return a list of events to create from the event data.
458 """
460 return [ {'name': f"{eventData['name']} Week {counter+1}",
461 'date': eventData['startDate'] + timedelta(days=7*counter),
462 "week": counter+1,
463 'location': eventData['location']
464 }
465 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
467def preprocessEventData(eventData):
468 """
469 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
471 - dates should exist and be date objects if there is a value
472 - checkboxes should be True or False
473 - if term is given, convert it to a model object
474 - times should exist be strings in 24 hour format example: 14:40
475 - seriesData should be a JSON string
476 - Look up matching certification requirement if necessary
477 """
478 ## Process checkboxes
479 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'isLaborOnly']
481 for checkBox in eventCheckBoxes:
482 if checkBox not in eventData:
483 eventData[checkBox] = False
484 else:
485 eventData[checkBox] = bool(eventData[checkBox])
487 ## Process dates
488 eventDates = ['startDate', 'endDate']
489 for eventDate in eventDates:
490 if eventDate not in eventData: # There is no date given
491 eventData[eventDate] = ''
492 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
493 eventData[eventDate] = parser.parse(eventData[eventDate])
494 elif not isinstance(eventData[eventDate], date): # The date is not a date object
495 eventData[eventDate] = ''
497 # Process seriesData
498 if 'seriesData' not in eventData:
499 eventData['seriesData'] = json.dumps([])
501 # Process terms
502 if 'term' in eventData:
503 try:
504 eventData['term'] = Term.get_by_id(eventData['term'])
505 except DoesNotExist:
506 eventData['term'] = ''
508 # Process requirement
509 if 'certRequirement' in eventData:
510 try:
511 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
512 except DoesNotExist:
513 eventData['certRequirement'] = ''
514 elif 'id' in eventData:
515 # look up requirement
516 match = RequirementMatch.get_or_none(event=eventData['id'])
517 if match:
518 eventData['certRequirement'] = match.requirement
519 if 'timeStart' in eventData:
520 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
522 if 'timeEnd' in eventData:
523 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
525 return eventData
527def getTomorrowsEvents():
528 """Grabs each event that occurs tomorrow"""
529 tomorrowDate = date.today() + timedelta(days=1)
530 events = list(Event.select().where(Event.startDate==tomorrowDate))
531 return events
533def addEventView(viewer,event):
534 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
535 if not viewer.isCeltsAdmin:
536 EventView.get_or_create(user = viewer, event = event)
538def getEventRsvpCountsForTerm(term):
539 """
540 Get all of the RSVPs for the events that exist in the term.
541 Returns a dictionary with the event id as the key and the amount of
542 current RSVPs to that event as the pair.
543 """
544 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
545 .join(EventRsvp, JOIN.LEFT_OUTER)
546 .where(Event.term == term, Event.deletionDate == None)
547 .group_by(Event.id))
549 amountAsDict = {event.id: event.count for event in amount}
551 return amountAsDict
553def getEventRsvpCount(eventId):
554 """
555 Returns the number of RSVP'd participants for a given eventId.
556 """
557 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
559def getCountdownToEvent(event, *, currentDatetime=None):
560 """
561 Given an event, this function returns a string that conveys the amount of time left
562 until the start of the event.
564 Note about dates:
565 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
566 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
567 tomorrow with no mention of the hour.
568 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
569 and hours in actual time.
571 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
572 relative to this morning and exclude all hours and minutes
574 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
575 the real difference in days and hours without the aforementioned simplifying language.
576 """
578 if currentDatetime is None:
579 currentDatetime = datetime.now().replace(second=0, microsecond=0)
580 currentMorning = currentDatetime.replace(hour=0, minute=0)
582 eventStart = datetime.combine(event.startDate, event.timeStart)
583 eventEnd = datetime.combine(event.startDate, event.timeEnd)
585 if eventEnd < currentDatetime:
586 return "Already passed"
587 elif eventStart <= currentDatetime <= eventEnd:
588 return "Happening now"
590 timeUntilEvent = relativedelta(eventStart, currentDatetime)
591 calendarDelta = relativedelta(eventStart, currentMorning)
592 calendarYearsUntilEvent = calendarDelta.years
593 calendarMonthsUntilEvent = calendarDelta.months
594 calendarDaysUntilEvent = calendarDelta.days
596 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
597 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
598 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
599 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
600 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
602 # Years until
603 if calendarYearsUntilEvent:
604 if calendarMonthsUntilEvent:
605 return f"{yearString} and {monthString}"
606 return f"{yearString}"
607 # Months until
608 if calendarMonthsUntilEvent:
609 if calendarDaysUntilEvent:
610 return f"{monthString} and {dayString}"
611 return f"{monthString}"
612 # Days until
613 if calendarDaysUntilEvent:
614 if eventStart.time() < currentDatetime.time():
615 if calendarDaysUntilEvent == 1:
616 return "Tomorrow"
617 return f"{dayString}"
618 if timeUntilEvent.hours:
619 return f"{dayString} and {hourString}"
620 return f"{dayString}"
621 # Hours until
622 if timeUntilEvent.hours:
623 if timeUntilEvent.minutes:
624 return f"{hourString} and {minuteString}"
625 return f"{hourString}"
626 # Minutes until
627 elif timeUntilEvent.minutes > 1:
628 return f"{minuteString}"
629 # Seconds until
630 return "<1 minute"
632def copyRsvpToNewEvent(priorEvent, newEvent):
633 """
634 Copies rvsps from priorEvent to newEvent
635 """
636 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
638 for student in rsvpInfo:
639 newRsvp = EventRsvp(
640 user = student.user,
641 event = newEvent,
642 rsvpWaitlist = student.rsvpWaitlist
643 )
644 newRsvp.save()
645 numRsvps = len(rsvpInfo)
646 if numRsvps:
647 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
650def inviteCohortsToEvent(event, cohortYears):
651 """
652 Invites cohorts to a newly created event by associating the cohorts directly.
653 """
654 invitedCohorts = []
655 try:
656 for year in cohortYears:
657 year = int(year)
658 EventCohort.get_or_create(
659 event=event,
660 year=year,
661 defaults={'invited_at': datetime.now()}
662 )
664 addBonnerCohortToRsvpLog(year, event.id)
665 rsvpForBonnerCohort(year, event.id)
666 invitedCohorts.append(year)
668 if invitedCohorts:
669 cohortList = ', '.join(map(str, invitedCohorts))
670 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
672 return True, "Cohorts successfully added to new event", invitedCohorts
674 except Exception as e:
675 print(f"Error inviting cohorts to new event: {e}")
676 return False, f"Error adding cohorts to new event: {e}", []
678def updateEventCohorts(event, cohortYears):
679 """
680 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
681 """
682 invitedCohorts = []
683 try:
684 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
685 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
686 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
688 for year in yearsToAdd:
689 EventCohort.get_or_create(
690 event=event,
691 year=year,
692 defaults={'invited_at': datetime.now()}
693 )
695 addBonnerCohortToRsvpLog(year, event.id)
696 rsvpForBonnerCohort(year, event.id)
697 invitedCohorts.append(year)
699 if yearsToAdd:
700 cohortList = ', '.join(map(str, invitedCohorts))
701 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
703 return True, "Cohorts successfully updated for event", invitedCohorts
705 except Exception as e:
706 print(f"Error updating cohorts for event: {e}")
707 return False, f"Error updating cohorts for event: {e}", []