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