Coverage for app/logic/events.py: 94%
352 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-09-10 21:56 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-09-10 21:56 +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 eventName = fn.LOWER(Event.name)
415 checkIfLaborMeeting = eventName.contains("labor meeting")
417 participatedEvents = (Event.select(Event,
418 Program.programName,
419 Case(None,
420 (
421 ((Event.allowsLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"),
422 ((Event.allowsLabor | Event.isLaborOnly | Event.name.contains("Labor")), "Labor"),
423 (Event.isService, "Volunteer")
424 ),
425 "Attendee").alias("participatedType"),
426 EventParticipant.hoursEarned
427 )
428 .join(Program, JOIN.LEFT_OUTER).switch()
429 .join(EventParticipant)
430 .where(EventParticipant.user == user,
431 Event.isAllVolunteerTraining == False, Event.deletionDate == None, Event.isCeltsTraining == False)
432 .order_by(Event.startDate, Event.name))
434 allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType"), Value(0).alias("hoursEarned"))
435 .join(EventParticipant)
436 .where(Event.isAllVolunteerTraining == True,
437 EventParticipant.user == user))
438 union = participatedEvents.union_all(allVolunteer)
439 unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.isService, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType, union.c.hoursEarned).order_by(union.c.startDate, union.c.name).execute())
440 return unionParticipationWithVolunteer
442def validateNewEventData(data):
443 """
444 Confirm that the provided data is valid for an event.
446 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
448 Returns 3 values: (boolean success, the validation error message, the data object)
449 """
451 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['allowsLabor']]:
452 return (False, "Raw form data passed to validate method. Preprocess first.")
454 if data['timeEnd'] <= data['timeStart']:
455 return (False, "Event end time must be after start time.")
457 # Validation if we are inserting a new event
458 if 'id' not in data:
460 sameEventList = list((Event.select().where((Event.name == data['name']) &
461 (Event.location == data['location']) &
462 (Event.startDate == data['startDate']) &
463 (Event.timeStart == data['timeStart'])).execute()))
465 sameEventListCopy = sameEventList.copy()
467 for event in sameEventListCopy:
468 if event.isCanceled or (event.seriesId and event.isRepeating):
469 sameEventList.remove(event)
471 try:
472 Term.get_by_id(data['term'])
473 except DoesNotExist as e:
474 return (False, f"Not a valid term: {data['term']}")
475 if sameEventList:
476 return (False, "This event already exists")
478 data['valid'] = True
479 return (True, "All inputs are valid.")
481def calculateNewSeriesId():
482 """
483 Gets the max series ID so that new seriesId can be assigned.
484 """
485 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
486 if maxSeriesId:
487 return maxSeriesId + 1
488 return 1
490def getPreviousSeriesEventData(seriesId):
491 """
492 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.
494 """
495 previousEventVolunteers = (User.select(User).distinct()
496 .join(EventParticipant)
497 .join(Event)
498 .where(Event.seriesId==seriesId))
499 return previousEventVolunteers
501def getRepeatingEventsData(eventData):
502 """
503 Calculate the events to create based on a repeating event start and end date. Takes a
504 dictionary of event data.
506 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
508 Return a list of events to create from the event data.
509 """
511 return [ {'name': f"{eventData['name']} Week {counter+1}",
512 'date': eventData['startDate'] + timedelta(days=7*counter),
513 "week": counter+1,
514 'location': eventData['location']
515 }
516 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
518def preprocessEventData(eventData):
519 """
520 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
522 - dates should exist and be date objects if there is a value
523 - checkboxes should be True or False
524 - if term is given, convert it to a model object
525 - times should exist be strings in 24 hour format example: 14:40
526 - seriesData should be a JSON string
527 - Look up matching certification requirement if necessary
528 """
530 ## Process checkboxes and templateData
531 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'allowsLabor', 'isLaborOnly', 'isCeltsTraining']
533 for checkBox in eventCheckBoxes:
534 if checkBox not in eventData:
535 eventData[checkBox] = False
536 else:
537 eventData[checkBox] = bool(eventData[checkBox])
539 ## Process dates
540 eventDates = ['startDate', 'endDate']
541 for eventDate in eventDates:
542 if eventDate not in eventData: # There is no date given
543 eventData[eventDate] = ''
544 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
545 eventData[eventDate] = parser.parse(eventData[eventDate])
546 elif not isinstance(eventData[eventDate], date): # The date is not a date object
547 eventData[eventDate] = ''
549 # Process seriesData
550 if 'seriesData' not in eventData:
551 eventData['seriesData'] = json.dumps([])
553 # Process terms
554 if 'term' in eventData:
555 try:
556 eventData['term'] = Term.get_by_id(eventData['term'])
557 except DoesNotExist:
558 eventData['term'] = ''
560 # Process requirement
561 if 'certRequirement' in eventData:
562 try:
563 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
564 except DoesNotExist:
565 eventData['certRequirement'] = ''
566 elif 'id' in eventData:
567 # look up requirement
568 match = RequirementMatch.get_or_none(event=eventData['id'])
569 if match:
570 eventData['certRequirement'] = match.requirement
571 if 'timeStart' in eventData:
572 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
574 if 'timeEnd' in eventData:
575 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
577 return eventData
579def getTomorrowsEvents():
580 """Grabs each event that occurs tomorrow"""
581 tomorrowDate = date.today() + timedelta(days=1)
582 events = list(Event.select().where(Event.startDate==tomorrowDate))
583 return events
585def addEventView(viewer,event):
586 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
587 if not viewer.isCeltsAdmin:
588 EventView.get_or_create(user = viewer, event = event)
590def getEventRsvpCountsForTerm(term):
591 """
592 Get all of the RSVPs for the events that exist in the term.
593 Returns a dictionary with the event id as the key and the amount of
594 current RSVPs to that event as the pair.
595 """
596 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
597 .join(EventRsvp, JOIN.LEFT_OUTER)
598 .where(Event.term == term, Event.deletionDate == None)
599 .group_by(Event.id))
601 amountAsDict = {event.id: event.count for event in amount}
603 return amountAsDict
605def getEventRsvpCount(eventId):
606 """
607 Returns the number of RSVP'd participants for a given eventId.
608 """
609 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
611def getCountdownToEvent(event, *, currentDatetime=None):
612 """
613 Given an event, this function returns a string that conveys the amount of time left
614 until the start of the event.
616 Note about dates:
617 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
618 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
619 tomorrow with no mention of the hour.
620 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
621 and hours in actual time.
623 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
624 relative to this morning and exclude all hours and minutes
626 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
627 the real difference in days and hours without the aforementioned simplifying language.
628 """
630 if currentDatetime is None:
631 currentDatetime = datetime.now().replace(second=0, microsecond=0)
632 currentMorning = currentDatetime.replace(hour=0, minute=0)
634 eventStart = datetime.combine(event.startDate, event.timeStart)
635 eventEnd = datetime.combine(event.startDate, event.timeEnd)
637 if eventEnd < currentDatetime:
638 return "Already passed"
639 elif eventStart <= currentDatetime <= eventEnd:
640 return "Happening now"
642 timeUntilEvent = relativedelta(eventStart, currentDatetime)
643 calendarDelta = relativedelta(eventStart, currentMorning)
644 calendarYearsUntilEvent = calendarDelta.years
645 calendarMonthsUntilEvent = calendarDelta.months
646 calendarDaysUntilEvent = calendarDelta.days
648 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
649 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
650 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
651 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
652 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
654 # Years until
655 if calendarYearsUntilEvent:
656 if calendarMonthsUntilEvent:
657 return f"{yearString} and {monthString}"
658 return f"{yearString}"
659 # Months until
660 if calendarMonthsUntilEvent:
661 if calendarDaysUntilEvent:
662 return f"{monthString} and {dayString}"
663 return f"{monthString}"
664 # Days until
665 if calendarDaysUntilEvent:
666 if eventStart.time() < currentDatetime.time():
667 if calendarDaysUntilEvent == 1:
668 return "Tomorrow"
669 return f"{dayString}"
670 if timeUntilEvent.hours:
671 return f"{dayString} and {hourString}"
672 return f"{dayString}"
673 # Hours until
674 if timeUntilEvent.hours:
675 if timeUntilEvent.minutes:
676 return f"{hourString} and {minuteString}"
677 return f"{hourString}"
678 # Minutes until
679 elif timeUntilEvent.minutes > 1:
680 return f"{minuteString}"
681 # Seconds until
682 return "<1 minute"
684def copyRsvpToNewEvent(priorEvent, newEvent):
685 """
686 Copies rvsps from priorEvent to newEvent
687 """
688 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
690 for student in rsvpInfo:
691 newRsvp = EventRsvp(
692 user = student.user,
693 event = newEvent,
694 rsvpWaitlist = student.rsvpWaitlist
695 )
696 newRsvp.save()
697 numRsvps = len(rsvpInfo)
698 if numRsvps:
699 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")
702def inviteCohortsToEvent(event, cohortYears):
703 """
704 Invites cohorts to a newly created event by associating the cohorts directly.
705 """
706 invitedCohorts = []
707 try:
708 for year in cohortYears:
709 year = int(year)
710 EventCohort.get_or_create(
711 event=event,
712 year=year,
713 defaults={'invited_at': datetime.now()}
714 )
716 addBonnerCohortToRsvpLog(year, event.id)
717 rsvpForBonnerCohort(year, event.id)
718 invitedCohorts.append(year)
720 if invitedCohorts:
721 cohortList = ', '.join(map(str, invitedCohorts))
722 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}")
724 return True, "Cohorts successfully added to new event", invitedCohorts
726 except Exception as e:
727 print(f"Error inviting cohorts to new event: {e}")
728 return False, f"Error adding cohorts to new event: {e}", []
730def updateEventCohorts(event, cohortYears):
731 """
732 Updates the cohorts for an existing event by adding new ones and removing outdated ones.
733 """
734 invitedCohorts = []
735 try:
736 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event))
737 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts]
738 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears]
740 for year in yearsToAdd:
741 EventCohort.get_or_create(
742 event=event,
743 year=year,
744 defaults={'invited_at': datetime.now()}
745 )
747 addBonnerCohortToRsvpLog(year, event.id)
748 rsvpForBonnerCohort(year, event.id)
749 invitedCohorts.append(year)
751 if yearsToAdd:
752 cohortList = ', '.join(map(str, invitedCohorts))
753 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}")
755 return True, "Cohorts successfully updated for event", invitedCohorts
757 except Exception as e:
758 print(f"Error updating cohorts for event: {e}")
759 return False, f"Error updating cohorts for event: {e}", []