Coverage for app/logic/events.py: 93%
333 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-18 20:14 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-18 20:14 +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 "isLaborOnly" : newEventData['isLaborOnly'],
199 "isTraining": newEventData['isTraining'],
200 "isEngagement": newEventData['isEngagement'],
201 "isRsvpRequired": newEventData['isRsvpRequired'],
202 "isService": newEventData['isService'],
203 "startDate": newEventData['startDate'],
204 "rsvpLimit": newEventData['rsvpLimit'],
205 "contactEmail": newEventData['contactEmail'],
206 "contactName": newEventData['contactName'],
207 }
209 # The three fields below are only relevant during event creation so we only set/change them when
210 # it is a new event.
211 if isNewEvent:
212 eventData['program'] = newEventData['program']
213 eventData['seriesId'] = newEventData.get('seriesId')
214 eventData['isRepeating'] = bool(newEventData.get('isRepeating'))
215 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
216 eventRecord = Event.create(**eventData)
217 else:
218 eventRecord = Event.get_by_id(newEventData['id'])
219 Event.update(**eventData).where(Event.id == eventRecord).execute()
221 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
222 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
224 eventRecords.append(eventRecord)
225 return eventRecords
227def getVolunteerOpportunities(term):
228 volunteerOpportunities = list(Event.select(Event, Program)
229 .join(Program)
230 .where((Event.term == term) &
231 (Event.deletionDate.is_null(True)) &
232 (Event.isService == True) &
233 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True))
234 )
235 .order_by(Event.startDate, Event.timeStart)
236 .execute())
238 programs = {}
240 for event in volunteerOpportunities:
241 programs.setdefault(event.program, []).append(event)
243 return programs
245def getEngagementEvents(term):
246 engagementEvents = list(Event.select(Event, Program)
247 .join(Program)
248 .where(Event.isEngagement, Event.isLaborOnly == False,
249 Event.term == term, Event.deletionDate == None)
250 .order_by(Event.startDate, Event.timeStart)
251 .execute())
252 return engagementEvents
254def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
255 """
256 Return a count of all upcoming events for each volunteer opportunitiesprogram.
257 """
259 upcomingCount = (
260 Program
261 .select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
262 .join(Event, on=(Program.id == Event.program_id))
263 .where(
264 (Event.term == term) &
265 (Event.deletionDate.is_null(True)) &
266 (Event.isService == True) &
267 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
268 ((Event.startDate > currentTime) |
269 ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime))) &
270 (Event.isCanceled == False)
271 )
272 .group_by(Program.id)
273 )
275 programCountDict = {}
276 for programCount in upcomingCount:
277 programCountDict[programCount.id] = programCount.eventCount
278 return programCountDict
280def getTrainingEvents(term, user):
281 """
282 The allTrainingsEvent query is designed to select and count eventId's after grouping them
283 together by id's of similiar value. The query will then return the event that is associated
284 with the most programs (highest count) by doing this we can ensure that the event being
285 returned is the All Trainings Event.
286 term: expected to be the ID of a term
287 user: expected to be the current user
288 return: a list of all trainings the user can view
289 """
290 trainingQuery = (Event.select(Event).distinct()
291 .join(Program, JOIN.LEFT_OUTER)
292 .where(Event.isTraining == True, Event.isLaborOnly == False,
293 Event.term == term, Event.deletionDate == None)
294 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
296 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
297 if hideBonner:
298 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
300 return list(trainingQuery.execute())
302def getBonnerEvents(term):
303 bonnerScholarsEvents = list(
304 Event.select(Event, Program.id.alias("program_id"))
305 .join(Program)
306 .where(
307 Program.isBonnerScholars,
308 Event.term == term,
309 Event.deletionDate == None
310 )
311 .order_by(Event.startDate, Event.timeStart)
312 .execute()
313 )
314 return bonnerScholarsEvents
316def getCeltsLabor(term):
317 """
318 Labor tab: events explicitly marked as Labor Only.
319 """
320 celtsLabor = list(Event.select()
321 .where(Event.term == term, Event.deletionDate == None, Event.isLaborOnly == True)
322 .order_by(Event.startDate, Event.timeStart, Event.id)
323 .execute())
324 return celtsLabor
326def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
327 """
328 Get the list of upcoming events that the user is interested in as long
329 as they are not banned from the program that the event is a part of.
330 :param user: a username or User object
331 :param asOf: The date to use when determining future and past events.
332 Used in testing, defaults to the current timestamp.
333 :return: A list of Event objects
334 """
336 events = (Event.select().distinct()
337 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
338 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
339 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
340 .where(Event.deletionDate == None, Event.startDate >= asOf,
341 (Interest.user == user) | (EventRsvp.user == user),
342 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
344 if program:
345 events = events.where(Event.program == program)
347 events = events.order_by(Event.startDate, Event.timeStart)
349 eventsList = []
350 seriesEventsList = []
352 # removes all events in series except for the next upcoming one
353 for event in events:
354 if event.seriesId:
355 if not event.isCanceled:
356 if event.seriesId not in seriesEventsList:
357 eventsList.append(event)
358 seriesEventsList.append(event.seriesId)
359 else:
360 if not event.isCanceled:
361 eventsList.append(event)
363 return eventsList
365def getParticipatedEventsForUser(user):
366 """
367 Get all the events a user has participated in.
368 :param user: a username or User object
369 :param asOf: The date to use when determining future and past events.
370 Used in testing, defaults to the current timestamp.
371 :return: A list of Event objects
372 """
374 participatedEvents = (Event.select(Event, Program.programName)
375 .join(Program, JOIN.LEFT_OUTER).switch()
376 .join(EventParticipant)
377 .where(EventParticipant.user == user,
378 Event.isAllVolunteerTraining == False, Event.deletionDate == None)
379 .order_by(Event.startDate, Event.name))
381 allVolunteer = (Event.select(Event, "")
382 .join(EventParticipant)
383 .where(Event.isAllVolunteerTraining == True,
384 EventParticipant.user == user))
385 union = participatedEvents.union_all(allVolunteer)
386 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())
388 return unionParticipationWithVolunteer
390def validateNewEventData(data):
391 """
392 Confirm that the provided data is valid for an event.
394 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
396 Returns 3 values: (boolean success, the validation error message, the data object)
397 """
399 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['isLaborOnly']]:
400 return (False, "Raw form data passed to validate method. Preprocess first.")
402 if data['timeEnd'] <= data['timeStart']:
403 return (False, "Event end time must be after start time.")
405 # Validation if we are inserting a new event
406 if 'id' not in data:
408 sameEventList = list((Event.select().where((Event.name == data['name']) &
409 (Event.location == data['location']) &
410 (Event.startDate == data['startDate']) &
411 (Event.timeStart == data['timeStart'])).execute()))
413 sameEventListCopy = sameEventList.copy()
415 for event in sameEventListCopy:
416 if event.isCanceled or (event.seriesId and event.isRepeating):
417 sameEventList.remove(event)
419 try:
420 Term.get_by_id(data['term'])
421 except DoesNotExist as e:
422 return (False, f"Not a valid term: {data['term']}")
423 if sameEventList:
424 return (False, "This event already exists")
426 data['valid'] = True
427 return (True, "All inputs are valid.")
429def calculateNewSeriesId():
430 """
431 Gets the max series ID so that new seriesId can be assigned.
432 """
433 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
434 if maxSeriesId:
435 return maxSeriesId + 1
436 return 1
438def getPreviousSeriesEventData(seriesId):
439 """
440 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.
442 """
443 previousEventVolunteers = (User.select(User).distinct()
444 .join(EventParticipant)
445 .join(Event)
446 .where(Event.seriesId==seriesId))
447 return previousEventVolunteers
449def getRepeatingEventsData(eventData):
450 """
451 Calculate the events to create based on a repeating event start and end date. Takes a
452 dictionary of event data.
454 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
456 Return a list of events to create from the event data.
457 """
459 return [ {'name': f"{eventData['name']} Week {counter+1}",
460 'date': eventData['startDate'] + timedelta(days=7*counter),
461 "week": counter+1}
462 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
464def preprocessEventData(eventData):
465 """
466 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
468 - dates should exist and be date objects if there is a value
469 - checkboxes should be True or False
470 - if term is given, convert it to a model object
471 - times should exist be strings in 24 hour format example: 14:40
472 - seriesData should be a JSON string
473 - Look up matching certification requirement if necessary
474 """
475 ## Process checkboxes
476 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'isLaborOnly']
478 for checkBox in eventCheckBoxes:
479 if checkBox not in eventData:
480 eventData[checkBox] = False
481 else:
482 eventData[checkBox] = bool(eventData[checkBox])
484 ## Process dates
485 eventDates = ['startDate', 'endDate']
486 for eventDate in eventDates:
487 if eventDate not in eventData: # There is no date given
488 eventData[eventDate] = ''
489 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
490 eventData[eventDate] = parser.parse(eventData[eventDate])
491 elif not isinstance(eventData[eventDate], date): # The date is not a date object
492 eventData[eventDate] = ''
494 # Process seriesData
495 if 'seriesData' not in eventData:
496 eventData['seriesData'] = json.dumps([])
498 # Process terms
499 if 'term' in eventData:
500 try:
501 eventData['term'] = Term.get_by_id(eventData['term'])
502 except DoesNotExist:
503 eventData['term'] = ''
505 # Process requirement
506 if 'certRequirement' in eventData:
507 try:
508 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
509 except DoesNotExist:
510 eventData['certRequirement'] = ''
511 elif 'id' in eventData:
512 # look up requirement
513 match = RequirementMatch.get_or_none(event=eventData['id'])
514 if match:
515 eventData['certRequirement'] = match.requirement
516 if 'timeStart' in eventData:
517 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
519 if 'timeEnd' in eventData:
520 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
522 return eventData
524def getTomorrowsEvents():
525 """Grabs each event that occurs tomorrow"""
526 tomorrowDate = date.today() + timedelta(days=1)
527 events = list(Event.select().where(Event.startDate==tomorrowDate))
528 return events
530def addEventView(viewer,event):
531 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
532 if not viewer.isCeltsAdmin:
533 EventView.get_or_create(user = viewer, event = event)
535def getEventRsvpCountsForTerm(term):
536 """
537 Get all of the RSVPs for the events that exist in the term.
538 Returns a dictionary with the event id as the key and the amount of
539 current RSVPs to that event as the pair.
540 """
541 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
542 .join(EventRsvp, JOIN.LEFT_OUTER)
543 .where(Event.term == term, Event.deletionDate == None)
544 .group_by(Event.id))
546 amountAsDict = {event.id: event.count for event in amount}
548 return amountAsDict
550def getEventRsvpCount(eventId):
551 """
552 Returns the number of RSVP'd participants for a given eventId.
553 """
554 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
556def getCountdownToEvent(event, *, currentDatetime=None):
557 """
558 Given an event, this function returns a string that conveys the amount of time left
559 until the start of the event.
561 Note about dates:
562 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
563 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
564 tomorrow with no mention of the hour.
565 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
566 and hours in actual time.
568 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
569 relative to this morning and exclude all hours and minutes
571 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
572 the real difference in days and hours without the aforementioned simplifying language.
573 """
575 if currentDatetime is None:
576 currentDatetime = datetime.now().replace(second=0, microsecond=0)
577 currentMorning = currentDatetime.replace(hour=0, minute=0)
579 eventStart = datetime.combine(event.startDate, event.timeStart)
580 eventEnd = datetime.combine(event.startDate, event.timeEnd)
582 if eventEnd < currentDatetime:
583 return "Already passed"
584 elif eventStart <= currentDatetime <= eventEnd:
585 return "Happening now"
587 timeUntilEvent = relativedelta(eventStart, currentDatetime)
588 calendarDelta = relativedelta(eventStart, currentMorning)
589 calendarYearsUntilEvent = calendarDelta.years
590 calendarMonthsUntilEvent = calendarDelta.months
591 calendarDaysUntilEvent = calendarDelta.days
593 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
594 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
595 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
596 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
597 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
599 # Years until
600 if calendarYearsUntilEvent:
601 if calendarMonthsUntilEvent:
602 return f"{yearString} and {monthString}"
603 return f"{yearString}"
604 # Months until
605 if calendarMonthsUntilEvent:
606 if calendarDaysUntilEvent:
607 return f"{monthString} and {dayString}"
608 return f"{monthString}"
609 # Days until
610 if calendarDaysUntilEvent:
611 if eventStart.time() < currentDatetime.time():
612 if calendarDaysUntilEvent == 1:
613 return "Tomorrow"
614 return f"{dayString}"
615 if timeUntilEvent.hours:
616 return f"{dayString} and {hourString}"
617 return f"{dayString}"
618 # Hours until
619 if timeUntilEvent.hours:
620 if timeUntilEvent.minutes:
621 return f"{hourString} and {minuteString}"
622 return f"{hourString}"
623 # Minutes until
624 elif timeUntilEvent.minutes > 1:
625 return f"{minuteString}"
626 # Seconds until
627 return "<1 minute"
629def copyRsvpToNewEvent(priorEvent, newEvent):
630 """
631 Copies rvsps from priorEvent to newEvent
632 """
633 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
635 for student in rsvpInfo:
636 newRsvp = EventRsvp(
637 user = student.user,
638 event = newEvent,
639 rsvpWaitlist = student.rsvpWaitlist
640 )
641 newRsvp.save()
642 numRsvps = len(rsvpInfo)
643 if numRsvps:
644 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
647def inviteCohortsToEvent(event, cohortYears):
648 """
649 Invites cohorts to a newly created event by associating the cohorts directly.
650 """
651 invitedCohorts = []
652 try:
653 for year in cohortYears:
654 year = int(year)
655 EventCohort.get_or_create(
656 event=event,
657 year=year,
658 defaults={'invited_at': datetime.now()}
659 )
661 addBonnerCohortToRsvpLog(year, event.id)
662 rsvpForBonnerCohort(year, event.id)
663 invitedCohorts.append(year)
665 if invitedCohorts:
666 cohortList = ', '.join(map(str, invitedCohorts))
667 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
669 return True, "Cohorts successfully added to new event", invitedCohorts
671 except Exception as e:
672 print(f"Error inviting cohorts to new event: {e}")
673 return False, f"Error adding cohorts to new event: {e}", []
675def updateEventCohorts(event, cohortYears):
676 """
677 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
678 """
679 invitedCohorts = []
680 try:
681 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
682 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
683 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
685 for year in yearsToAdd:
686 EventCohort.get_or_create(
687 event=event,
688 year=year,
689 defaults={'invited_at': datetime.now()}
690 )
692 addBonnerCohortToRsvpLog(year, event.id)
693 rsvpForBonnerCohort(year, event.id)
694 invitedCohorts.append(year)
696 if yearsToAdd:
697 cohortList = ', '.join(map(str, invitedCohorts))
698 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
700 return True, "Cohorts successfully updated for event", invitedCohorts
702 except Exception as e:
703 print(f"Error updating cohorts for event: {e}")
704 return False, f"Error updating cohorts for event: {e}", []