Coverage for app/logic/events.py: 93%
346 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-09-18 19:56 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-09-18 19:56 +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 "isRsvpRequired": newEventData['isRsvpRequired'],
222 "isService": newEventData['isService'],
223 "startDate": eventInstance['date'],
224 "rsvpLimit": newEventData['rsvpLimit'],
225 "endDate": eventInstance['date'],
226 "contactEmail": newEventData['contactEmail'],
227 "contactName": newEventData['contactName']
228 }
230 # The three fields below are only relevant during event creation so we only set/change them when
231 # it is a new event.
232 if isNewEvent:
233 eventData['program'] = newEventData['program']
234 eventData['recurringId'] = recurringSeriesId
235 eventData['multipleOfferingId'] = multipleSeriesId
236 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
237 eventRecord = Event.create(**eventData)
238 else:
239 eventRecord = Event.get_by_id(newEventData['id'])
240 Event.update(**eventData).where(Event.id == eventRecord).execute()
242 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
243 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
245 eventRecords.append(eventRecord)
246 return eventRecords
248def getStudentLedEvents(term):
249 studentLedEvents = list(Event.select(Event, Program)
250 .join(Program)
251 .where(Program.isStudentLed,
252 Event.term == term, Event.deletionDate == None)
253 .order_by(Event.startDate, Event.timeStart)
254 .execute())
256 programs = {}
258 for event in studentLedEvents:
259 programs.setdefault(event.program, []).append(event)
261 return programs
263def getUpcomingStudentLedCount(term, currentTime):
264 """
265 Return a count of all upcoming events for each student led program.
266 """
268 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
269 .join(Event, on=(Program.id == Event.program_id))
270 .where(Program.isStudentLed,
271 Event.term == term, Event.deletionDate == None,
272 (Event.endDate > currentTime) | ((Event.endDate == currentTime) & (Event.timeEnd >= currentTime)),
273 Event.isCanceled == False)
274 .group_by(Program.id))
276 programCountDict = {}
278 for programCount in upcomingCount:
279 programCountDict[programCount.id] = programCount.eventCount
280 return programCountDict
282def getTrainingEvents(term, user):
283 """
284 The allTrainingsEvent query is designed to select and count eventId's after grouping them
285 together by id's of similiar value. The query will then return the event that is associated
286 with the most programs (highest count) by doing this we can ensure that the event being
287 returned is the All Trainings Event.
288 term: expected to be the ID of a term
289 user: expected to be the current user
290 return: a list of all trainings the user can view
291 """
292 trainingQuery = (Event.select(Event).distinct()
293 .join(Program, JOIN.LEFT_OUTER)
294 .where(Event.isTraining == True,
295 Event.term == term, Event.deletionDate == None)
296 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
298 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
299 if hideBonner:
300 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
302 return list(trainingQuery.execute())
304def getBonnerEvents(term):
305 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id"))
306 .join(Program)
307 .where(Program.isBonnerScholars,
308 Event.term == term, Event.deletionDate == None)
309 .order_by(Event.startDate, Event.timeStart)
310 .execute())
311 return bonnerScholarsEvents
313def getOtherEvents(term):
314 """
316 Get the list of the events not caught by other functions to be displayed in
317 the Other Events section of the Events List page.
318 :return: A list of Other Event objects
319 """
320 # Gets all events that are not associated with a program and are not trainings
321 # Gets all events that have a program but don't fit anywhere
323 otherEvents = list(Event.select(Event, Program)
324 .join(Program, JOIN.LEFT_OUTER)
325 .where(Event.term == term, Event.deletionDate == None,
326 Event.isTraining == False,
327 Event.isAllVolunteerTraining == False,
328 ((Program.isOtherCeltsSponsored) |
329 ((Program.isStudentLed == False) &
330 (Program.isBonnerScholars == False))))
331 .order_by(Event.startDate, Event.timeStart, Event.id)
332 .execute())
334 return otherEvents
336def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
337 """
338 Get the list of upcoming events that the user is interested in as long
339 as they are not banned from the program that the event is a part of.
340 :param user: a username or User object
341 :param asOf: The date to use when determining future and past events.
342 Used in testing, defaults to the current timestamp.
343 :return: A list of Event objects
344 """
346 events = (Event.select().distinct()
347 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
348 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
349 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
350 .where(Event.deletionDate == None, Event.startDate >= asOf,
351 (Interest.user == user) | (EventRsvp.user == user),
352 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
354 if program:
355 events = events.where(Event.program == program)
357 events = events.order_by(Event.startDate, Event.timeStart)
359 eventsList = []
360 shownRecurringEventList = []
361 shownMultipleOfferingEventList = []
363 # removes all recurring events except for the next upcoming one
364 for event in events:
365 if event.recurringId or event.multipleOfferingId:
366 if not event.isCanceled:
367 if event.recurringId not in shownRecurringEventList:
368 eventsList.append(event)
369 shownRecurringEventList.append(event.recurringId)
370 if event.multipleOfferingId not in shownMultipleOfferingEventList:
371 eventsList.append(event)
372 shownMultipleOfferingEventList.append(event.multipleOfferingId)
373 else:
374 if not event.isCanceled:
375 eventsList.append(event)
377 return eventsList
379def getParticipatedEventsForUser(user):
380 """
381 Get all the events a user has participated in.
382 :param user: a username or User object
383 :param asOf: The date to use when determining future and past events.
384 Used in testing, defaults to the current timestamp.
385 :return: A list of Event objects
386 """
388 participatedEvents = (Event.select(Event, Program.programName)
389 .join(Program, JOIN.LEFT_OUTER).switch()
390 .join(EventParticipant)
391 .where(EventParticipant.user == user,
392 Event.isAllVolunteerTraining == False)
393 .order_by(Event.startDate, Event.name))
395 allVolunteer = (Event.select(Event, "")
396 .join(EventParticipant)
397 .where(Event.isAllVolunteerTraining == True,
398 EventParticipant.user == user))
399 union = participatedEvents.union_all(allVolunteer)
400 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())
402 return unionParticipationWithVolunteer
404def validateNewEventData(data):
405 """
406 Confirm that the provided data is valid for an event.
408 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
410 Returns 3 values: (boolean success, the validation error message, the data object)
411 """
413 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isService'], data['isRecurring'], data['isMultipleOffering']]:
414 return (False, "Raw form data passed to validate method. Preprocess first.")
416 if data['isRecurring'] and data['endDate'] < data['startDate']:
417 return (False, "Event start date is after event end date.")
419 if data['timeEnd'] <= data['timeStart']:
420 return (False, "Event end time must be after start time.")
422 # Validation if we are inserting a new event
423 if 'id' not in data:
425 sameEventList = list((Event.select().where((Event.name == data['name']) &
426 (Event.location == data['location']) &
427 (Event.startDate == data['startDate']) &
428 (Event.timeStart == data['timeStart'])).execute()))
430 sameEventListCopy = sameEventList.copy()
432 for event in sameEventListCopy:
433 if event.isCanceled or event.recurringId:
434 sameEventList.remove(event)
436 try:
437 Term.get_by_id(data['term'])
438 except DoesNotExist as e:
439 return (False, f"Not a valid term: {data['term']}")
440 if sameEventList:
441 return (False, "This event already exists")
443 data['valid'] = True
444 return (True, "All inputs are valid.")
446def calculateNewrecurringId():
447 """
448 Gets the highest recurring Id so that a new recurring Id can be assigned
449 """
450 recurringId = Event.select(fn.MAX(Event.recurringId)).scalar()
451 if recurringId:
452 return recurringId + 1
453 else:
454 return 1
455def calculateNewMultipleOfferingId():
456 """
457 Gets the highest recurring Id so that a new recurring Id can be assigned
458 """
459 multipleOfferingId = Event.select(fn.MAX(Event.multipleOfferingId)).scalar()
460 if multipleOfferingId:
461 return multipleOfferingId + 1
462 else:
463 return 1
465def getPreviousRecurringEventData(recurringId):
466 """
467 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
468 """
469 previousEventVolunteers = (User.select(User).distinct()
470 .join(EventParticipant)
471 .join(Event)
472 .where(Event.recurringId==recurringId))
473 return previousEventVolunteers
475def getPreviousMultipleOfferingEventData(multipleOfferingId):
476 """
477 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
478 """
479 previousEventVolunteers = (User.select(User).distinct()
480 .join(EventParticipant)
481 .join(Event)
482 .where(Event.multipleOfferingId == multipleOfferingId))
483 return previousEventVolunteers
485def getRecurringEventsData(eventData):
486 """
487 Calculate the events to create based on a recurring event start and end date. Takes a
488 dictionary of event data.
490 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
492 Return a list of events to create from the event data.
493 """
494 if not isinstance(eventData['endDate'], date) or not isinstance(eventData['startDate'], date):
495 raise Exception("startDate and endDate must be datetime.date objects.")
497 if eventData['endDate'] == eventData['startDate']:
498 raise Exception("This event is not a recurring event")
500 return [ {'name': f"{eventData['name']} Week {counter+1}",
501 'date': eventData['startDate'] + timedelta(days=7*counter),
502 "week": counter+1}
503 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
505def preprocessEventData(eventData):
506 """
507 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
509 - dates should exist and be date objects if there is a value
510 - checkboxes should be True or False
511 - if term is given, convert it to a model object
512 - times should exist be strings in 24 hour format example: 14:40
513 - multipleOfferingData should be a JSON string
514 - Look up matching certification requirement if necessary
515 """
516 ## Process checkboxes
517 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isRecurring', 'isMultipleOffering', 'isAllVolunteerTraining']
519 for checkBox in eventCheckBoxes:
520 if checkBox not in eventData:
521 eventData[checkBox] = False
522 else:
523 eventData[checkBox] = bool(eventData[checkBox])
525 ## Process dates
526 eventDates = ['startDate', 'endDate']
527 for eventDate in eventDates:
528 if eventDate not in eventData: # There is no date given
529 eventData[eventDate] = ''
530 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
531 eventData[eventDate] = parser.parse(eventData[eventDate])
532 elif not isinstance(eventData[eventDate], date): # The date is not a date object
533 eventData[eventDate] = ''
535 # If we aren't recurring, all of our events are single-day or mutliple offerings, which also have the same start and end date
536 if not eventData['isRecurring']:
537 eventData['endDate'] = eventData['startDate']
539 # Process multipleOfferingData
540 if 'multipleOfferingData' not in eventData:
541 eventData['multipleOfferingData'] = json.dumps([])
542 elif type(eventData['multipleOfferingData']) is str:
543 try:
544 multipleOfferingData = json.loads(eventData['multipleOfferingData'])
545 eventData['multipleOfferingData'] = multipleOfferingData
546 if type(multipleOfferingData) != list:
547 eventData['multipleOfferingData'] = json.dumps([])
548 except json.decoder.JSONDecodeError as e:
549 eventData['multipleOfferingData'] = json.dumps([])
550 if type(eventData['multipleOfferingData']) is list:
551 # validate the list data. Make sure there is 'eventName', 'startDate', 'timeStart', 'timeEnd', and 'isDuplicate' data
552 multipleOfferingData = eventData['multipleOfferingData']
553 for offeringDatum in multipleOfferingData:
554 for attribute in ['eventName', 'startDate', 'timeStart', 'timeEnd']:
555 if type(offeringDatum.get(attribute)) != str:
556 offeringDatum[attribute] = ''
557 if type(offeringDatum.get('isDuplicate')) != bool:
558 offeringDatum['isDuplicate'] = False
560 eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData'])
562 # Process terms
563 if 'term' in eventData:
564 try:
565 eventData['term'] = Term.get_by_id(eventData['term'])
566 except DoesNotExist:
567 eventData['term'] = ''
569 # Process requirement
570 if 'certRequirement' in eventData:
571 try:
572 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
573 except DoesNotExist:
574 eventData['certRequirement'] = ''
575 elif 'id' in eventData:
576 # look up requirement
577 match = RequirementMatch.get_or_none(event=eventData['id'])
578 if match:
579 eventData['certRequirement'] = match.requirement
580 if 'timeStart' in eventData:
581 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
583 if 'timeEnd' in eventData:
584 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
586 return eventData
588def getTomorrowsEvents():
589 """Grabs each event that occurs tomorrow"""
590 tomorrowDate = date.today() + timedelta(days=1)
591 events = list(Event.select().where(Event.startDate==tomorrowDate))
592 return events
594def addEventView(viewer,event):
595 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
596 if not viewer.isCeltsAdmin:
597 EventView.get_or_create(user = viewer, event = event)
599def getEventRsvpCountsForTerm(term):
600 """
601 Get all of the RSVPs for the events that exist in the term.
602 Returns a dictionary with the event id as the key and the amount of
603 current RSVPs to that event as the pair.
604 """
605 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
606 .join(EventRsvp, JOIN.LEFT_OUTER)
607 .where(Event.term == term, Event.deletionDate == None)
608 .group_by(Event.id))
610 amountAsDict = {event.id: event.count for event in amount}
612 return amountAsDict
614def getEventRsvpCount(eventId):
615 """
616 Returns the number of RSVP'd participants for a given eventId.
617 """
618 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
620def getCountdownToEvent(event, *, currentDatetime=None):
621 """
622 Given an event, this function returns a string that conveys the amount of time left
623 until the start of the event.
625 Note about dates:
626 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
627 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
628 tomorrow with no mention of the hour.
629 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
630 and hours in actual time.
632 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
633 relative to this morning and exclude all hours and minutes
635 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
636 the real difference in days and hours without the aforementioned simplifying language.
637 """
639 if currentDatetime is None:
640 currentDatetime = datetime.now().replace(second=0, microsecond=0)
641 currentMorning = currentDatetime.replace(hour=0, minute=0)
643 eventStart = datetime.combine(event.startDate, event.timeStart)
644 eventEnd = datetime.combine(event.endDate, event.timeEnd)
646 if eventEnd < currentDatetime:
647 return "Already passed"
648 elif eventStart <= currentDatetime <= eventEnd:
649 return "Happening now"
651 timeUntilEvent = relativedelta(eventStart, currentDatetime)
652 calendarDelta = relativedelta(eventStart, currentMorning)
653 calendarYearsUntilEvent = calendarDelta.years
654 calendarMonthsUntilEvent = calendarDelta.months
655 calendarDaysUntilEvent = calendarDelta.days
657 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
658 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
659 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
660 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
661 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
663 # Years until
664 if calendarYearsUntilEvent:
665 if calendarMonthsUntilEvent:
666 return f"{yearString} and {monthString}"
667 return f"{yearString}"
668 # Months until
669 if calendarMonthsUntilEvent:
670 if calendarDaysUntilEvent:
671 return f"{monthString} and {dayString}"
672 return f"{monthString}"
673 # Days until
674 if calendarDaysUntilEvent:
675 if eventStart.time() < currentDatetime.time():
676 if calendarDaysUntilEvent == 1:
677 return "Tomorrow"
678 return f"{dayString}"
679 if timeUntilEvent.hours:
680 return f"{dayString} and {hourString}"
681 return f"{dayString}"
682 # Hours until
683 if timeUntilEvent.hours:
684 if timeUntilEvent.minutes:
685 return f"{hourString} and {minuteString}"
686 return f"{hourString}"
687 # Minutes until
688 elif timeUntilEvent.minutes > 1:
689 return f"{minuteString}"
690 # Seconds until
691 return "<1 minute"
693def copyRsvpToNewEvent(priorEvent, newEvent):
694 """
695 Copies rvsps from priorEvent to newEvent
696 """
697 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
699 for student in rsvpInfo:
700 newRsvp = EventRsvp(
701 user = student.user,
702 event = newEvent,
703 rsvpWaitlist = student.rsvpWaitlist
704 )
705 newRsvp.save()
706 numRsvps = len(rsvpInfo)
707 if numRsvps:
708 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")