Coverage for app/logic/events.py: 93%
288 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-07-29 13:28 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-07-29 13:28 +0000
1from flask import url_for
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
7from app.models import mainDB
8from app.models.user import User
9from app.models.event import Event
10from app.models.eventParticipant import EventParticipant
11from app.models.program import Program
12from app.models.term import Term
13from app.models.programBan import ProgramBan
14from app.models.interest import Interest
15from app.models.eventRsvp import EventRsvp
16from app.models.requirementMatch import RequirementMatch
17from app.models.certificationRequirement import CertificationRequirement
18from app.models.eventViews import EventView
20from app.logic.createLogs import createActivityLog, createRsvpLog
21from app.logic.utils import format24HourTime
22from app.logic.fileHandler import FileHandler
23from app.logic.certification import updateCertRequirementForEvent
25def cancelEvent(eventId):
26 """
27 :param eventId : Integer used to find specific event to be canceled
29 """
30 event = Event.get_or_none(Event.id == eventId)
32 if event:
33 event.isCanceled = True
34 event.save()
36 program = event.program
37 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')}.")
40def deleteEvent(eventId):
41 """
42 :param eventId : Integer used to find specific event to be deleted.
44 Deletes an event, if it is a recurring event, rename all following events
45 to make sure there is no gap in weeks.
46 """
47 event = Event.get_or_none(Event.id == eventId)
49 if event:
50 #Builds list of recurring events in a series
51 if event.recurringId:
52 recurringId = event.recurringId
53 recurringEvents = list(Event.select().where(Event.recurringId==recurringId).order_by(Event.id)) # orders for tests
54 eventDeleted = False
56 # once the deleted event is detected, change all other names to the previous event's name
57 for recurringEvent in recurringEvents:
58 if eventDeleted:
59 Event.update({Event.name:newEventName}).where(Event.id==recurringEvent.id).execute()
60 newEventName = recurringEvent.name
62 if recurringEvent == event:
63 newEventName = recurringEvent.name
64 eventDeleted = True
66 program = event.program
68 if program:
69 createActivityLog(f"Deleted \"{event.name}\" for {program.programName}, which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
70 else:
71 createActivityLog(f"Deleted a non-program event, \"{event.name}\", which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.")
73 event.delete_instance(recursive = True, delete_nullable = True)
75def deleteEventAndAllFollowing(eventId):
76 """
77 :param eventId : Integer used to find the first event and following events to be deleted.
79 Deletes a recurring event and all the recurring events after it.
80 """
82 event = Event.get_or_none(Event.id == eventId)
83 if event:
84 #Makes a list of all instances of the recurring event and events AFTER its start date.
85 if event.recurringId:
86 recurringId = event.recurringId
87 recurringSeries = list(Event.select().where((Event.recurringId == recurringId) & (Event.startDate >= event.startDate)))
88 # Loops through list, deletes matching events in database.
89 for seriesEvent in recurringSeries:
90 seriesEvent.delete_instance(recursive = True)
92def deleteAllRecurringEvents(eventId):
93 """
94 :param eventId : Integer used to find all recurring events in a series to be deleted.
96 Deletes all recurring events in a series.
97 """
98 event = Event.get_or_none(Event.id == eventId)
99 if event:
100 if event.recurringId:
101 #Makes of list of all recurring events in a series
102 recurringId = event.recurringId
103 allRecurringEvents = list(Event.select().where(Event.recurringId == recurringId))
104 #Deletes the events from the data base
105 for aRecurringEvent in allRecurringEvents:
106 aRecurringEvent.delete_instance(recursive = True)
109def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
110 """
111 :param eventData: Dictionary that holds data related to the event.
112 :param attachmentFiles: files to be attached to event.
113 :param bool renewedEvent: Checks if the event has passed and is being reused.
115 Tries to save an event to the database:
116 Checks that the event data is valid, if it is, it continues to save the new
117 event to the database and adds files if there are any.
119 :returns: Created events OR an error message.
120 """
122 if eventData["rsvpLimit"] == "":
123 eventData["rsvpLimit"] = None
124 #preprocess data to match data with proper values
125 newEventData = preprocessEventData(eventData)
127 isValid, validationErrorMessage = validateNewEventData(newEventData)
129 if not isValid:
130 return False, validationErrorMessage
131 try:
132 events = saveEventToDb(newEventData, renewedEvent)
133 if attachmentFiles:
134 for event in events:
135 addFile = FileHandler(attachmentFiles, eventId=event.id)
136 addFile.saveFiles(saveOriginalFile=events[0])
137 return events, ""
138 except Exception as e:
139 print(f'Failed attemptSaveEvent() with Exception: {e}')
140 return False, e
142def saveEventToDb(newEventData, renewedEvent = False):
143 """
144 :param dict newEventData: Dictionary containing event info (name, dates etc)
145 :param bool renewedEvent: Deterimines if the event is being renewed from an existing event
147 Takes in a preprocessed dictionary (newEventData)
148 and adds it to the data base, checking to see it is
149 reoccuring, and if its a new event.
151 :return eventRecords: dictionary containing event data
152 """
154 if not newEventData.get('valid', False) and not renewedEvent:
155 raise Exception("Unvalidated data passed to saveEventToDb")
157 isNewEvent = ('id' not in newEventData)
159 eventsToCreate = []
160 recurringSeriesId = None
162 if (isNewEvent and newEventData['isRecurring']) and not renewedEvent:
164 eventsToCreate = calculateRecurringEventFrequency(newEventData)
166 recurringSeriesId = calculateNewrecurringId()
167 else:
168 eventsToCreate.append({'name': f"{newEventData['name']}",
169 'date':newEventData['startDate'],
170 "week":1})
171 if renewedEvent:
172 recurringSeriesId = newEventData.get('recurringId')
173 eventRecords = []
175 #loops based on how many times an event happens
176 #Executes once if not recurring
178 for eventInstance in eventsToCreate:
179 with mainDB.atomic():
181 eventData = {
182 "term": newEventData['term'],
183 "name": eventInstance['name'],
184 "description": newEventData['description'],
185 "timeStart": newEventData['timeStart'],
186 "timeEnd": newEventData['timeEnd'],
187 "location": newEventData['location'],
188 "isFoodProvided" : newEventData['isFoodProvided'],
189 "isTraining": newEventData['isTraining'],
190 "isRsvpRequired": newEventData['isRsvpRequired'],
191 "isService": newEventData['isService'],
192 "startDate": eventInstance['date'],
193 "rsvpLimit": newEventData['rsvpLimit'],
194 "endDate": eventInstance['date'],
195 "contactEmail": newEventData['contactEmail'],
196 "contactName": newEventData['contactName']
197 }
199 # The three fields below are only relevant during event creation so we only set/change them when
200 # it is a new event.
201 if isNewEvent:
202 eventData['program'] = newEventData['program']
203 eventData['recurringId'] = recurringSeriesId
204 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
205 #copy of eventData dictionary containing updated program, recurringId, and volunteer check
206 eventRecord = Event.create(**eventData)
207 else:
208 #for accessing event to be editted (this is if it already exists)
209 eventRecord = Event.get_by_id(newEventData['id'])
210 Event.update(**eventData).where(Event.id == eventRecord).execute()
212 #If event requires certification, add that certification to eventRecord
213 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
214 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
216 eventRecords.append(eventRecord)
217 return eventRecords
219def getStudentLedEvents(term):
220 """
221 :param term: Object that gives a range of time (fall 2024, spring 2024..)
222 as an integer, 1 being the oldest term in DB.
224 :returns programs: dictionary containing all student led Events for given term
225 and which programs they belong to.
226 """
227 #lists program IDs with their corresponding events saved in the DB sorted by date and time
228 #student led programs included Berea Buddies(program 2) and adopt a grandparent (program 3)
229 studentLedEvents = list(Event.select(Event, Program)
230 .join(Program)
231 .where(Program.isStudentLed,
232 Event.term == term)
233 .order_by(Event.startDate, Event.timeStart)
234 .execute())
236 programs = {}
238 #loops through list of student led events and adds them to their proper program categories
239 for event in studentLedEvents:
240 programs.setdefault(event.program, []).append(event)
241 return programs
243def getUpcomingStudentLedCount(term, currentTime):
244 """
245 :param term: Object that gives a range of time (fall 2024, spring 2024..)
246 as an integer ID, 1 being the oldest term in DB.
248 :param currentTime: Takes in the current time
250 Return a count of all upcoming events for each student led program.
252 :return: programCountDict, a list of key value pairs
253 key = programID, value = number of events for that key(Program) in a given term
254 """
255 #Gets all student led events in DB whose start time is >= current time(Get)
256 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
257 .join(Event, on=(Program.id == Event.program_id))
258 .where(Program.isStudentLed,
259 Event.term == term,
260 (Event.endDate > currentTime) | ((Event.endDate == currentTime) & (Event.timeEnd >= currentTime)),
261 Event.isCanceled == False)
262 .group_by(Program.id))
264 programCountDict = {}
266 for programCount in upcomingCount:
267 programCountDict[programCount.id] = programCount.eventCount
268 return programCountDict
270def getTrainingEvents(term, user):
271 """
272 :param term: Object that gives a range of time (fall 2024, spring 2024..)
273 as an integer ID, 1 being the oldest term in DB.
275 :param user: Expected to be the current user
277 The allTrainingsEvent query is designed to select and count eventId's after grouping them
278 together by events of similiar type.
280 return: a list of all trainings the user can view
281 """
282 #Get all the train events for term in question
283 trainingQuery = (Event.select(Event).distinct()
284 .join(Program, JOIN.LEFT_OUTER)
285 .where(Event.isTraining == True,
286 Event.term == term)
287 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
288 #Hide Bonner Scholar sect. if user IS NOT ADMIN OR BONNER SCHOLAR
289 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
290 if hideBonner:
291 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
293 return list(trainingQuery.execute())
295def getBonnerEvents(term):
296 """
297 :param term: Object that gives a range of time (fall 2024, spring 2024..)
298 as an integer ID, 1 being the oldest term in DB.
300 :returns bonnerScholarsEvents: dictionary containing all Bonner related events for given term
301 """
302 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id"))
303 .join(Program)
304 .where(Program.isBonnerScholars,
305 Event.term == term)
306 .order_by(Event.startDate, Event.timeStart)
307 .execute())
308 return bonnerScholarsEvents
310def getOtherEvents(term):
311 """
312 :param term: Object that gives a range of time (fall 2024, spring 2024..)
313 as an integer ID, 1 being the oldest term in DB.
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.
318 :return: A list of Other Event objects
319 """
320 otherEvents = list(Event.select(Event, Program)
321 .join(Program, JOIN.LEFT_OUTER)
322 .where(Event.term == term,
323 Event.isTraining == False,
324 Event.isAllVolunteerTraining == False,
325 ((Program.isOtherCeltsSponsored) |
326 ((Program.isStudentLed == False) &
327 (Program.isBonnerScholars == False))))
328 .order_by(Event.startDate, Event.timeStart, Event.id)
329 .execute())
331 return otherEvents
333def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
334 """
335 :param user: Expected to be the current user
336 :param asOf: The date to use when determining future and past events.
337 Used in testing, defaults to the current timestamp.
338 :param program: What type of program the event is.
340 Get the list of upcoming events that the user is interested in as long
341 as they are not banned from the program that the event is a part of.
343 :return: A list of Event objects
344 """
345 #Get its all events from DB that user is not banned from
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.startDate >= asOf,
351 (Interest.user == user) | (EventRsvp.user == user),
352 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
353 #set events to programs that the user is interested in, then ordered by closest start date.
354 if program:
355 events = events.where(Event.program == program)
356 events = events.order_by(Event.startDate, Event.name)
358 events_list = []
359 shown_recurring_event_list = []
361 # removes all recurring events except for the next upcoming one
362 for event in events:
363 if event.recurringId:
364 if not event.isCanceled:
365 if event.recurringId not in shown_recurring_event_list:
366 events_list.append(event)
367 shown_recurring_event_list.append(event.recurringId)
368 else:
369 if not event.isCanceled:
370 events_list.append(event)
372 return events_list
374def getParticipatedEventsForUser(user):
375 """
376 :param user: Expected to be the current user.
378 :return: A list of Event objects
379 """
380 #Get all events that the user has participated in
381 participatedEvents = (Event.select(Event, Program.programName)
382 .join(Program, JOIN.LEFT_OUTER).switch()
383 .join(EventParticipant)
384 .where(EventParticipant.user == user,
385 Event.isAllVolunteerTraining == False)
386 .order_by(Event.startDate, Event.name))
387 #Get all volunteer events the user has taken part in
388 allVolunteer = (Event.select(Event, "")
389 .join(EventParticipant)
390 .where(Event.isAllVolunteerTraining == True,
391 EventParticipant.user == user))
392 union = participatedEvents.union_all(allVolunteer)
393 #list of events separated by ID and whether they are volunteer events or not.
394 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())
395 return unionParticipationWithVolunteer
397def validateNewEventData(data):
398 """
399 :param data: Dictionary containing information pertaining to an event
401 Confirm that the provided data is valid for an event.
403 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
405 :return: Boolean success, the validation error message.
406 """
408 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isService'], data['isRecurring']]:
409 return (False, "Raw form data passed to validate method. Preprocess first.")
411 if data['isRecurring'] and data['endDate'] < data['startDate']:
412 return (False, "Event start date is after event end date.")
414 if data['timeEnd'] <= data['timeStart']:
415 return (False, "Event end time must be after start time.")
417 # Validation if we are inserting a new event
418 if 'id' not in data:
420 sameEventList = list((Event.select().where((Event.name == data['name']) &
421 (Event.location == data['location']) &
422 (Event.startDate == data['startDate']) &
423 (Event.timeStart == data['timeStart'])).execute()))
425 sameEventListCopy = sameEventList.copy()
427 #removes canceled/recurring events from sameEventList
428 for event in sameEventListCopy:
429 if event.isCanceled or event.recurringId:
430 sameEventList.remove(event)
432 try:
433 Term.get_by_id(data['term'])
434 except DoesNotExist as e:
435 return (False, f"Not a valid term: {data['term']}")
436 if sameEventList:
437 return (False, "This event already exists")
439 data['valid'] = True
440 return (True, "All inputs are valid.")
442def calculateNewrecurringId():
443 """
444 Gets the highest recurring Id so that a new recurring Id can be assigned
445 if the event is recurring
447 :return: integer based on the amount of events in a recurring series
448 (used to ID recurring events)
449 """
450 recurringId = Event.select(fn.MAX(Event.recurringId)).scalar()
451 if recurringId:
452 return recurringId + 1
453 else:
454 return 1
456def getPreviousRecurringEventData(recurringId):
457 """
458 :param recurringId: Id for a specific recurring event series.
460 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
462 :return previousEventVolunteers: key value pairs containing users that have participated in past events
463 within a same recurring series
464 """
466 previousEventVolunteers = (User.select(User).distinct()
467 .join(EventParticipant)
468 .join(Event)
469 .where(Event.recurringId==recurringId))
470 return previousEventVolunteers
472def calculateRecurringEventFrequency(event):
473 """
474 :param event: Event object in question.
476 Calculate the events to create based on a recurring event start and end date.
477 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
479 :Return: a list of events to create from the event data.
480 """
481 if not isinstance(event['endDate'], date) or not isinstance(event['startDate'], date):
482 raise Exception("startDate and endDate must be datetime.date objects.")
484 if event['endDate'] == event['startDate']:
485 raise Exception("This event is not a recurring event")
486 return [ {'name': f"{event['name']} Week {counter+1}",
487 'date': event['startDate'] + timedelta(days=7*counter),
488 "week": counter+1}
489 for counter in range(0, ((event['endDate']-event['startDate']).days//7)+1)]
491def preprocessEventData(eventData):
492 """
493 :param eventData: Data of an event from the database.
495 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
497 - dates should exist and be date objects if there is a value
498 - checkboxes should be True or False
499 - if term is given, convert it to a model object
500 - times should exist be strings in 24 hour format example: 14:40
501 - Look up matching certification requirement if necessary
503 :return eventData: Dictionary of data for the event after being processed
504 """
505 ## Process checkboxes
506 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isRecurring', 'isAllVolunteerTraining']
508 for checkBox in eventCheckBoxes:
509 if checkBox not in eventData:
510 eventData[checkBox] = False
511 else:
512 eventData[checkBox] = bool(eventData[checkBox])
514 ## Process dates
515 eventDates = ['startDate', 'endDate']
516 for eventDate in eventDates:
517 if eventDate not in eventData:
518 eventData[eventDate] = ''
519 elif type(eventData[eventDate]) is str and eventData[eventDate]:
520 eventData[eventDate] = parser.parse(eventData[eventDate])
521 elif not isinstance(eventData[eventDate], date):
522 eventData[eventDate] = ''
524 # If we aren't recurring, all of our events are single-day
525 if not eventData['isRecurring']:
526 eventData['endDate'] = eventData['startDate']
528 # Process terms
529 if 'term' in eventData:
530 try:
531 eventData['term'] = Term.get_by_id(eventData['term'])
532 except DoesNotExist:
533 eventData['term'] = ''
535 # Process requirement
536 if 'certRequirement' in eventData:
537 try:
538 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
539 except DoesNotExist:
540 eventData['certRequirement'] = ''
541 elif 'id' in eventData:
542 # look up requirement
543 match = RequirementMatch.get_or_none(event=eventData['id'])
544 if match:
545 eventData['certRequirement'] = match.requirement
546 #changes event start and end times to matach a 24hr format
547 if 'timeStart' in eventData:
548 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
550 if 'timeEnd' in eventData:
551 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
553 return eventData
555def getTomorrowsEvents():
556 """
557 :return events: List of event objects that occur tomorrow.
558 """
559 tomorrowDate = date.today() + timedelta(days=1)
560 events = list(Event.select().where(Event.startDate==tomorrowDate))
561 return events
563def addEventView(viewer,event):
564 """
565 :param viewer: User that is looking at specified event
567 :param event: Event object in question.
569 This checks if the current user already viewed the event. If not, insert a record to EventView table.
570 """
571 if not viewer.isCeltsAdmin:
572 EventView.get_or_create(user = viewer, event = event)
574def getEventRsvpCountsForTerm(term):
575 """
576 :param term: Object that gives a range of time (fall 2024, spring 2024..)
577 as an integer ID, 1 being the oldest term in DB.
579 Get all of the RSVPs for the events that exist in the term.
581 :return amountAsDict: dictionary with the event id as the key and the amount of
582 current RSVPs to that event as the pair.
583 """
584 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
585 .join(EventRsvp, JOIN.LEFT_OUTER)
586 .where(Event.term == term)
587 .group_by(Event.id))
589 amountAsDict = {event.id: event.count for event in amount}
591 return amountAsDict
593def getEventRsvpCount(eventId):
594 """
595 :param eventId : Integer used to track specific event.
597 :return: the number of RSVP'd participants for a given eventId.
598 """
599 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
601def getCountdownToEvent(event, *, currentDatetime=None):
602 """
603 :param event: Event object in question.
605 :param *: Must specify currentDatetime in parameters, when called
606 this would look like: getCountdownToEvent(event, currentDatetime=currentTime)
608 :param currentDatetime: Current time
610 Note about dates:
611 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
612 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
613 tomorrow with no mention of the hour.
614 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
615 and hours in actual time.
617 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
618 relative to this morning and exclude all hours and minutes
620 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
621 the real difference in days and hours without the aforementioned simplifying language.
623 :return: A string that conveys the amount of time left until the start of the event.
624 """
626 #If currentDatetime is None, get current time
627 if currentDatetime is None:
628 currentDatetime = datetime.now().replace(second=0, microsecond=0)
629 currentMorning = currentDatetime.replace(hour=0, minute=0)
630 #formats how times look into variables
631 eventStart = datetime.combine(event.startDate, event.timeStart)
632 eventEnd = datetime.combine(event.endDate, event.timeEnd)
634 if eventEnd < currentDatetime:
635 return "Already passed"
636 elif eventStart <= currentDatetime <= eventEnd:
637 return "Happening now"
639 #assgins remaining days (years, months, days) and remaining time until the event starts
640 timeUntilEvent = relativedelta(eventStart, currentDatetime)
641 calendarDelta = relativedelta(eventStart, currentMorning)
642 calendarYearsUntilEvent = calendarDelta.years
643 calendarMonthsUntilEvent = calendarDelta.months
644 calendarDaysUntilEvent = calendarDelta.days
646 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
647 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
648 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
649 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
650 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
652 # Years until
653 if calendarYearsUntilEvent:
654 if calendarMonthsUntilEvent:
655 return f"{yearString} and {monthString}"
656 return f"{yearString}"
657 # Months until
658 if calendarMonthsUntilEvent:
659 if calendarDaysUntilEvent:
660 return f"{monthString} and {dayString}"
661 return f"{monthString}"
662 # Days until
663 if calendarDaysUntilEvent:
664 if eventStart.time() < currentDatetime.time():
665 if calendarDaysUntilEvent == 1:
666 return "Tomorrow"
667 return f"{dayString}"
668 if timeUntilEvent.hours:
669 return f"{dayString} and {hourString}"
670 return f"{dayString}"
671 # Hours until
672 if timeUntilEvent.hours:
673 if timeUntilEvent.minutes:
674 return f"{hourString} and {minuteString}"
675 return f"{hourString}"
676 # Minutes until
677 elif timeUntilEvent.minutes > 1:
678 return f"{minuteString}"
679 # Seconds until
680 return "<1 minute"
682def copyRsvpToNewEvent(priorEvent, newEvent):
683 """
684 :param priorEvent: Dictionary data from a prior event
686 :param newEvent: Dictionary data from a new event
688 Copies rvsps from priorEvent to newEvent and saves it to the DB
689 """
690 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
692 for student in rsvpInfo:
693 newRsvp = EventRsvp(
694 user = student.user,
695 event = newEvent,
696 rsvpWaitlist = student.rsvpWaitlist
697 )
698 newRsvp.save()
699 numRsvps = len(rsvpInfo)
700 if numRsvps:
701 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")