Coverage for app/logic/events.py: 94%
339 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-25 19:18 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-25 19:18 +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 getpastVolunteerOpportunitiesCount(term, currentTime):
281 """
282 Return a count of all past events for each volunteer opportunities program.
283 """
285 pastCount = (
286 Program
287 .select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
288 .join(Event, on=(Program.id == Event.program_id))
289 .where(
290 (Event.term == term) &
291 (Event.deletionDate.is_null(True)) &
292 (Event.isService == True) &
293 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
294 ((Event.startDate < currentTime) |
295 ((Event.startDate == currentTime) & (Event.timeStart <= currentTime))) &
296 (Event.isCanceled == False)
297 )
298 .group_by(Program.id)
299 )
301 programCountDict = {}
302 for programCount in pastCount:
303 programCountDict[programCount.id] = programCount.eventCount
304 return programCountDict
306def getTrainingEvents(term, user):
307 """
308 The allTrainingsEvent query is designed to select and count eventId's after grouping them
309 together by id's of similiar value. The query will then return the event that is associated
310 with the most programs (highest count) by doing this we can ensure that the event being
311 returned is the All Trainings Event.
312 term: expected to be the ID of a term
313 user: expected to be the current user
314 return: a list of all trainings the user can view
315 """
316 trainingQuery = (Event.select(Event).distinct()
317 .join(Program, JOIN.LEFT_OUTER)
318 .where(Event.isTraining == True, Event.isLaborOnly == False,
319 Event.term == term, Event.deletionDate == None)
320 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
322 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
323 if hideBonner:
324 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
326 return list(trainingQuery.execute())
328def getBonnerEvents(term):
329 bonnerScholarsEvents = list(
330 Event.select(Event, Program.id.alias("program_id"))
331 .join(Program)
332 .where(
333 Program.isBonnerScholars,
334 Event.term == term,
335 Event.deletionDate == None
336 )
337 .order_by(Event.startDate, Event.timeStart)
338 .execute()
339 )
340 return bonnerScholarsEvents
342def getCeltsLabor(term):
343 """
344 Labor tab: events explicitly marked as Labor Only.
345 """
346 celtsLabor = list(Event.select()
347 .where(Event.term == term, Event.deletionDate == None, Event.isLaborOnly == True)
348 .order_by(Event.startDate, Event.timeStart, Event.id)
349 .execute())
350 return celtsLabor
352def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
353 """
354 Get the list of upcoming events that the user is interested in as long
355 as they are not banned from the program that the event is a part of.
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 events = (Event.select().distinct()
363 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
364 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
365 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
366 .where(Event.deletionDate == None, Event.startDate >= asOf,
367 (Interest.user == user) | (EventRsvp.user == user),
368 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
370 if program:
371 events = events.where(Event.program == program)
373 events = events.order_by(Event.startDate, Event.timeStart)
375 eventsList = []
376 seriesEventsList = []
378 # removes all events in series except for the next upcoming one
379 for event in events:
380 if event.seriesId:
381 if not event.isCanceled:
382 if event.seriesId not in seriesEventsList:
383 eventsList.append(event)
384 seriesEventsList.append(event.seriesId)
385 else:
386 if not event.isCanceled:
387 eventsList.append(event)
389 return eventsList
391def getParticipatedEventsForUser(user):
392 """
393 Get all the events a user has participated in.
394 :param user: a username or User object
395 :param asOf: The date to use when determining future and past events.
396 Used in testing, defaults to the current timestamp.
397 :return: A list of Event objects
398 """
400 participatedEvents = (Event.select(Event, Program.programName)
401 .join(Program, JOIN.LEFT_OUTER).switch()
402 .join(EventParticipant)
403 .where(EventParticipant.user == user,
404 Event.isAllVolunteerTraining == False, Event.deletionDate == None)
405 .order_by(Event.startDate, Event.name))
407 allVolunteer = (Event.select(Event, "")
408 .join(EventParticipant)
409 .where(Event.isAllVolunteerTraining == True,
410 EventParticipant.user == user))
411 union = participatedEvents.union_all(allVolunteer)
412 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())
414 return unionParticipationWithVolunteer
416def validateNewEventData(data):
417 """
418 Confirm that the provided data is valid for an event.
420 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
422 Returns 3 values: (boolean success, the validation error message, the data object)
423 """
425 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['isLaborOnly']]:
426 return (False, "Raw form data passed to validate method. Preprocess first.")
428 if data['timeEnd'] <= data['timeStart']:
429 return (False, "Event end time must be after start time.")
431 # Validation if we are inserting a new event
432 if 'id' not in data:
434 sameEventList = list((Event.select().where((Event.name == data['name']) &
435 (Event.location == data['location']) &
436 (Event.startDate == data['startDate']) &
437 (Event.timeStart == data['timeStart'])).execute()))
439 sameEventListCopy = sameEventList.copy()
441 for event in sameEventListCopy:
442 if event.isCanceled or (event.seriesId and event.isRepeating):
443 sameEventList.remove(event)
445 try:
446 Term.get_by_id(data['term'])
447 except DoesNotExist as e:
448 return (False, f"Not a valid term: {data['term']}")
449 if sameEventList:
450 return (False, "This event already exists")
452 data['valid'] = True
453 return (True, "All inputs are valid.")
455def calculateNewSeriesId():
456 """
457 Gets the max series ID so that new seriesId can be assigned.
458 """
459 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
460 if maxSeriesId:
461 return maxSeriesId + 1
462 return 1
464def getPreviousSeriesEventData(seriesId):
465 """
466 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.
468 """
469 previousEventVolunteers = (User.select(User).distinct()
470 .join(EventParticipant)
471 .join(Event)
472 .where(Event.seriesId==seriesId))
473 return previousEventVolunteers
475def getRepeatingEventsData(eventData):
476 """
477 Calculate the events to create based on a repeating event start and end date. Takes a
478 dictionary of event data.
480 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
482 Return a list of events to create from the event data.
483 """
485 return [ {'name': f"{eventData['name']} Week {counter+1}",
486 'date': eventData['startDate'] + timedelta(days=7*counter),
487 "week": counter+1}
488 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
490def preprocessEventData(eventData):
491 """
492 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
494 - dates should exist and be date objects if there is a value
495 - checkboxes should be True or False
496 - if term is given, convert it to a model object
497 - times should exist be strings in 24 hour format example: 14:40
498 - seriesData should be a JSON string
499 - Look up matching certification requirement if necessary
500 """
501 ## Process checkboxes
502 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'isLaborOnly']
504 for checkBox in eventCheckBoxes:
505 if checkBox not in eventData:
506 eventData[checkBox] = False
507 else:
508 eventData[checkBox] = bool(eventData[checkBox])
510 ## Process dates
511 eventDates = ['startDate', 'endDate']
512 for eventDate in eventDates:
513 if eventDate not in eventData: # There is no date given
514 eventData[eventDate] = ''
515 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
516 eventData[eventDate] = parser.parse(eventData[eventDate])
517 elif not isinstance(eventData[eventDate], date): # The date is not a date object
518 eventData[eventDate] = ''
520 # Process seriesData
521 if 'seriesData' not in eventData:
522 eventData['seriesData'] = json.dumps([])
524 # Process terms
525 if 'term' in eventData:
526 try:
527 eventData['term'] = Term.get_by_id(eventData['term'])
528 except DoesNotExist:
529 eventData['term'] = ''
531 # Process requirement
532 if 'certRequirement' in eventData:
533 try:
534 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
535 except DoesNotExist:
536 eventData['certRequirement'] = ''
537 elif 'id' in eventData:
538 # look up requirement
539 match = RequirementMatch.get_or_none(event=eventData['id'])
540 if match:
541 eventData['certRequirement'] = match.requirement
542 if 'timeStart' in eventData:
543 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
545 if 'timeEnd' in eventData:
546 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
548 return eventData
550def getTomorrowsEvents():
551 """Grabs each event that occurs tomorrow"""
552 tomorrowDate = date.today() + timedelta(days=1)
553 events = list(Event.select().where(Event.startDate==tomorrowDate))
554 return events
556def addEventView(viewer,event):
557 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
558 if not viewer.isCeltsAdmin:
559 EventView.get_or_create(user = viewer, event = event)
561def getEventRsvpCountsForTerm(term):
562 """
563 Get all of the RSVPs for the events that exist in the term.
564 Returns a dictionary with the event id as the key and the amount of
565 current RSVPs to that event as the pair.
566 """
567 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
568 .join(EventRsvp, JOIN.LEFT_OUTER)
569 .where(Event.term == term, Event.deletionDate == None)
570 .group_by(Event.id))
572 amountAsDict = {event.id: event.count for event in amount}
574 return amountAsDict
576def getEventRsvpCount(eventId):
577 """
578 Returns the number of RSVP'd participants for a given eventId.
579 """
580 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
582def getCountdownToEvent(event, *, currentDatetime=None):
583 """
584 Given an event, this function returns a string that conveys the amount of time left
585 until the start of the event.
587 Note about dates:
588 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
589 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
590 tomorrow with no mention of the hour.
591 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
592 and hours in actual time.
594 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
595 relative to this morning and exclude all hours and minutes
597 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
598 the real difference in days and hours without the aforementioned simplifying language.
599 """
601 if currentDatetime is None:
602 currentDatetime = datetime.now().replace(second=0, microsecond=0)
603 currentMorning = currentDatetime.replace(hour=0, minute=0)
605 eventStart = datetime.combine(event.startDate, event.timeStart)
606 eventEnd = datetime.combine(event.startDate, event.timeEnd)
608 if eventEnd < currentDatetime:
609 return "Already passed"
610 elif eventStart <= currentDatetime <= eventEnd:
611 return "Happening now"
613 timeUntilEvent = relativedelta(eventStart, currentDatetime)
614 calendarDelta = relativedelta(eventStart, currentMorning)
615 calendarYearsUntilEvent = calendarDelta.years
616 calendarMonthsUntilEvent = calendarDelta.months
617 calendarDaysUntilEvent = calendarDelta.days
619 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
620 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
621 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
622 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
623 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
625 # Years until
626 if calendarYearsUntilEvent:
627 if calendarMonthsUntilEvent:
628 return f"{yearString} and {monthString}"
629 return f"{yearString}"
630 # Months until
631 if calendarMonthsUntilEvent:
632 if calendarDaysUntilEvent:
633 return f"{monthString} and {dayString}"
634 return f"{monthString}"
635 # Days until
636 if calendarDaysUntilEvent:
637 if eventStart.time() < currentDatetime.time():
638 if calendarDaysUntilEvent == 1:
639 return "Tomorrow"
640 return f"{dayString}"
641 if timeUntilEvent.hours:
642 return f"{dayString} and {hourString}"
643 return f"{dayString}"
644 # Hours until
645 if timeUntilEvent.hours:
646 if timeUntilEvent.minutes:
647 return f"{hourString} and {minuteString}"
648 return f"{hourString}"
649 # Minutes until
650 elif timeUntilEvent.minutes > 1:
651 return f"{minuteString}"
652 # Seconds until
653 return "<1 minute"
655def copyRsvpToNewEvent(priorEvent, newEvent):
656 """
657 Copies rvsps from priorEvent to newEvent
658 """
659 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
661 for student in rsvpInfo:
662 newRsvp = EventRsvp(
663 user = student.user,
664 event = newEvent,
665 rsvpWaitlist = student.rsvpWaitlist
666 )
667 newRsvp.save()
668 numRsvps = len(rsvpInfo)
669 if numRsvps:
670 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
673def inviteCohortsToEvent(event, cohortYears):
674 """
675 Invites cohorts to a newly created event by associating the cohorts directly.
676 """
677 invitedCohorts = []
678 try:
679 for year in cohortYears:
680 year = int(year)
681 EventCohort.get_or_create(
682 event=event,
683 year=year,
684 defaults={'invited_at': datetime.now()}
685 )
687 addBonnerCohortToRsvpLog(year, event.id)
688 rsvpForBonnerCohort(year, event.id)
689 invitedCohorts.append(year)
691 if invitedCohorts:
692 cohortList = ', '.join(map(str, invitedCohorts))
693 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
695 return True, "Cohorts successfully added to new event", invitedCohorts
697 except Exception as e:
698 print(f"Error inviting cohorts to new event: {e}")
699 return False, f"Error adding cohorts to new event: {e}", []
701def updateEventCohorts(event, cohortYears):
702 """
703 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
704 """
705 invitedCohorts = []
706 try:
707 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
708 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
709 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
711 for year in yearsToAdd:
712 EventCohort.get_or_create(
713 event=event,
714 year=year,
715 defaults={'invited_at': datetime.now()}
716 )
718 addBonnerCohortToRsvpLog(year, event.id)
719 rsvpForBonnerCohort(year, event.id)
720 invitedCohorts.append(year)
722 if yearsToAdd:
723 cohortList = ', '.join(map(str, invitedCohorts))
724 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
726 return True, "Cohorts successfully updated for event", invitedCohorts
728 except Exception as e:
729 print(f"Error updating cohorts for event: {e}")
730 return False, f"Error updating cohorts for event: {e}", []