Coverage for app/logic/events.py: 93%
333 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-05-02 15:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-05-02 15:35 +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 getEngagementEvents(term):
242 engagementEvents = list(Event.select(Event, Program)
243 .join(Program)
244 .where(Event.isEngagement,
245 Event.term == term, Event.deletionDate == None)
246 .order_by(Event.startDate, Event.timeStart)
247 .execute())
248 return engagementEvents
250def getUpcomingStudentLedCount(term, currentTime):
251 """
252 Return a count of all upcoming events for each student led program.
253 """
255 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
256 .join(Event, on=(Program.id == Event.program_id))
257 .where(Program.isStudentLed,
258 Event.term == term, Event.deletionDate == None,
259 (Event.startDate > currentTime) | ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime)),
260 Event.isCanceled == False)
261 .group_by(Program.id))
263 programCountDict = {}
265 for programCount in upcomingCount:
266 programCountDict[programCount.id] = programCount.eventCount
267 return programCountDict
269def getTrainingEvents(term, user):
270 """
271 The allTrainingsEvent query is designed to select and count eventId's after grouping them
272 together by id's of similiar value. The query will then return the event that is associated
273 with the most programs (highest count) by doing this we can ensure that the event being
274 returned is the All Trainings Event.
275 term: expected to be the ID of a term
276 user: expected to be the current user
277 return: a list of all trainings the user can view
278 """
279 trainingQuery = (Event.select(Event).distinct()
280 .join(Program, JOIN.LEFT_OUTER)
281 .where(Event.isTraining == True,
282 Event.term == term, Event.deletionDate == None)
283 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
285 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
286 if hideBonner:
287 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
289 return list(trainingQuery.execute())
291def getBonnerEvents(term):
292 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id"))
293 .join(Program)
294 .where(Program.isBonnerScholars,
295 Event.term == term, Event.deletionDate == None)
296 .order_by(Event.startDate, Event.timeStart)
297 .execute())
298 return bonnerScholarsEvents
300def getOtherEvents(term):
301 """
303 Get the list of the events not caught by other functions to be displayed in
304 the Other Events section of the Events List page.
305 :return: A list of Other Event objects
306 """
307 # Gets all events that are not associated with a program and are not trainings
308 # Gets all events that have a program but don't fit anywhere
310 otherEvents = list(Event.select(Event, Program)
311 .join(Program, JOIN.LEFT_OUTER)
312 .where(Event.term == term, Event.deletionDate == None,
313 Event.isTraining == False,
314 Event.isAllVolunteerTraining == False,
315 ((Program.isOtherCeltsSponsored) |
316 ((Program.isStudentLed == False) &
317 (Program.isBonnerScholars == False))))
318 .order_by(Event.startDate, Event.timeStart, Event.id)
319 .execute())
321 return otherEvents
323def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
324 """
325 Get the list of upcoming events that the user is interested in as long
326 as they are not banned from the program that the event is a part of.
327 :param user: a username or User object
328 :param asOf: The date to use when determining future and past events.
329 Used in testing, defaults to the current timestamp.
330 :return: A list of Event objects
331 """
333 events = (Event.select().distinct()
334 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
335 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
336 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
337 .where(Event.deletionDate == None, Event.startDate >= asOf,
338 (Interest.user == user) | (EventRsvp.user == user),
339 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
341 if program:
342 events = events.where(Event.program == program)
344 events = events.order_by(Event.startDate, Event.timeStart)
346 eventsList = []
347 seriesEventsList = []
349 # removes all events in series except for the next upcoming one
350 for event in events:
351 if event.seriesId:
352 if not event.isCanceled:
353 if event.seriesId not in seriesEventsList:
354 eventsList.append(event)
355 seriesEventsList.append(event.seriesId)
356 else:
357 if not event.isCanceled:
358 eventsList.append(event)
360 return eventsList
362def getParticipatedEventsForUser(user):
363 """
364 Get all the events a user has participated in.
365 :param user: a username or User object
366 :param asOf: The date to use when determining future and past events.
367 Used in testing, defaults to the current timestamp.
368 :return: A list of Event objects
369 """
371 participatedEvents = (Event.select(Event, Program.programName)
372 .join(Program, JOIN.LEFT_OUTER).switch()
373 .join(EventParticipant)
374 .where(EventParticipant.user == user,
375 Event.isAllVolunteerTraining == False, Event.deletionDate == None)
376 .order_by(Event.startDate, Event.name))
378 allVolunteer = (Event.select(Event, "")
379 .join(EventParticipant)
380 .where(Event.isAllVolunteerTraining == True,
381 EventParticipant.user == user))
382 union = participatedEvents.union_all(allVolunteer)
383 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())
385 return unionParticipationWithVolunteer
387def validateNewEventData(data):
388 """
389 Confirm that the provided data is valid for an event.
391 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
393 Returns 3 values: (boolean success, the validation error message, the data object)
394 """
396 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating']]:
397 return (False, "Raw form data passed to validate method. Preprocess first.")
399 if data['timeEnd'] <= data['timeStart']:
400 return (False, "Event end time must be after start time.")
402 # Validation if we are inserting a new event
403 if 'id' not in data:
405 sameEventList = list((Event.select().where((Event.name == data['name']) &
406 (Event.location == data['location']) &
407 (Event.startDate == data['startDate']) &
408 (Event.timeStart == data['timeStart'])).execute()))
410 sameEventListCopy = sameEventList.copy()
412 for event in sameEventListCopy:
413 if event.isCanceled or (event.seriesId and event.isRepeating):
414 sameEventList.remove(event)
416 try:
417 Term.get_by_id(data['term'])
418 except DoesNotExist as e:
419 return (False, f"Not a valid term: {data['term']}")
420 if sameEventList:
421 return (False, "This event already exists")
423 data['valid'] = True
424 return (True, "All inputs are valid.")
426def calculateNewSeriesId():
427 """
428 Gets the max series ID so that new seriesId can be assigned.
429 """
430 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
431 if maxSeriesId:
432 return maxSeriesId + 1
433 return 1
435def getPreviousSeriesEventData(seriesId):
436 """
437 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.
439 """
440 previousEventVolunteers = (User.select(User).distinct()
441 .join(EventParticipant)
442 .join(Event)
443 .where(Event.seriesId==seriesId))
444 return previousEventVolunteers
446def getRepeatingEventsData(eventData):
447 """
448 Calculate the events to create based on a repeating event start and end date. Takes a
449 dictionary of event data.
451 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
453 Return a list of events to create from the event data.
454 """
456 return [ {'name': f"{eventData['name']} Week {counter+1}",
457 'date': eventData['startDate'] + timedelta(days=7*counter),
458 "week": counter+1}
459 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
461def preprocessEventData(eventData):
462 """
463 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
465 - dates should exist and be date objects if there is a value
466 - checkboxes should be True or False
467 - if term is given, convert it to a model object
468 - times should exist be strings in 24 hour format example: 14:40
469 - seriesData should be a JSON string
470 - Look up matching certification requirement if necessary
471 """
472 ## Process checkboxes
473 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining']
475 for checkBox in eventCheckBoxes:
476 if checkBox not in eventData:
477 eventData[checkBox] = False
478 else:
479 eventData[checkBox] = bool(eventData[checkBox])
481 ## Process dates
482 eventDates = ['startDate', 'endDate']
483 for eventDate in eventDates:
484 if eventDate not in eventData: # There is no date given
485 eventData[eventDate] = ''
486 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
487 eventData[eventDate] = parser.parse(eventData[eventDate])
488 elif not isinstance(eventData[eventDate], date): # The date is not a date object
489 eventData[eventDate] = ''
491 # Process seriesData
492 if 'seriesData' not in eventData:
493 eventData['seriesData'] = json.dumps([])
495 # Process terms
496 if 'term' in eventData:
497 try:
498 eventData['term'] = Term.get_by_id(eventData['term'])
499 except DoesNotExist:
500 eventData['term'] = ''
502 # Process requirement
503 if 'certRequirement' in eventData:
504 try:
505 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
506 except DoesNotExist:
507 eventData['certRequirement'] = ''
508 elif 'id' in eventData:
509 # look up requirement
510 match = RequirementMatch.get_or_none(event=eventData['id'])
511 if match:
512 eventData['certRequirement'] = match.requirement
513 if 'timeStart' in eventData:
514 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
516 if 'timeEnd' in eventData:
517 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
519 return eventData
521def getTomorrowsEvents():
522 """Grabs each event that occurs tomorrow"""
523 tomorrowDate = date.today() + timedelta(days=1)
524 events = list(Event.select().where(Event.startDate==tomorrowDate))
525 return events
527def addEventView(viewer,event):
528 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
529 if not viewer.isCeltsAdmin:
530 EventView.get_or_create(user = viewer, event = event)
532def getEventRsvpCountsForTerm(term):
533 """
534 Get all of the RSVPs for the events that exist in the term.
535 Returns a dictionary with the event id as the key and the amount of
536 current RSVPs to that event as the pair.
537 """
538 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
539 .join(EventRsvp, JOIN.LEFT_OUTER)
540 .where(Event.term == term, Event.deletionDate == None)
541 .group_by(Event.id))
543 amountAsDict = {event.id: event.count for event in amount}
545 return amountAsDict
547def getEventRsvpCount(eventId):
548 """
549 Returns the number of RSVP'd participants for a given eventId.
550 """
551 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
553def getCountdownToEvent(event, *, currentDatetime=None):
554 """
555 Given an event, this function returns a string that conveys the amount of time left
556 until the start of the event.
558 Note about dates:
559 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
560 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
561 tomorrow with no mention of the hour.
562 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
563 and hours in actual time.
565 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
566 relative to this morning and exclude all hours and minutes
568 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
569 the real difference in days and hours without the aforementioned simplifying language.
570 """
572 if currentDatetime is None:
573 currentDatetime = datetime.now().replace(second=0, microsecond=0)
574 currentMorning = currentDatetime.replace(hour=0, minute=0)
576 eventStart = datetime.combine(event.startDate, event.timeStart)
577 eventEnd = datetime.combine(event.startDate, event.timeEnd)
579 if eventEnd < currentDatetime:
580 return "Already passed"
581 elif eventStart <= currentDatetime <= eventEnd:
582 return "Happening now"
584 timeUntilEvent = relativedelta(eventStart, currentDatetime)
585 calendarDelta = relativedelta(eventStart, currentMorning)
586 calendarYearsUntilEvent = calendarDelta.years
587 calendarMonthsUntilEvent = calendarDelta.months
588 calendarDaysUntilEvent = calendarDelta.days
590 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
591 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
592 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
593 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
594 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
596 # Years until
597 if calendarYearsUntilEvent:
598 if calendarMonthsUntilEvent:
599 return f"{yearString} and {monthString}"
600 return f"{yearString}"
601 # Months until
602 if calendarMonthsUntilEvent:
603 if calendarDaysUntilEvent:
604 return f"{monthString} and {dayString}"
605 return f"{monthString}"
606 # Days until
607 if calendarDaysUntilEvent:
608 if eventStart.time() < currentDatetime.time():
609 if calendarDaysUntilEvent == 1:
610 return "Tomorrow"
611 return f"{dayString}"
612 if timeUntilEvent.hours:
613 return f"{dayString} and {hourString}"
614 return f"{dayString}"
615 # Hours until
616 if timeUntilEvent.hours:
617 if timeUntilEvent.minutes:
618 return f"{hourString} and {minuteString}"
619 return f"{hourString}"
620 # Minutes until
621 elif timeUntilEvent.minutes > 1:
622 return f"{minuteString}"
623 # Seconds until
624 return "<1 minute"
626def copyRsvpToNewEvent(priorEvent, newEvent):
627 """
628 Copies rvsps from priorEvent to newEvent
629 """
630 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
632 for student in rsvpInfo:
633 newRsvp = EventRsvp(
634 user = student.user,
635 event = newEvent,
636 rsvpWaitlist = student.rsvpWaitlist
637 )
638 newRsvp.save()
639 numRsvps = len(rsvpInfo)
640 if numRsvps:
641 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
644def inviteCohortsToEvent(event, cohortYears):
645 """
646 Invites cohorts to a newly created event by associating the cohorts directly.
647 """
648 invitedCohorts = []
649 try:
650 for year in cohortYears:
651 year = int(year)
652 EventCohort.get_or_create(
653 event=event,
654 year=year,
655 defaults={'invited_at': datetime.now()}
656 )
658 addBonnerCohortToRsvpLog(year, event.id)
659 rsvpForBonnerCohort(year, event.id)
660 invitedCohorts.append(year)
662 if invitedCohorts:
663 cohortList = ', '.join(map(str, invitedCohorts))
664 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
666 return True, "Cohorts successfully added to new event", invitedCohorts
668 except Exception as e:
669 print(f"Error inviting cohorts to new event: {e}")
670 return False, f"Error adding cohorts to new event: {e}", []
672def updateEventCohorts(event, cohortYears):
673 """
674 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
675 """
676 invitedCohorts = []
677 try:
678 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
679 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
680 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
682 for year in yearsToAdd:
683 EventCohort.get_or_create(
684 event=event,
685 year=year,
686 defaults={'invited_at': datetime.now()}
687 )
689 addBonnerCohortToRsvpLog(year, event.id)
690 rsvpForBonnerCohort(year, event.id)
691 invitedCohorts.append(year)
693 if yearsToAdd:
694 cohortList = ', '.join(map(str, invitedCohorts))
695 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
697 return True, "Cohorts successfully updated for event", invitedCohorts
699 except Exception as e:
700 print(f"Error updating cohorts for event: {e}")
701 return False, f"Error updating cohorts for event: {e}", []