Coverage for app/logic/events.py: 93%
330 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-02-10 14:41 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-02-10 14:41 +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 'seriesId': seriesId,
133 'isRepeating': bool(isRepeating)
134 })
135 # Try to save each offering
136 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles)
137 if validationErrorMessage:
138 failedSavedOfferings.append((index, validationErrorMessage))
139 allSavesWereSuccessful = False
140 else:
141 savedEvent = savedEvents[0]
142 savedOfferings.append(savedEvent)
143 if not allSavesWereSuccessful:
144 savedOfferings = []
145 transaction.rollback()
147 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings
150def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
151 """
152 Tries to save an event to the database:
153 Checks that the event data is valid and if it is, it continues to save the new
154 event to the database and adds files if there are any.
155 If it is not valid it will return a validation error.
157 Returns:
158 The saved event, created events and an error message if an error occurred.
159 """
161 # Manually set the value of RSVP Limit if it is and empty string since it is
162 # automatically changed from "" to 0
163 if eventData["rsvpLimit"] == "":
164 eventData["rsvpLimit"] = None
166 newEventData = preprocessEventData(eventData)
168 isValid, validationErrorMessage = validateNewEventData(newEventData)
169 if not isValid:
170 return [], validationErrorMessage
172 events = saveEventToDb(newEventData, renewedEvent)
173 if attachmentFiles:
174 for event in events:
175 addFile = FileHandler(attachmentFiles, eventId=event.id)
176 addFile.saveFiles(saveOriginalFile=events[0])
177 return events, ""
180def saveEventToDb(newEventData, renewedEvent = False):
182 if not newEventData.get('valid', False) and not renewedEvent:
183 raise Exception("Unvalidated data passed to saveEventToDb")
185 isNewEvent = ('id' not in newEventData)
187 eventRecords = []
188 with mainDB.atomic():
190 eventData = {
191 "term": newEventData['term'],
192 "name": newEventData['name'],
193 "description": newEventData['description'],
194 "timeStart": newEventData['timeStart'],
195 "timeEnd": newEventData['timeEnd'],
196 "location": newEventData['location'],
197 "isFoodProvided" : newEventData['isFoodProvided'],
198 "isTraining": newEventData['isTraining'],
199 "isEngagement": newEventData['isEngagement'],
200 "isRsvpRequired": newEventData['isRsvpRequired'],
201 "isService": newEventData['isService'],
202 "startDate": newEventData['startDate'],
203 "rsvpLimit": newEventData['rsvpLimit'],
204 "contactEmail": newEventData['contactEmail'],
205 "contactName": newEventData['contactName']
206 }
208 # The three fields below are only relevant during event creation so we only set/change them when
209 # it is a new event.
210 if isNewEvent:
211 eventData['program'] = newEventData['program']
212 eventData['seriesId'] = newEventData.get('seriesId')
213 eventData['isRepeating'] = bool(newEventData.get('isRepeating'))
214 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
215 eventRecord = Event.create(**eventData)
216 else:
217 eventRecord = Event.get_by_id(newEventData['id'])
218 Event.update(**eventData).where(Event.id == eventRecord).execute()
220 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
221 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
223 eventRecords.append(eventRecord)
224 return eventRecords
226def getStudentLedEvents(term):
227 studentLedEvents = list(Event.select(Event, Program)
228 .join(Program)
229 .where(Program.isStudentLed,
230 Event.term == term, Event.deletionDate == None)
231 .order_by(Event.startDate, Event.timeStart)
232 .execute())
234 programs = {}
236 for event in studentLedEvents:
237 programs.setdefault(event.program, []).append(event)
239 return programs
241def getUpcomingStudentLedCount(term, currentTime):
242 """
243 Return a count of all upcoming events for each student led program.
244 """
246 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
247 .join(Event, on=(Program.id == Event.program_id))
248 .where(Program.isStudentLed,
249 Event.term == term, Event.deletionDate == None,
250 (Event.startDate > currentTime) | ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime)),
251 Event.isCanceled == False)
252 .group_by(Program.id))
254 programCountDict = {}
256 for programCount in upcomingCount:
257 programCountDict[programCount.id] = programCount.eventCount
258 return programCountDict
260def getTrainingEvents(term, user):
261 """
262 The allTrainingsEvent query is designed to select and count eventId's after grouping them
263 together by id's of similiar value. The query will then return the event that is associated
264 with the most programs (highest count) by doing this we can ensure that the event being
265 returned is the All Trainings Event.
266 term: expected to be the ID of a term
267 user: expected to be the current user
268 return: a list of all trainings the user can view
269 """
270 trainingQuery = (Event.select(Event).distinct()
271 .join(Program, JOIN.LEFT_OUTER)
272 .where(Event.isTraining == True,
273 Event.term == term, Event.deletionDate == None)
274 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
276 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
277 if hideBonner:
278 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
280 return list(trainingQuery.execute())
282def getBonnerEvents(term):
283 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id"))
284 .join(Program)
285 .where(Program.isBonnerScholars,
286 Event.term == term, Event.deletionDate == None)
287 .order_by(Event.startDate, Event.timeStart)
288 .execute())
289 return bonnerScholarsEvents
291def getOtherEvents(term):
292 """
294 Get the list of the events not caught by other functions to be displayed in
295 the Other Events section of the Events List page.
296 :return: A list of Other Event objects
297 """
298 # Gets all events that are not associated with a program and are not trainings
299 # Gets all events that have a program but don't fit anywhere
301 otherEvents = list(Event.select(Event, Program)
302 .join(Program, JOIN.LEFT_OUTER)
303 .where(Event.term == term, Event.deletionDate == None,
304 Event.isTraining == False,
305 Event.isAllVolunteerTraining == False,
306 ((Program.isOtherCeltsSponsored) |
307 ((Program.isStudentLed == False) &
308 (Program.isBonnerScholars == False))))
309 .order_by(Event.startDate, Event.timeStart, Event.id)
310 .execute())
312 return otherEvents
314def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
315 """
316 Get the list of upcoming events that the user is interested in as long
317 as they are not banned from the program that the event is a part of.
318 :param user: a username or User object
319 :param asOf: The date to use when determining future and past events.
320 Used in testing, defaults to the current timestamp.
321 :return: A list of Event objects
322 """
324 events = (Event.select().distinct()
325 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
326 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
327 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
328 .where(Event.deletionDate == None, Event.startDate >= asOf,
329 (Interest.user == user) | (EventRsvp.user == user),
330 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
332 if program:
333 events = events.where(Event.program == program)
335 events = events.order_by(Event.startDate, Event.timeStart)
337 eventsList = []
338 seriesEventsList = []
340 # removes all events in series except for the next upcoming one
341 for event in events:
342 if event.seriesId:
343 if not event.isCanceled:
344 if event.seriesId not in seriesEventsList:
345 eventsList.append(event)
346 seriesEventsList.append(event.seriesId)
347 else:
348 if not event.isCanceled:
349 eventsList.append(event)
351 return eventsList
353def getParticipatedEventsForUser(user):
354 """
355 Get all the events a user has participated in.
356 :param user: a username or User object
357 :param asOf: The date to use when determining future and past events.
358 Used in testing, defaults to the current timestamp.
359 :return: A list of Event objects
360 """
362 participatedEvents = (Event.select(Event, Program.programName)
363 .join(Program, JOIN.LEFT_OUTER).switch()
364 .join(EventParticipant)
365 .where(EventParticipant.user == user,
366 Event.isAllVolunteerTraining == False)
367 .order_by(Event.startDate, Event.name))
369 allVolunteer = (Event.select(Event, "")
370 .join(EventParticipant)
371 .where(Event.isAllVolunteerTraining == True,
372 EventParticipant.user == user))
373 union = participatedEvents.union_all(allVolunteer)
374 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())
376 return unionParticipationWithVolunteer
378def validateNewEventData(data):
379 """
380 Confirm that the provided data is valid for an event.
382 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
384 Returns 3 values: (boolean success, the validation error message, the data object)
385 """
387 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating']]:
388 return (False, "Raw form data passed to validate method. Preprocess first.")
390 if data['timeEnd'] <= data['timeStart']:
391 return (False, "Event end time must be after start time.")
393 # Validation if we are inserting a new event
394 if 'id' not in data:
396 sameEventList = list((Event.select().where((Event.name == data['name']) &
397 (Event.location == data['location']) &
398 (Event.startDate == data['startDate']) &
399 (Event.timeStart == data['timeStart'])).execute()))
401 sameEventListCopy = sameEventList.copy()
403 for event in sameEventListCopy:
404 if event.isCanceled or (event.seriesId and event.isRepeating):
405 sameEventList.remove(event)
407 try:
408 Term.get_by_id(data['term'])
409 except DoesNotExist as e:
410 return (False, f"Not a valid term: {data['term']}")
411 if sameEventList:
412 return (False, "This event already exists")
414 data['valid'] = True
415 return (True, "All inputs are valid.")
417def calculateNewSeriesId():
418 """
419 Gets the max series ID so that new seriesId can be assigned.
420 """
421 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
422 if maxSeriesId:
423 return maxSeriesId + 1
424 return 1
426def getPreviousSeriesEventData(seriesId):
427 """
428 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.
430 """
431 previousEventVolunteers = (User.select(User).distinct()
432 .join(EventParticipant)
433 .join(Event)
434 .where(Event.seriesId==seriesId))
435 return previousEventVolunteers
437def getRepeatingEventsData(eventData):
438 """
439 Calculate the events to create based on a repeating event start and end date. Takes a
440 dictionary of event data.
442 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
444 Return a list of events to create from the event data.
445 """
447 return [ {'name': f"{eventData['name']} Week {counter+1}",
448 'date': eventData['startDate'] + timedelta(days=7*counter),
449 "week": counter+1}
450 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
452def preprocessEventData(eventData):
453 """
454 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
456 - dates should exist and be date objects if there is a value
457 - checkboxes should be True or False
458 - if term is given, convert it to a model object
459 - times should exist be strings in 24 hour format example: 14:40
460 - seriesData should be a JSON string
461 - Look up matching certification requirement if necessary
462 """
463 ## Process checkboxes
464 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining']
466 for checkBox in eventCheckBoxes:
467 if checkBox not in eventData:
468 eventData[checkBox] = False
469 else:
470 eventData[checkBox] = bool(eventData[checkBox])
472 ## Process dates
473 eventDates = ['startDate', 'endDate']
474 for eventDate in eventDates:
475 if eventDate not in eventData: # There is no date given
476 eventData[eventDate] = ''
477 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
478 eventData[eventDate] = parser.parse(eventData[eventDate])
479 elif not isinstance(eventData[eventDate], date): # The date is not a date object
480 eventData[eventDate] = ''
482 # Process seriesData
483 if 'seriesData' not in eventData:
484 eventData['seriesData'] = json.dumps([])
486 # Process terms
487 if 'term' in eventData:
488 try:
489 eventData['term'] = Term.get_by_id(eventData['term'])
490 except DoesNotExist:
491 eventData['term'] = ''
493 # Process requirement
494 if 'certRequirement' in eventData:
495 try:
496 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
497 except DoesNotExist:
498 eventData['certRequirement'] = ''
499 elif 'id' in eventData:
500 # look up requirement
501 match = RequirementMatch.get_or_none(event=eventData['id'])
502 if match:
503 eventData['certRequirement'] = match.requirement
504 if 'timeStart' in eventData:
505 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
507 if 'timeEnd' in eventData:
508 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
510 return eventData
512def getTomorrowsEvents():
513 """Grabs each event that occurs tomorrow"""
514 tomorrowDate = date.today() + timedelta(days=1)
515 events = list(Event.select().where(Event.startDate==tomorrowDate))
516 return events
518def addEventView(viewer,event):
519 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
520 if not viewer.isCeltsAdmin:
521 EventView.get_or_create(user = viewer, event = event)
523def getEventRsvpCountsForTerm(term):
524 """
525 Get all of the RSVPs for the events that exist in the term.
526 Returns a dictionary with the event id as the key and the amount of
527 current RSVPs to that event as the pair.
528 """
529 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
530 .join(EventRsvp, JOIN.LEFT_OUTER)
531 .where(Event.term == term, Event.deletionDate == None)
532 .group_by(Event.id))
534 amountAsDict = {event.id: event.count for event in amount}
536 return amountAsDict
538def getEventRsvpCount(eventId):
539 """
540 Returns the number of RSVP'd participants for a given eventId.
541 """
542 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
544def getCountdownToEvent(event, *, currentDatetime=None):
545 """
546 Given an event, this function returns a string that conveys the amount of time left
547 until the start of the event.
549 Note about dates:
550 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
551 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
552 tomorrow with no mention of the hour.
553 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
554 and hours in actual time.
556 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
557 relative to this morning and exclude all hours and minutes
559 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
560 the real difference in days and hours without the aforementioned simplifying language.
561 """
563 if currentDatetime is None:
564 currentDatetime = datetime.now().replace(second=0, microsecond=0)
565 currentMorning = currentDatetime.replace(hour=0, minute=0)
567 eventStart = datetime.combine(event.startDate, event.timeStart)
568 eventEnd = datetime.combine(event.startDate, event.timeEnd)
570 if eventEnd < currentDatetime:
571 return "Already passed"
572 elif eventStart <= currentDatetime <= eventEnd:
573 return "Happening now"
575 timeUntilEvent = relativedelta(eventStart, currentDatetime)
576 calendarDelta = relativedelta(eventStart, currentMorning)
577 calendarYearsUntilEvent = calendarDelta.years
578 calendarMonthsUntilEvent = calendarDelta.months
579 calendarDaysUntilEvent = calendarDelta.days
581 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
582 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
583 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
584 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
585 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
587 # Years until
588 if calendarYearsUntilEvent:
589 if calendarMonthsUntilEvent:
590 return f"{yearString} and {monthString}"
591 return f"{yearString}"
592 # Months until
593 if calendarMonthsUntilEvent:
594 if calendarDaysUntilEvent:
595 return f"{monthString} and {dayString}"
596 return f"{monthString}"
597 # Days until
598 if calendarDaysUntilEvent:
599 if eventStart.time() < currentDatetime.time():
600 if calendarDaysUntilEvent == 1:
601 return "Tomorrow"
602 return f"{dayString}"
603 if timeUntilEvent.hours:
604 return f"{dayString} and {hourString}"
605 return f"{dayString}"
606 # Hours until
607 if timeUntilEvent.hours:
608 if timeUntilEvent.minutes:
609 return f"{hourString} and {minuteString}"
610 return f"{hourString}"
611 # Minutes until
612 elif timeUntilEvent.minutes > 1:
613 return f"{minuteString}"
614 # Seconds until
615 return "<1 minute"
617def copyRsvpToNewEvent(priorEvent, newEvent):
618 """
619 Copies rvsps from priorEvent to newEvent
620 """
621 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
623 for student in rsvpInfo:
624 newRsvp = EventRsvp(
625 user = student.user,
626 event = newEvent,
627 rsvpWaitlist = student.rsvpWaitlist
628 )
629 newRsvp.save()
630 numRsvps = len(rsvpInfo)
631 if numRsvps:
632 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
635def inviteCohortsToEvent(event, cohortYears):
636 """
637 Invites cohorts to a newly created event by associating the cohorts directly.
638 """
639 invitedCohorts = []
640 try:
641 for year in cohortYears:
642 year = int(year)
643 EventCohort.get_or_create(
644 event=event,
645 year=year,
646 defaults={'invited_at': datetime.now()}
647 )
649 addBonnerCohortToRsvpLog(year, event.id)
650 rsvpForBonnerCohort(year, event.id)
651 invitedCohorts.append(year)
653 if invitedCohorts:
654 cohortList = ', '.join(map(str, invitedCohorts))
655 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
657 return True, "Cohorts successfully added to new event", invitedCohorts
659 except Exception as e:
660 print(f"Error inviting cohorts to new event: {e}")
661 return False, f"Error adding cohorts to new event: {e}", []
663def updateEventCohorts(event, cohortYears):
664 """
665 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
666 """
667 invitedCohorts = []
668 try:
669 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
670 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
671 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
673 for year in yearsToAdd:
674 EventCohort.get_or_create(
675 event=event,
676 year=year,
677 defaults={'invited_at': datetime.now()}
678 )
680 addBonnerCohortToRsvpLog(year, event.id)
681 rsvpForBonnerCohort(year, event.id)
682 invitedCohorts.append(year)
684 if yearsToAdd:
685 cohortList = ', '.join(map(str, invitedCohorts))
686 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
688 return True, "Cohorts successfully updated for event", invitedCohorts
690 except Exception as e:
691 print(f"Error updating cohorts for event: {e}")
692 return False, f"Error updating cohorts for event: {e}", []