Coverage for app/logic/events.py: 93%
335 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-03-12 20:44 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-03-12 20:44 +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 for each event in the series, inheriting from the original eventData
123 seriesData = sorted(eventData.get('seriesData'), key=lambda x: datetime.strptime(f"{x['eventDate']} {x['startTime']}",'%Y-%m-%d %H:%M'))
124 isRepeating = bool(eventData.get('isRepeating'))
125 with mainDB.atomic() as transaction:
126 for index, event in enumerate(seriesData):
127 eventInfo = eventData.copy()
128 eventInfo.update({
129 'name': event['eventName'],
130 'startDate': event['eventDate'],
131 'timeStart': event['startTime'],
132 'timeEnd': event['endTime'],
133 'location': event['eventLocation'],
134 'seriesId': seriesId,
135 'isRepeating': bool(isRepeating),
136 })
137 # Try to save each offering
138 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles)
139 if validationErrorMessage:
140 failedSavedOfferings.append((index, validationErrorMessage))
141 allSavesWereSuccessful = False
142 else:
143 savedEvent = savedEvents[0]
144 savedOfferings.append(savedEvent)
145 if not allSavesWereSuccessful:
146 savedOfferings = []
147 transaction.rollback()
149 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings
152def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
153 """
154 Tries to save an event to the database:
155 Checks that the event data is valid and if it is, it continues to save the new
156 event to the database and adds files if there are any.
157 If it is not valid it will return a validation error.
159 Returns:
160 The saved event, created events and an error message if an error occurred.
161 """
163 # Manually set the value of RSVP Limit if it is and empty string since it is
164 # automatically changed from "" to 0
165 if eventData["rsvpLimit"] == "":
166 eventData["rsvpLimit"] = None
168 newEventData = preprocessEventData(eventData)
170 isValid, validationErrorMessage = validateNewEventData(newEventData)
171 if not isValid:
172 return [], validationErrorMessage
174 events = saveEventToDb(newEventData, renewedEvent)
175 if attachmentFiles:
176 for event in events:
177 addFile = FileHandler(attachmentFiles, eventId=event.id)
178 addFile.saveFiles(saveOriginalFile=events[0])
179 return events, ""
182def saveEventToDb(newEventData, renewedEvent = False):
184 if not newEventData.get('valid', False) and not renewedEvent:
185 raise Exception("Unvalidated data passed to saveEventToDb")
187 isNewEvent = ('id' not in newEventData)
189 eventRecords = []
190 with mainDB.atomic():
192 eventData = {
193 "term": newEventData['term'],
194 "name": newEventData['name'],
195 "description": newEventData['description'],
196 "timeStart": newEventData['timeStart'],
197 "timeEnd": newEventData['timeEnd'],
198 "location": newEventData['location'],
199 "isFoodProvided" : newEventData['isFoodProvided'],
200 "isLaborOnly" : newEventData['isLaborOnly'],
201 "isTraining": newEventData['isTraining'],
202 "isEngagement": newEventData['isEngagement'],
203 "isRsvpRequired": newEventData['isRsvpRequired'],
204 "isService": newEventData['isService'],
205 "startDate": newEventData['startDate'],
206 "rsvpLimit": newEventData['rsvpLimit'],
207 "contactEmail": newEventData['contactEmail'],
208 "contactName": newEventData['contactName'],
209 }
211 # The three fields below are only relevant during event creation so we only set/change them when
212 # it is a new event.
213 if isNewEvent:
214 eventData['program'] = newEventData['program']
215 eventData['seriesId'] = newEventData.get('seriesId')
216 eventData['isRepeating'] = bool(newEventData.get('isRepeating'))
217 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
218 eventRecord = Event.create(**eventData)
219 else:
220 eventRecord = Event.get_by_id(newEventData['id'])
221 Event.update(**eventData).where(Event.id == eventRecord).execute()
223 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
224 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
226 eventRecords.append(eventRecord)
227 return eventRecords
229def getVolunteerOpportunities(term):
230 volunteerOpportunities = list(Event.select(Event, Program)
231 .join(Program)
232 .where((Event.term == term) &
233 (Event.deletionDate.is_null(True)) &
234 (Event.isService == True) &
235 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True))
236 )
237 .order_by(Event.startDate, Event.timeStart)
238 .execute())
240 programs = {}
242 for event in volunteerOpportunities:
243 programs.setdefault(event.program, []).append(event)
245 return programs
247def getEngagementEvents(term):
248 engagementEvents = list(Event.select(Event, Program)
249 .join(Program)
250 .where(Event.isEngagement, Event.isLaborOnly == False,
251 Event.term == term, Event.deletionDate == None)
252 .order_by(Event.startDate, Event.timeStart)
253 .execute())
254 return engagementEvents
256def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
257 """
258 Return a count of all upcoming events for each volunteer opportunitiesprogram.
259 """
261 upcomingCount = (
262 Program
263 .select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
264 .join(Event, on=(Program.id == Event.program_id))
265 .where(
266 (Event.term == term) &
267 (Event.deletionDate.is_null(True)) &
268 (Event.isService == True) &
269 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
270 ((Event.startDate > currentTime) |
271 ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime))) &
272 (Event.isCanceled == False)
273 )
274 .group_by(Program.id)
275 )
277 programCountDict = {}
278 for programCount in upcomingCount:
279 programCountDict[programCount.id] = programCount.eventCount
280 return programCountDict
282def getTrainingEvents(term, user):
283 """
284 The allTrainingsEvent query is designed to select and count eventId's after grouping them
285 together by id's of similiar value. The query will then return the event that is associated
286 with the most programs (highest count) by doing this we can ensure that the event being
287 returned is the All Trainings Event.
288 term: expected to be the ID of a term
289 user: expected to be the current user
290 return: a list of all trainings the user can view
291 """
292 trainingQuery = (Event.select(Event).distinct()
293 .join(Program, JOIN.LEFT_OUTER)
294 .where(Event.isTraining == True, Event.isLaborOnly == False,
295 Event.term == term, Event.deletionDate == None)
296 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
298 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
299 if hideBonner:
300 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
302 return list(trainingQuery.execute())
304def getBonnerEvents(term):
305 bonnerScholarsEvents = list(
306 Event.select(Event, Program.id.alias("program_id"))
307 .join(Program)
308 .where(
309 Program.isBonnerScholars,
310 Event.term == term,
311 Event.deletionDate == None
312 )
313 .order_by(Event.startDate, Event.timeStart)
314 .execute()
315 )
316 return bonnerScholarsEvents
318def getCeltsLabor(term):
319 """
320 Labor tab: events explicitly marked as Labor Only.
321 """
322 celtsLabor = list(Event.select()
323 .where(Event.term == term, Event.deletionDate == None, Event.isLaborOnly == True)
324 .order_by(Event.startDate, Event.timeStart, Event.id)
325 .execute())
326 return celtsLabor
328def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
329 """
330 Get the list of upcoming events that the user is interested in as long
331 as they are not banned from the program that the event is a part of.
332 :param user: a username or User object
333 :param asOf: The date to use when determining future and past events.
334 Used in testing, defaults to the current timestamp.
335 :return: A list of Event objects
336 """
338 events = (Event.select().distinct()
339 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
340 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
341 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
342 .where(Event.deletionDate == None, Event.startDate >= asOf,
343 (Interest.user == user) | (EventRsvp.user == user),
344 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
346 if program:
347 events = events.where(Event.program == program)
349 events = events.order_by(Event.startDate, Event.timeStart)
351 eventsList = []
352 seriesEventsList = []
354 # removes all events in series except for the next upcoming one
355 for event in events:
356 if event.seriesId:
357 if not event.isCanceled:
358 if event.seriesId not in seriesEventsList:
359 eventsList.append(event)
360 seriesEventsList.append(event.seriesId)
361 else:
362 if not event.isCanceled:
363 eventsList.append(event)
365 return eventsList
367def getParticipatedEventsForUser(user):
368 """
369 Get all the events a user has participated in.
370 :param user: a username or User object
371 :param asOf: The date to use when determining future and past events.
372 Used in testing, defaults to the current timestamp.
373 :return: A list of Event objects
374 """
376 eventName = fn.LOWER(Event.name)
377 checkIfLaborMeeting = eventName.contains("labor meeting")
379 participatedEvents = (Event.select(Event, Program.programName)
380 .join(Program, JOIN.LEFT_OUTER).switch()
381 .join(EventParticipant)
382 .where(EventParticipant.user == user,
383 Event.isAllVolunteerTraining == False, Event.deletionDate == None, ~checkIfLaborMeeting)
384 .order_by(Event.startDate, Event.name))
386 allVolunteer = (Event.select(Event, "")
387 .join(EventParticipant)
388 .where(Event.isAllVolunteerTraining == True,
389 EventParticipant.user == user))
390 union = participatedEvents.union_all(allVolunteer)
391 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())
393 return unionParticipationWithVolunteer
395def validateNewEventData(data):
396 """
397 Confirm that the provided data is valid for an event.
399 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
401 Returns 3 values: (boolean success, the validation error message, the data object)
402 """
404 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['isLaborOnly']]:
405 return (False, "Raw form data passed to validate method. Preprocess first.")
407 if data['timeEnd'] <= data['timeStart']:
408 return (False, "Event end time must be after start time.")
410 # Validation if we are inserting a new event
411 if 'id' not in data:
413 sameEventList = list((Event.select().where((Event.name == data['name']) &
414 (Event.location == data['location']) &
415 (Event.startDate == data['startDate']) &
416 (Event.timeStart == data['timeStart'])).execute()))
418 sameEventListCopy = sameEventList.copy()
420 for event in sameEventListCopy:
421 if event.isCanceled or (event.seriesId and event.isRepeating):
422 sameEventList.remove(event)
424 try:
425 Term.get_by_id(data['term'])
426 except DoesNotExist as e:
427 return (False, f"Not a valid term: {data['term']}")
428 if sameEventList:
429 return (False, "This event already exists")
431 data['valid'] = True
432 return (True, "All inputs are valid.")
434def calculateNewSeriesId():
435 """
436 Gets the max series ID so that new seriesId can be assigned.
437 """
438 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
439 if maxSeriesId:
440 return maxSeriesId + 1
441 return 1
443def getPreviousSeriesEventData(seriesId):
444 """
445 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.
447 """
448 previousEventVolunteers = (User.select(User).distinct()
449 .join(EventParticipant)
450 .join(Event)
451 .where(Event.seriesId==seriesId))
452 return previousEventVolunteers
454def getRepeatingEventsData(eventData):
455 """
456 Calculate the events to create based on a repeating event start and end date. Takes a
457 dictionary of event data.
459 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
461 Return a list of events to create from the event data.
462 """
464 return [ {'name': f"{eventData['name']} Week {counter+1}",
465 'date': eventData['startDate'] + timedelta(days=7*counter),
466 "week": counter+1,
467 'location': eventData['location']
468 }
469 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
471def preprocessEventData(eventData):
472 """
473 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
475 - dates should exist and be date objects if there is a value
476 - checkboxes should be True or False
477 - if term is given, convert it to a model object
478 - times should exist be strings in 24 hour format example: 14:40
479 - seriesData should be a JSON string
480 - Look up matching certification requirement if necessary
481 """
482 ## Process checkboxes
483 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'isLaborOnly']
485 for checkBox in eventCheckBoxes:
486 if checkBox not in eventData:
487 eventData[checkBox] = False
488 else:
489 eventData[checkBox] = bool(eventData[checkBox])
491 ## Process dates
492 eventDates = ['startDate', 'endDate']
493 for eventDate in eventDates:
494 if eventDate not in eventData: # There is no date given
495 eventData[eventDate] = ''
496 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
497 eventData[eventDate] = parser.parse(eventData[eventDate])
498 elif not isinstance(eventData[eventDate], date): # The date is not a date object
499 eventData[eventDate] = ''
501 # Process seriesData
502 if 'seriesData' not in eventData:
503 eventData['seriesData'] = json.dumps([])
505 # Process terms
506 if 'term' in eventData:
507 try:
508 eventData['term'] = Term.get_by_id(eventData['term'])
509 except DoesNotExist:
510 eventData['term'] = ''
512 # Process requirement
513 if 'certRequirement' in eventData:
514 try:
515 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
516 except DoesNotExist:
517 eventData['certRequirement'] = ''
518 elif 'id' in eventData:
519 # look up requirement
520 match = RequirementMatch.get_or_none(event=eventData['id'])
521 if match:
522 eventData['certRequirement'] = match.requirement
523 if 'timeStart' in eventData:
524 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
526 if 'timeEnd' in eventData:
527 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
529 return eventData
531def getTomorrowsEvents():
532 """Grabs each event that occurs tomorrow"""
533 tomorrowDate = date.today() + timedelta(days=1)
534 events = list(Event.select().where(Event.startDate==tomorrowDate))
535 return events
537def addEventView(viewer,event):
538 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
539 if not viewer.isCeltsAdmin:
540 EventView.get_or_create(user = viewer, event = event)
542def getEventRsvpCountsForTerm(term):
543 """
544 Get all of the RSVPs for the events that exist in the term.
545 Returns a dictionary with the event id as the key and the amount of
546 current RSVPs to that event as the pair.
547 """
548 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
549 .join(EventRsvp, JOIN.LEFT_OUTER)
550 .where(Event.term == term, Event.deletionDate == None)
551 .group_by(Event.id))
553 amountAsDict = {event.id: event.count for event in amount}
555 return amountAsDict
557def getEventRsvpCount(eventId):
558 """
559 Returns the number of RSVP'd participants for a given eventId.
560 """
561 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
563def getCountdownToEvent(event, *, currentDatetime=None):
564 """
565 Given an event, this function returns a string that conveys the amount of time left
566 until the start of the event.
568 Note about dates:
569 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
570 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
571 tomorrow with no mention of the hour.
572 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
573 and hours in actual time.
575 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
576 relative to this morning and exclude all hours and minutes
578 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
579 the real difference in days and hours without the aforementioned simplifying language.
580 """
582 if currentDatetime is None:
583 currentDatetime = datetime.now().replace(second=0, microsecond=0)
584 currentMorning = currentDatetime.replace(hour=0, minute=0)
586 eventStart = datetime.combine(event.startDate, event.timeStart)
587 eventEnd = datetime.combine(event.startDate, event.timeEnd)
589 if eventEnd < currentDatetime:
590 return "Already passed"
591 elif eventStart <= currentDatetime <= eventEnd:
592 return "Happening now"
594 timeUntilEvent = relativedelta(eventStart, currentDatetime)
595 calendarDelta = relativedelta(eventStart, currentMorning)
596 calendarYearsUntilEvent = calendarDelta.years
597 calendarMonthsUntilEvent = calendarDelta.months
598 calendarDaysUntilEvent = calendarDelta.days
600 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
601 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
602 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
603 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
604 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
606 # Years until
607 if calendarYearsUntilEvent:
608 if calendarMonthsUntilEvent:
609 return f"{yearString} and {monthString}"
610 return f"{yearString}"
611 # Months until
612 if calendarMonthsUntilEvent:
613 if calendarDaysUntilEvent:
614 return f"{monthString} and {dayString}"
615 return f"{monthString}"
616 # Days until
617 if calendarDaysUntilEvent:
618 if eventStart.time() < currentDatetime.time():
619 if calendarDaysUntilEvent == 1:
620 return "Tomorrow"
621 return f"{dayString}"
622 if timeUntilEvent.hours:
623 return f"{dayString} and {hourString}"
624 return f"{dayString}"
625 # Hours until
626 if timeUntilEvent.hours:
627 if timeUntilEvent.minutes:
628 return f"{hourString} and {minuteString}"
629 return f"{hourString}"
630 # Minutes until
631 elif timeUntilEvent.minutes > 1:
632 return f"{minuteString}"
633 # Seconds until
634 return "<1 minute"
636def copyRsvpToNewEvent(priorEvent, newEvent):
637 """
638 Copies rvsps from priorEvent to newEvent
639 """
640 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
642 for student in rsvpInfo:
643 newRsvp = EventRsvp(
644 user = student.user,
645 event = newEvent,
646 rsvpWaitlist = student.rsvpWaitlist
647 )
648 newRsvp.save()
649 numRsvps = len(rsvpInfo)
650 if numRsvps:
651 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
654def inviteCohortsToEvent(event, cohortYears):
655 """
656 Invites cohorts to a newly created event by associating the cohorts directly.
657 """
658 invitedCohorts = []
659 try:
660 for year in cohortYears:
661 year = int(year)
662 EventCohort.get_or_create(
663 event=event,
664 year=year,
665 defaults={'invited_at': datetime.now()}
666 )
668 addBonnerCohortToRsvpLog(year, event.id)
669 rsvpForBonnerCohort(year, event.id)
670 invitedCohorts.append(year)
672 if invitedCohorts:
673 cohortList = ', '.join(map(str, invitedCohorts))
674 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
676 return True, "Cohorts successfully added to new event", invitedCohorts
678 except Exception as e:
679 print(f"Error inviting cohorts to new event: {e}")
680 return False, f"Error adding cohorts to new event: {e}", []
682def updateEventCohorts(event, cohortYears):
683 """
684 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
685 """
686 invitedCohorts = []
687 try:
688 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
689 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
690 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
692 for year in yearsToAdd:
693 EventCohort.get_or_create(
694 event=event,
695 year=year,
696 defaults={'invited_at': datetime.now()}
697 )
699 addBonnerCohortToRsvpLog(year, event.id)
700 rsvpForBonnerCohort(year, event.id)
701 invitedCohorts.append(year)
703 if yearsToAdd:
704 cohortList = ', '.join(map(str, invitedCohorts))
705 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
707 return True, "Cohorts successfully updated for event", invitedCohorts
709 except Exception as e:
710 print(f"Error updating cohorts for event: {e}")
711 return False, f"Error updating cohorts for event: {e}", []