Coverage for app/logic/events.py: 93%
344 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-23 03:00 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-23 03:00 +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
21from app.logic.createLogs import createActivityLog, createRsvpLog
22from app.logic.utils import format24HourTime
23from app.logic.fileHandler import FileHandler
24from app.logic.certification import updateCertRequirementForEvent
26def cancelEvent(eventId):
27 """
28 Cancels an event.
29 """
30 event = Event.get_or_none(Event.id == eventId)
31 if event:
32 event.isCanceled = True
33 event.save()
35 program = event.program
36 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')}.")
39def deleteEvent(eventId):
40 """
41 Deletes an event, if it is a recurring event, rename all following events
42 to make sure there is no gap in weeks.
43 """
44 event = Event.get_or_none(Event.id == eventId)
46 if event:
47 if event.recurringId:
48 recurringId = event.recurringId
49 recurringEvents = list(Event.select().where(Event.recurringId==recurringId, Event.deletionDate.is_null(True)).order_by(Event.id)) # orders for tests
50 eventDeleted = False
51 # once the deleted event is detected, change all other names to the previous event's name
52 for recurringEvent in recurringEvents:
53 if eventDeleted:
54 Event.update({Event.name:newEventName}).where(Event.id==recurringEvent.id).execute()
55 newEventName = recurringEvent.name
57 if recurringEvent == event:
58 newEventName = recurringEvent.name
59 eventDeleted = True
61 program = event.program
63 if program:
64 createActivityLog(f"Deleted \"{event.name}\" for {program.programName}, which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
65 else:
66 createActivityLog(f"Deleted a non-program event, \"{event.name}\", which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
68 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where(Event.id == event.id).execute()
71def deleteEventAndAllFollowing(eventId):
72 """
73 Deletes a recurring event and all the recurring events after it.
74 Modified to also apply to the case of events with multiple offerings
75 """
76 event = Event.get_or_none(Event.id == eventId)
77 if event:
78 if event.recurringId:
79 recurringId = event.recurringId
80 recurringSeries = list(Event.select(Event.id).where((Event.recurringId == recurringId) & (Event.startDate >= event.startDate)))
81 deletedEventList = [recurringEvent.id for recurringEvent in recurringSeries]
82 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where((Event.recurringId == recurringId) & (Event.startDate >= event.startDate)).execute()
83 return deletedEventList
85def deleteAllRecurringEvents(eventId):
86 """
87 Deletes all recurring events.
88 Modified to also apply for events with multiple offerings
89 """
90 event = Event.get_or_none(Event.id == eventId)
91 if event:
92 if event.recurringId:
93 recurringId = event.recurringId
94 allRecurringEvents = list(Event.select(Event.id).where(Event.recurringId == recurringId).order_by(Event.startDate))
95 eventId = allRecurringEvents[0].id
96 return deleteEventAndAllFollowing(eventId)
98def attemptSaveMultipleOfferings(eventData, attachmentFiles = None):
99 """
100 Tries to save an event with multiple offerings to the database:
101 Creates separate event data inheriting from the original eventData
102 with the specifics of each offering.
103 Calls attemptSaveEvent on each of the newly created datum
104 If any data is not valid it will return a validation error.
106 Returns:
107 allSavesWereSuccessful : bool | Whether or not all offering saves were successful
108 savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesWereSuccessful is False then this list will be empty.
109 failedSavedOfferings : List[(int, str), ...] | Tuples containing the indicies of failed saved offerings and the associated validation error message.
110 """
111 savedOfferings = []
112 failedSavedOfferings = []
113 allSavesWereSuccessful = True
115 # Creates a shared multipleOfferingId for all offerings to have
116 multipleOfferingId = calculateNewMultipleOfferingId()
118 # Create separate event data inheriting from the original eventData
119 multipleOfferingData = eventData.get('multipleOfferingData')
120 with mainDB.atomic() as transaction:
121 for index, event in enumerate(multipleOfferingData):
122 multipleOfferingDict = eventData.copy()
123 multipleOfferingDict.update({
124 'name': event['eventName'],
125 'startDate': event['eventDate'],
126 'timeStart': event['startTime'],
127 'timeEnd': event['endTime'],
128 'multipleOfferingId': multipleOfferingId
129 })
130 # Try to save each offering
131 savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles)
132 if validationErrorMessage:
133 failedSavedOfferings.append((index, validationErrorMessage))
134 allSavesWereSuccessful = False
135 else:
136 savedEvent = savedEvents[0]
137 savedOfferings.append(savedEvent)
138 if not allSavesWereSuccessful:
139 savedOfferings = []
140 transaction.rollback()
142 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings
145def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
146 """
147 Tries to save an event to the database:
148 Checks that the event data is valid and if it is, it continues to save the new
149 event to the database and adds files if there are any.
150 If it is not valid it will return a validation error.
152 Returns:
153 The saved event, created events and an error message if an error occurred.
154 """
156 # Manually set the value of RSVP Limit if it is and empty string since it is
157 # automatically changed from "" to 0
158 if eventData["rsvpLimit"] == "":
159 eventData["rsvpLimit"] = None
161 newEventData = preprocessEventData(eventData)
163 isValid, validationErrorMessage = validateNewEventData(newEventData)
164 if not isValid:
165 return [], validationErrorMessage
167 try:
168 events = saveEventToDb(newEventData, renewedEvent)
169 if attachmentFiles:
170 for event in events:
171 addFile = FileHandler(attachmentFiles, eventId=event.id)
172 addFile.saveFiles(saveOriginalFile=events[0])
173 return events, ""
174 except Exception as e:
175 print(f'Failed attemptSaveEvent() with Exception: {e}')
176 return [], e
178def saveEventToDb(newEventData, renewedEvent = False):
180 if not newEventData.get('valid', False) and not renewedEvent:
181 raise Exception("Unvalidated data passed to saveEventToDb")
184 isNewEvent = ('id' not in newEventData)
187 eventsToCreate = []
188 recurringSeriesId = None
189 multipleSeriesId = None
190 if (isNewEvent and newEventData['isRecurring']) and not renewedEvent:
191 eventsToCreate = getRecurringEventsData(newEventData)
192 recurringSeriesId = calculateNewrecurringId()
194 #temporarily applying the append for single events for now to tests
195 elif(isNewEvent and newEventData['isMultipleOffering']) and not renewedEvent:
196 eventsToCreate.append({'name': f"{newEventData['name']}",
197 'date':newEventData['startDate'],
198 "week":1})
199 multipleSeriesId = newEventData['multipleOfferingId']
201 else:
202 eventsToCreate.append({'name': f"{newEventData['name']}",
203 'date':newEventData['startDate'],
204 "week":1})
205 if renewedEvent:
206 recurringSeriesId = newEventData.get('recurringId')
207 eventRecords = []
208 for eventInstance in eventsToCreate:
209 with mainDB.atomic():
211 eventData = {
212 "term": newEventData['term'],
213 "name": eventInstance['name'],
214 "description": newEventData['description'],
215 "timeStart": newEventData['timeStart'],
216 "timeEnd": newEventData['timeEnd'],
217 "location": newEventData['location'],
218 "isFoodProvided" : newEventData['isFoodProvided'],
219 "isTraining": newEventData['isTraining'],
220 "isEngagement": newEventData['isEngagement'],
221 "isRsvpRequired": newEventData['isRsvpRequired'],
222 "isService": newEventData['isService'],
223 "startDate": eventInstance['date'],
224 "rsvpLimit": newEventData['rsvpLimit'],
225 "contactEmail": newEventData['contactEmail'],
226 "contactName": newEventData['contactName']
227 }
229 # The three fields below are only relevant during event creation so we only set/change them when
230 # it is a new event.
231 if isNewEvent:
232 eventData['program'] = newEventData['program']
233 eventData['recurringId'] = recurringSeriesId
234 eventData['multipleOfferingId'] = multipleSeriesId
235 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
236 eventRecord = Event.create(**eventData)
237 else:
238 eventRecord = Event.get_by_id(newEventData['id'])
239 Event.update(**eventData).where(Event.id == eventRecord).execute()
241 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
242 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
244 eventRecords.append(eventRecord)
245 return eventRecords
247def getStudentLedEvents(term):
248 studentLedEvents = list(Event.select(Event, Program)
249 .join(Program)
250 .where(Program.isStudentLed,
251 Event.term == term, Event.deletionDate == None)
252 .order_by(Event.startDate, Event.timeStart)
253 .execute())
255 programs = {}
257 for event in studentLedEvents:
258 programs.setdefault(event.program, []).append(event)
260 return programs
262def getUpcomingStudentLedCount(term, currentTime):
263 """
264 Return a count of all upcoming events for each student led program.
265 """
267 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
268 .join(Event, on=(Program.id == Event.program_id))
269 .where(Program.isStudentLed,
270 Event.term == term, Event.deletionDate == None,
271 (Event.startDate > currentTime) | ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime)),
272 Event.isCanceled == False)
273 .group_by(Program.id))
275 programCountDict = {}
277 for programCount in upcomingCount:
278 programCountDict[programCount.id] = programCount.eventCount
279 return programCountDict
281def getTrainingEvents(term, user):
282 """
283 The allTrainingsEvent query is designed to select and count eventId's after grouping them
284 together by id's of similiar value. The query will then return the event that is associated
285 with the most programs (highest count) by doing this we can ensure that the event being
286 returned is the All Trainings Event.
287 term: expected to be the ID of a term
288 user: expected to be the current user
289 return: a list of all trainings the user can view
290 """
291 trainingQuery = (Event.select(Event).distinct()
292 .join(Program, JOIN.LEFT_OUTER)
293 .where(Event.isTraining == True,
294 Event.term == term, Event.deletionDate == None)
295 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
297 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
298 if hideBonner:
299 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
301 return list(trainingQuery.execute())
303def getBonnerEvents(term):
304 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id"))
305 .join(Program)
306 .where(Program.isBonnerScholars,
307 Event.term == term, Event.deletionDate == None)
308 .order_by(Event.startDate, Event.timeStart)
309 .execute())
310 return bonnerScholarsEvents
312def getOtherEvents(term):
313 """
315 Get the list of the events not caught by other functions to be displayed in
316 the Other Events section of the Events List page.
317 :return: A list of Other Event objects
318 """
319 # Gets all events that are not associated with a program and are not trainings
320 # Gets all events that have a program but don't fit anywhere
322 otherEvents = list(Event.select(Event, Program)
323 .join(Program, JOIN.LEFT_OUTER)
324 .where(Event.term == term, Event.deletionDate == None,
325 Event.isTraining == False,
326 Event.isAllVolunteerTraining == False,
327 ((Program.isOtherCeltsSponsored) |
328 ((Program.isStudentLed == False) &
329 (Program.isBonnerScholars == False))))
330 .order_by(Event.startDate, Event.timeStart, Event.id)
331 .execute())
333 return otherEvents
335def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
336 """
337 Get the list of upcoming events that the user is interested in as long
338 as they are not banned from the program that the event is a part of.
339 :param user: a username or User object
340 :param asOf: The date to use when determining future and past events.
341 Used in testing, defaults to the current timestamp.
342 :return: A list of Event objects
343 """
345 events = (Event.select().distinct()
346 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
347 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
348 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
349 .where(Event.deletionDate == None, Event.startDate >= asOf,
350 (Interest.user == user) | (EventRsvp.user == user),
351 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
353 if program:
354 events = events.where(Event.program == program)
356 events = events.order_by(Event.startDate, Event.timeStart)
358 eventsList = []
359 shownRecurringEventList = []
360 shownMultipleOfferingEventList = []
362 # removes all recurring events except for the next upcoming one
363 for event in events:
364 if event.recurringId or event.multipleOfferingId:
365 if not event.isCanceled:
366 if event.recurringId not in shownRecurringEventList:
367 eventsList.append(event)
368 shownRecurringEventList.append(event.recurringId)
369 if event.multipleOfferingId not in shownMultipleOfferingEventList:
370 eventsList.append(event)
371 shownMultipleOfferingEventList.append(event.multipleOfferingId)
372 else:
373 if not event.isCanceled:
374 eventsList.append(event)
376 return eventsList
378def getParticipatedEventsForUser(user):
379 """
380 Get all the events a user has participated in.
381 :param user: a username or User object
382 :param asOf: The date to use when determining future and past events.
383 Used in testing, defaults to the current timestamp.
384 :return: A list of Event objects
385 """
387 participatedEvents = (Event.select(Event, Program.programName)
388 .join(Program, JOIN.LEFT_OUTER).switch()
389 .join(EventParticipant)
390 .where(EventParticipant.user == user,
391 Event.isAllVolunteerTraining == False)
392 .order_by(Event.startDate, Event.name))
394 allVolunteer = (Event.select(Event, "")
395 .join(EventParticipant)
396 .where(Event.isAllVolunteerTraining == True,
397 EventParticipant.user == user))
398 union = participatedEvents.union_all(allVolunteer)
399 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())
401 return unionParticipationWithVolunteer
403def validateNewEventData(data):
404 """
405 Confirm that the provided data is valid for an event.
407 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
409 Returns 3 values: (boolean success, the validation error message, the data object)
410 """
412 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRecurring'], data['isMultipleOffering']]:
413 return (False, "Raw form data passed to validate method. Preprocess first.")
415 if data['isRecurring'] and data['endDate'] < data['startDate']:
416 return (False, "Event start date is after event end date.")
418 if data['timeEnd'] <= data['timeStart']:
419 return (False, "Event end time must be after start time.")
421 # Validation if we are inserting a new event
422 if 'id' not in data:
424 sameEventList = list((Event.select().where((Event.name == data['name']) &
425 (Event.location == data['location']) &
426 (Event.startDate == data['startDate']) &
427 (Event.timeStart == data['timeStart'])).execute()))
429 sameEventListCopy = sameEventList.copy()
431 for event in sameEventListCopy:
432 if event.isCanceled or event.recurringId:
433 sameEventList.remove(event)
435 try:
436 Term.get_by_id(data['term'])
437 except DoesNotExist as e:
438 return (False, f"Not a valid term: {data['term']}")
439 if sameEventList:
440 return (False, "This event already exists")
442 data['valid'] = True
443 return (True, "All inputs are valid.")
445def calculateNewrecurringId():
446 """
447 Gets the highest recurring Id so that a new recurring Id can be assigned
448 """
449 recurringId = Event.select(fn.MAX(Event.recurringId)).scalar()
450 if recurringId:
451 return recurringId + 1
452 else:
453 return 1
454def calculateNewMultipleOfferingId():
455 """
456 Gets the highest recurring Id so that a new recurring Id can be assigned
457 """
458 multipleOfferingId = Event.select(fn.MAX(Event.multipleOfferingId)).scalar()
459 if multipleOfferingId:
460 return multipleOfferingId + 1
461 else:
462 return 1
464def getPreviousRecurringEventData(recurringId):
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
467 """
468 previousEventVolunteers = (User.select(User).distinct()
469 .join(EventParticipant)
470 .join(Event)
471 .where(Event.recurringId==recurringId))
472 return previousEventVolunteers
474def getPreviousMultipleOfferingEventData(multipleOfferingId):
475 """
476 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
477 """
478 previousEventVolunteers = (User.select(User).distinct()
479 .join(EventParticipant)
480 .join(Event)
481 .where(Event.multipleOfferingId == multipleOfferingId))
482 return previousEventVolunteers
484def getRecurringEventsData(eventData):
485 """
486 Calculate the events to create based on a recurring event start and end date. Takes a
487 dictionary of event data.
489 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
491 Return a list of events to create from the event data.
492 """
493 if not isinstance(eventData['endDate'], date) or not isinstance(eventData['startDate'], date):
494 raise Exception("startDate and endDate must be datetime.date objects.")
496 if eventData['endDate'] == eventData['startDate']:
497 raise Exception("This event is not a recurring event")
499 return [ {'name': f"{eventData['name']} Week {counter+1}",
500 'date': eventData['startDate'] + timedelta(days=7*counter),
501 "week": counter+1}
502 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
504def preprocessEventData(eventData):
505 """
506 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
508 - dates should exist and be date objects if there is a value
509 - checkboxes should be True or False
510 - if term is given, convert it to a model object
511 - times should exist be strings in 24 hour format example: 14:40
512 - multipleOfferingData should be a JSON string
513 - Look up matching certification requirement if necessary
514 """
515 ## Process checkboxes
516 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRecurring', 'isMultipleOffering', 'isAllVolunteerTraining']
518 for checkBox in eventCheckBoxes:
519 if checkBox not in eventData:
520 eventData[checkBox] = False
521 else:
522 eventData[checkBox] = bool(eventData[checkBox])
524 ## Process dates
525 eventDates = ['startDate', 'endDate']
526 for eventDate in eventDates:
527 if eventDate not in eventData: # There is no date given
528 eventData[eventDate] = ''
529 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
530 eventData[eventDate] = parser.parse(eventData[eventDate])
531 elif not isinstance(eventData[eventDate], date): # The date is not a date object
532 eventData[eventDate] = ''
534 # Process multipleOfferingData
535 if 'multipleOfferingData' not in eventData:
536 eventData['multipleOfferingData'] = json.dumps([])
537 elif type(eventData['multipleOfferingData']) is str:
538 try:
539 multipleOfferingData = json.loads(eventData['multipleOfferingData'])
540 eventData['multipleOfferingData'] = multipleOfferingData
541 if type(multipleOfferingData) != list:
542 eventData['multipleOfferingData'] = json.dumps([])
543 except json.decoder.JSONDecodeError as e:
544 eventData['multipleOfferingData'] = json.dumps([])
545 if type(eventData['multipleOfferingData']) is list:
546 # validate the list data. Make sure there is 'eventName', 'startDate', 'timeStart', 'timeEnd', and 'isDuplicate' data
547 multipleOfferingData = eventData['multipleOfferingData']
548 for offeringDatum in multipleOfferingData:
549 for attribute in ['eventName', 'startDate', 'timeStart', 'timeEnd']:
550 if type(offeringDatum.get(attribute)) != str:
551 offeringDatum[attribute] = ''
552 if type(offeringDatum.get('isDuplicate')) != bool:
553 offeringDatum['isDuplicate'] = False
555 eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData'])
557 # Process terms
558 if 'term' in eventData:
559 try:
560 eventData['term'] = Term.get_by_id(eventData['term'])
561 except DoesNotExist:
562 eventData['term'] = ''
564 # Process requirement
565 if 'certRequirement' in eventData:
566 try:
567 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
568 except DoesNotExist:
569 eventData['certRequirement'] = ''
570 elif 'id' in eventData:
571 # look up requirement
572 match = RequirementMatch.get_or_none(event=eventData['id'])
573 if match:
574 eventData['certRequirement'] = match.requirement
575 if 'timeStart' in eventData:
576 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
578 if 'timeEnd' in eventData:
579 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
581 return eventData
583def getTomorrowsEvents():
584 """Grabs each event that occurs tomorrow"""
585 tomorrowDate = date.today() + timedelta(days=1)
586 events = list(Event.select().where(Event.startDate==tomorrowDate))
587 return events
589def addEventView(viewer,event):
590 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
591 if not viewer.isCeltsAdmin:
592 EventView.get_or_create(user = viewer, event = event)
594def getEventRsvpCountsForTerm(term):
595 """
596 Get all of the RSVPs for the events that exist in the term.
597 Returns a dictionary with the event id as the key and the amount of
598 current RSVPs to that event as the pair.
599 """
600 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
601 .join(EventRsvp, JOIN.LEFT_OUTER)
602 .where(Event.term == term, Event.deletionDate == None)
603 .group_by(Event.id))
605 amountAsDict = {event.id: event.count for event in amount}
607 return amountAsDict
609def getEventRsvpCount(eventId):
610 """
611 Returns the number of RSVP'd participants for a given eventId.
612 """
613 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
615def getCountdownToEvent(event, *, currentDatetime=None):
616 """
617 Given an event, this function returns a string that conveys the amount of time left
618 until the start of the event.
620 Note about dates:
621 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
622 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
623 tomorrow with no mention of the hour.
624 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
625 and hours in actual time.
627 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
628 relative to this morning and exclude all hours and minutes
630 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
631 the real difference in days and hours without the aforementioned simplifying language.
632 """
634 if currentDatetime is None:
635 currentDatetime = datetime.now().replace(second=0, microsecond=0)
636 currentMorning = currentDatetime.replace(hour=0, minute=0)
638 eventStart = datetime.combine(event.startDate, event.timeStart)
639 eventEnd = datetime.combine(event.startDate, event.timeEnd)
641 if eventEnd < currentDatetime:
642 return "Already passed"
643 elif eventStart <= currentDatetime <= eventEnd:
644 return "Happening now"
646 timeUntilEvent = relativedelta(eventStart, currentDatetime)
647 calendarDelta = relativedelta(eventStart, currentMorning)
648 calendarYearsUntilEvent = calendarDelta.years
649 calendarMonthsUntilEvent = calendarDelta.months
650 calendarDaysUntilEvent = calendarDelta.days
652 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
653 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
654 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
655 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
656 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
658 # Years until
659 if calendarYearsUntilEvent:
660 if calendarMonthsUntilEvent:
661 return f"{yearString} and {monthString}"
662 return f"{yearString}"
663 # Months until
664 if calendarMonthsUntilEvent:
665 if calendarDaysUntilEvent:
666 return f"{monthString} and {dayString}"
667 return f"{monthString}"
668 # Days until
669 if calendarDaysUntilEvent:
670 if eventStart.time() < currentDatetime.time():
671 if calendarDaysUntilEvent == 1:
672 return "Tomorrow"
673 return f"{dayString}"
674 if timeUntilEvent.hours:
675 return f"{dayString} and {hourString}"
676 return f"{dayString}"
677 # Hours until
678 if timeUntilEvent.hours:
679 if timeUntilEvent.minutes:
680 return f"{hourString} and {minuteString}"
681 return f"{hourString}"
682 # Minutes until
683 elif timeUntilEvent.minutes > 1:
684 return f"{minuteString}"
685 # Seconds until
686 return "<1 minute"
688def copyRsvpToNewEvent(priorEvent, newEvent):
689 """
690 Copies rvsps from priorEvent to newEvent
691 """
692 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
694 for student in rsvpInfo:
695 newRsvp = EventRsvp(
696 user = student.user,
697 event = newEvent,
698 rsvpWaitlist = student.rsvpWaitlist
699 )
700 newRsvp.save()
701 numRsvps = len(rsvpInfo)
702 if numRsvps:
703 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")