Coverage for app/logic/events.py: 94%
350 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-08-24 19:35 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-08-24 19:35 +0000
1from flask import url_for, g, session
2from peewee import DoesNotExist, fn, JOIN, Case, Value
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 # Reformat dates from Jul 10, 2026 to 2026-07-10 for easier sorting
124 eventData2 = []
125 for ed in eventData['seriesData']:
126 try:
127 ed['eventDate'] = datetime.strptime(ed['eventDate'], "%b %d, %Y").strftime("%Y-%m-%d")
128 except:
129 pass # eventDate comes into the system differently for recurring weekly events and recurring events (non-weekly). This should format it correctly for both cases
130 eventData2.append(ed)
131 eventData['seriesData'] = eventData2
133 seriesData = sorted(eventData.get('seriesData'), key=lambda x: datetime.strptime(x['eventDate'].split(' ')[0] + ' ' + x['startTime'], '%Y-%m-%d %H:%M'))
134 # sorts the events in the series by date and time so that the events are created in order and the naming convention of Week 1, Week 2, etc. is consistent with the order of the events.
135 isRepeating = bool(eventData.get('isRepeating'))
136 with mainDB.atomic() as transaction:
137 for index, event in enumerate(seriesData):
138 eventInfo = eventData.copy()
139 eventInfo.update({
140 'name': event['eventName'],
141 'startDate': event['eventDate'],
142 'timeStart': event['startTime'],
143 'timeEnd': event['endTime'],
144 'location': event['eventLocation'],
145 'seriesId': seriesId,
146 'isRepeating': bool(isRepeating),
147 })
148 # Try to save each offering
149 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles)
150 if validationErrorMessage:
151 failedSavedOfferings.append((index, validationErrorMessage))
152 allSavesWereSuccessful = False
153 else:
154 savedEvent = savedEvents[0]
155 savedOfferings.append(savedEvent)
156 if not allSavesWereSuccessful:
157 savedOfferings = []
158 transaction.rollback()
160 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings
163def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
164 """
165 Tries to save an event to the database:
166 Checks that the event data is valid and if it is, it continues to save the new
167 event to the database and adds files if there are any.
168 If it is not valid it will return a validation error.
170 Returns:
171 The saved event, created events and an error message if an error occurred.
172 """
174 # Manually set the value of RSVP Limit if it is and empty string since it is
175 # automatically changed from "" to 0
176 if eventData["rsvpLimit"] == "":
177 eventData["rsvpLimit"] = None
178 newEventData = preprocessEventData(eventData)
180 isValid, validationErrorMessage = validateNewEventData(newEventData)
181 if not isValid:
182 return [], validationErrorMessage
184 events = saveEventToDb(newEventData, renewedEvent)
185 if attachmentFiles:
186 for event in events:
187 addFile = FileHandler(attachmentFiles, eventId=event.id)
188 addFile.saveFiles(parentEvent=events[0])
189 return events, ""
192def saveEventToDb(newEventData, renewedEvent = False):
194 if not newEventData.get('valid', False) and not renewedEvent:
195 raise Exception("Unvalidated data passed to saveEventToDb")
197 isNewEvent = ('id' not in newEventData)
198 eventRecords = []
199 with mainDB.atomic():
200 eventData = {
201 "term": newEventData['term'],
202 "name": newEventData['name'],
203 "description": newEventData['description'],
204 "timeStart": newEventData['timeStart'],
205 "timeEnd": newEventData['timeEnd'],
206 "location": newEventData['location'],
207 "isFoodProvided" : newEventData['isFoodProvided'],
208 "isLaborOnly" : newEventData['isLaborOnly'],
209 "allowsLabor" : newEventData['allowsLabor'],
210 "isTraining": newEventData['isTraining'],
211 "isEngagement": newEventData['isEngagement'],
212 "isRsvpRequired": newEventData['isRsvpRequired'],
213 "isService": newEventData['isService'],
214 "startDate": newEventData['startDate'],
215 "rsvpLimit": newEventData['rsvpLimit'],
216 "contactEmail": newEventData['contactEmail'],
217 "contactName": newEventData['contactName'],
218 }
220 # These fields below are only relevant during event creation so we only set/change them when
221 # it is a new event.
222 if isNewEvent:
223 eventData['program'] = newEventData['program']
224 eventData['seriesId'] = newEventData.get('seriesId')
225 eventData['isRepeating'] = bool(newEventData.get('isRepeating'))
226 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
227 eventData["isCeltsTraining"] = bool(newEventData.get('isCeltsTraining', False))
228 eventRecord = Event.create(**eventData)
229 else:
230 eventRecord = Event.get_by_id(newEventData['id'])
231 for key, value in eventData.items():
232 setattr(eventRecord, key, value)
233 eventRecord.save()
235 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
236 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
238 eventRecords.append(eventRecord)
239 return eventRecords
241def getVolunteerOpportunities(term):
242 volunteerOpportunities = list(Event.select(Event, Program)
243 .join(Program)
244 .where((Event.term == term) &
245 (Event.deletionDate.is_null()) &
246 (Event.isService == True) &
247 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null())
248 )
249 .order_by(Event.startDate, Event.timeStart)
250 .execute())
252 programs = {}
254 for event in volunteerOpportunities:
255 programs.setdefault(event.program, []).append(event)
257 return programs
259def getEngagementEvents(term):
260 engagementEvents = list(Event.select(Event, Program)
261 .join(Program)
262 .where(Event.isEngagement, Event.isLaborOnly == False,
263 Event.term == term, Event.deletionDate == None)
264 .order_by(Event.startDate, Event.timeStart)
265 .execute())
266 return engagementEvents
268def getUpcomingVolunteerOpportunitiesCount(term, currentDate):
269 """
270 Return a count of all upcoming events for each volunteer opportunitiesprogram.
271 """
273 upcomingCount = (
274 Program
275 .select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
276 .join(Event, on=(Program.id == Event.program_id))
277 .where(
278 (Event.term == term) &
279 (Event.deletionDate.is_null(True)) &
280 (Event.isService == True) &
281 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
282 ((Event.startDate > currentDate) |
283 ((Event.startDate == currentDate) & (Event.timeEnd >= currentDate.time()))) &
284 (Event.isCanceled == False)
285 )
286 .group_by(Program.id)
287 )
289 programCountDict = {}
290 for programCount in upcomingCount:
291 programCountDict[programCount.id] = programCount.eventCount
292 return programCountDict
294def getPastVolunteerOpportunitiesCount(term, currentDate):
295 """
296 Return a count of all past events for each volunteer opportunities program.
297 """
299 pastCount = (
300 Program
301 .select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
302 .join(Event, on=(Program.id == Event.program_id))
303 .where(
304 (Event.term == term) &
305 (Event.deletionDate.is_null(True)) &
306 (Event.isService == True) &
307 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
308 ((Event.startDate < currentDate) |
309 ((Event.startDate == currentDate) & (Event.timeStart <= currentDate.time()))) &
310 (Event.isCanceled == False)
311 )
312 .group_by(Program.id)
313 )
315 programCountDict = {}
316 for programCount in pastCount:
317 programCountDict[programCount.id] = programCount.eventCount
318 return programCountDict
320def getTrainingEvents(term, user):
321 """
322 The allTrainingsEvent query is designed to select and count eventId's after grouping them
323 together by id's of similiar value. The query will then return the event that is associated
324 with the most programs (highest count) by doing this we can ensure that the event being
325 returned is the All Trainings Event.
326 term: expected to be the ID of a term
327 user: expected to be the current user
328 return: a list of all trainings the user can view
329 """
330 trainingQuery = (Event.select(Event).distinct()
331 .join(Program, JOIN.LEFT_OUTER)
332 .where(Event.isTraining == True, Event.isLaborOnly == False,
333 Event.term == term, Event.deletionDate == None)
334 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
336 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
337 if hideBonner:
338 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
340 return list(trainingQuery.execute())
342def getBonnerEvents(term):
343 bonnerScholarsEvents = list(
344 Event.select(Event, Program.id.alias("program_id"))
345 .join(Program)
346 .where(
347 Program.isBonnerScholars,
348 Event.term == term,
349 Event.deletionDate == None
350 )
351 .order_by(Event.startDate, Event.timeStart)
352 .execute()
353 )
354 return bonnerScholarsEvents
356def getCeltsLabor(term):
357 """
358 Labor tab: events explicitly marked as Labor Only.
359 """
360 celtsLabor = list(Event.select()
361 .where(Event.term == term, Event.deletionDate == None, Event.isLaborOnly == True)
362 .order_by(Event.startDate, Event.timeStart, Event.id)
363 .execute())
364 return celtsLabor
366def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
367 """
368 Get the list of upcoming events that the user is interested in as long
369 as they are not banned from the program that the event is a part of.
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 events = (Event.select().distinct()
377 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
378 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
379 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
380 .where(Event.deletionDate == None, Event.startDate >= asOf,
381 (Interest.user == user) | (EventRsvp.user == user),
382 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
384 if program:
385 events = events.where(Event.program == program)
387 events = events.order_by(Event.startDate, Event.timeStart)
389 eventsList = []
390 seriesEventsList = []
392 # removes all events in series except for the next upcoming one
393 for event in events:
394 if event.seriesId:
395 if not event.isCanceled:
396 if event.seriesId not in seriesEventsList:
397 eventsList.append(event)
398 seriesEventsList.append(event.seriesId)
399 else:
400 if not event.isCanceled:
401 eventsList.append(event)
403 return eventsList
405def getParticipatedEventsForUser(user):
406 """
407 Get all the events a user has participated in.
408 :param user: a username or User object
409 :param asOf: The date to use when determining future and past events.
410 Used in testing, defaults to the current timestamp.
411 :return: A list of Event objects
412 """
414 # Does this handle labor only and/or includes labor events?
415 participatedEvents = (Event.select(Event, Program.programName, Case(None, (
416 ((Event.allowsLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"),
417 ((Event.allowsLabor | Event.isLaborOnly | Event.name.contains("Labor")), "Labor"),
418 (Event.isService, "Volunteer")), "Attendee").alias("participatedType"))
419 .join(Program, JOIN.LEFT_OUTER).switch()
420 .join(EventParticipant)
421 .where(EventParticipant.user == user,
422 Event.isAllVolunteerTraining == False, Event.deletionDate == None, Event.isCeltsTraining == False)
423 .order_by(Event.startDate, Event.name))
424 allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType"))
425 .join(EventParticipant)
426 .where(Event.isAllVolunteerTraining == True,
427 EventParticipant.user == user))
428 union = participatedEvents.union_all(allVolunteer)
429 unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType).order_by(union.c.startDate, union.c.name).execute())
430 return unionParticipationWithVolunteer
432def validateNewEventData(data):
433 """
434 Confirm that the provided data is valid for an event.
436 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
438 Returns 3 values: (boolean success, the validation error message, the data object)
439 """
441 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['allowsLabor']]:
442 return (False, "Raw form data passed to validate method. Preprocess first.")
444 if data['timeEnd'] <= data['timeStart']:
445 return (False, "Event end time must be after start time.")
447 # Validation if we are inserting a new event
448 if 'id' not in data:
450 sameEventList = list((Event.select().where((Event.name == data['name']) &
451 (Event.location == data['location']) &
452 (Event.startDate == data['startDate']) &
453 (Event.timeStart == data['timeStart'])).execute()))
455 sameEventListCopy = sameEventList.copy()
457 for event in sameEventListCopy:
458 if event.isCanceled or (event.seriesId and event.isRepeating):
459 sameEventList.remove(event)
461 try:
462 Term.get_by_id(data['term'])
463 except DoesNotExist as e:
464 return (False, f"Not a valid term: {data['term']}")
465 if sameEventList:
466 return (False, "This event already exists")
468 data['valid'] = True
469 return (True, "All inputs are valid.")
471def calculateNewSeriesId():
472 """
473 Gets the max series ID so that new seriesId can be assigned.
474 """
475 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
476 if maxSeriesId:
477 return maxSeriesId + 1
478 return 1
480def getPreviousSeriesEventData(seriesId):
481 """
482 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.
484 """
485 previousEventVolunteers = (User.select(User).distinct()
486 .join(EventParticipant)
487 .join(Event)
488 .where(Event.seriesId==seriesId))
489 return previousEventVolunteers
491def getRepeatingEventsData(eventData):
492 """
493 Calculate the events to create based on a repeating event start and end date. Takes a
494 dictionary of event data.
496 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
498 Return a list of events to create from the event data.
499 """
501 return [ {'name': f"{eventData['name']} Week {counter+1}",
502 'date': eventData['startDate'] + timedelta(days=7*counter),
503 "week": counter+1,
504 'location': eventData['location']
505 }
506 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
508def preprocessEventData(eventData):
509 """
510 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
512 - dates should exist and be date objects if there is a value
513 - checkboxes should be True or False
514 - if term is given, convert it to a model object
515 - times should exist be strings in 24 hour format example: 14:40
516 - seriesData should be a JSON string
517 - Look up matching certification requirement if necessary
518 """
520 ## Process checkboxes and templateData
521 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'allowsLabor', 'isLaborOnly', 'isCeltsTraining']
523 for checkBox in eventCheckBoxes:
524 if checkBox not in eventData:
525 eventData[checkBox] = False
526 else:
527 eventData[checkBox] = bool(eventData[checkBox])
529 ## Process dates
530 eventDates = ['startDate', 'endDate']
531 for eventDate in eventDates:
532 if eventDate not in eventData: # There is no date given
533 eventData[eventDate] = ''
534 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
535 eventData[eventDate] = parser.parse(eventData[eventDate])
536 elif not isinstance(eventData[eventDate], date): # The date is not a date object
537 eventData[eventDate] = ''
539 # Process seriesData
540 if 'seriesData' not in eventData:
541 eventData['seriesData'] = json.dumps([])
543 # Process terms
544 if 'term' in eventData:
545 try:
546 eventData['term'] = Term.get_by_id(eventData['term'])
547 except DoesNotExist:
548 eventData['term'] = ''
550 # Process requirement
551 if 'certRequirement' in eventData:
552 try:
553 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
554 except DoesNotExist:
555 eventData['certRequirement'] = ''
556 elif 'id' in eventData:
557 # look up requirement
558 match = RequirementMatch.get_or_none(event=eventData['id'])
559 if match:
560 eventData['certRequirement'] = match.requirement
561 if 'timeStart' in eventData:
562 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
564 if 'timeEnd' in eventData:
565 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
567 return eventData
569def getTomorrowsEvents():
570 """Grabs each event that occurs tomorrow"""
571 tomorrowDate = date.today() + timedelta(days=1)
572 events = list(Event.select().where(Event.startDate==tomorrowDate))
573 return events
575def addEventView(viewer,event):
576 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
577 if not viewer.isCeltsAdmin:
578 EventView.get_or_create(user = viewer, event = event)
580def getEventRsvpCountsForTerm(term):
581 """
582 Get all of the RSVPs for the events that exist in the term.
583 Returns a dictionary with the event id as the key and the amount of
584 current RSVPs to that event as the pair.
585 """
586 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
587 .join(EventRsvp, JOIN.LEFT_OUTER)
588 .where(Event.term == term, Event.deletionDate == None)
589 .group_by(Event.id))
591 amountAsDict = {event.id: event.count for event in amount}
593 return amountAsDict
595def getEventRsvpCount(eventId):
596 """
597 Returns the number of RSVP'd participants for a given eventId.
598 """
599 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
601def getCountdownToEvent(event, *, currentDatetime=None):
602 """
603 Given an event, this function returns a string that conveys the amount of time left
604 until the start of the event.
606 Note about dates:
607 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
608 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
609 tomorrow with no mention of the hour.
610 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
611 and hours in actual time.
613 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
614 relative to this morning and exclude all hours and minutes
616 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
617 the real difference in days and hours without the aforementioned simplifying language.
618 """
620 if currentDatetime is None:
621 currentDatetime = datetime.now().replace(second=0, microsecond=0)
622 currentMorning = currentDatetime.replace(hour=0, minute=0)
624 eventStart = datetime.combine(event.startDate, event.timeStart)
625 eventEnd = datetime.combine(event.startDate, event.timeEnd)
627 if eventEnd < currentDatetime:
628 return "Already passed"
629 elif eventStart <= currentDatetime <= eventEnd:
630 return "Happening now"
632 timeUntilEvent = relativedelta(eventStart, currentDatetime)
633 calendarDelta = relativedelta(eventStart, currentMorning)
634 calendarYearsUntilEvent = calendarDelta.years
635 calendarMonthsUntilEvent = calendarDelta.months
636 calendarDaysUntilEvent = calendarDelta.days
638 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
639 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
640 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
641 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
642 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
644 # Years until
645 if calendarYearsUntilEvent:
646 if calendarMonthsUntilEvent:
647 return f"{yearString} and {monthString}"
648 return f"{yearString}"
649 # Months until
650 if calendarMonthsUntilEvent:
651 if calendarDaysUntilEvent:
652 return f"{monthString} and {dayString}"
653 return f"{monthString}"
654 # Days until
655 if calendarDaysUntilEvent:
656 if eventStart.time() < currentDatetime.time():
657 if calendarDaysUntilEvent == 1:
658 return "Tomorrow"
659 return f"{dayString}"
660 if timeUntilEvent.hours:
661 return f"{dayString} and {hourString}"
662 return f"{dayString}"
663 # Hours until
664 if timeUntilEvent.hours:
665 if timeUntilEvent.minutes:
666 return f"{hourString} and {minuteString}"
667 return f"{hourString}"
668 # Minutes until
669 elif timeUntilEvent.minutes > 1:
670 return f"{minuteString}"
671 # Seconds until
672 return "<1 minute"
674def copyRsvpToNewEvent(priorEvent, newEvent):
675 """
676 Copies rvsps from priorEvent to newEvent
677 """
678 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
680 for student in rsvpInfo:
681 newRsvp = EventRsvp(
682 user = student.user,
683 event = newEvent,
684 rsvpWaitlist = student.rsvpWaitlist
685 )
686 newRsvp.save()
687 numRsvps = len(rsvpInfo)
688 if numRsvps:
689 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
692def inviteCohortsToEvent(event, cohortYears):
693 """
694 Invites cohorts to a newly created event by associating the cohorts directly.
695 """
696 invitedCohorts = []
697 try:
698 for year in cohortYears:
699 year = int(year)
700 EventCohort.get_or_create(
701 event=event,
702 year=year,
703 defaults={'invited_at': datetime.now()}
704 )
706 addBonnerCohortToRsvpLog(year, event.id)
707 rsvpForBonnerCohort(year, event.id)
708 invitedCohorts.append(year)
710 if invitedCohorts:
711 cohortList = ', '.join(map(str, invitedCohorts))
712 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
714 return True, "Cohorts successfully added to new event", invitedCohorts
716 except Exception as e:
717 print(f"Error inviting cohorts to new event: {e}")
718 return False, f"Error adding cohorts to new event: {e}", []
720def updateEventCohorts(event, cohortYears):
721 """
722 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
723 """
724 invitedCohorts = []
725 try:
726 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
727 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
728 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
730 for year in yearsToAdd:
731 EventCohort.get_or_create(
732 event=event,
733 year=year,
734 defaults={'invited_at': datetime.now()}
735 )
737 addBonnerCohortToRsvpLog(year, event.id)
738 rsvpForBonnerCohort(year, event.id)
739 invitedCohorts.append(year)
741 if yearsToAdd:
742 cohortList = ', '.join(map(str, invitedCohorts))
743 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
745 return True, "Cohorts successfully updated for event", invitedCohorts
747 except Exception as e:
748 print(f"Error updating cohorts for event: {e}")
749 return False, f"Error updating cohorts for event: {e}", []