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