Coverage for app/logic/events.py: 90%
313 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-29 20:22 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-29 20:22 +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')}.")
38#NEEDS FIXING: process not working properly for repeating events when two events are deleted consecutively
39def deleteEvent(eventId):
40 """
41 Deletes an event, if it is a repeating event, rename all following events
42 to make sure there is no gap in weeks.
44 """
45 event = Event.get_or_none(Event.id == eventId)
47 if event:
48 if event.isRepeating:
49 seriesId = event.seriesId
50 repeatingEvents = list(Event.select().where(Event.seriesId==seriesId).order_by(Event.id)) # orders for tests
51 eventDeleted = False
52 # once the deleted event is detected, change all other names to the previous event's name
53 for repeatingEvent in repeatingEvents:
54 if eventDeleted:
55 Event.update({Event.name:newEventName}).where(Event.id==repeatingEvent.id).execute()
56 newEventName = repeatingEvent.name
58 if repeatingEvent == event:
59 newEventName = repeatingEvent.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()
71def deleteEventAndAllFollowing(eventId):
72 """
73 Deletes an event in the series and all events after it
75 """
76 event = Event.get_or_none(Event.id == eventId)
77 if event:
78 if event.seriesId:
79 seriesId = event.seriesId
80 eventSeries = list(Event.select(Event.id).where((Event.seriesId == seriesId) & (Event.startDate >= event.startDate)))
81 deletedEventList = [event.id for event in eventSeries]
82 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where((Event.seriesId == seriesId) & (Event.startDate >= event.startDate)).execute()
83 return deletedEventList
85def deleteAllEventsInSeries(eventId):
86 """
87 Deletes all events in a series by getting the first event in the series and calling deleteEventAndAllFollowing().
89 """
90 event = Event.get_or_none(Event.id == eventId)
91 if event:
92 if event.seriesId:
93 seriesId = event.seriesId
94 allSeriesEvents = list(Event.select(Event.id).where(Event.seriesId == seriesId).order_by(Event.startDate))
95 eventId = allSeriesEvents[0].id
96 return deleteEventAndAllFollowing(eventId)
97 else:
98 raise ValueError(f"Event with id {event.id} does not belong to a series (seriesId is None).")
100def attemptSaveMultipleOfferings(eventData, attachmentFiles = None):
101 """
102 Tries to save an event with multiple offerings to the database:
103 Creates separate event data inheriting from the original eventData
104 with the specifics of each offering.
105 Calls attemptSaveEvent on each of the newly created datum
106 If any data is not valid it will return a validation error.
108 Returns:
109 allSavesWereSuccessful : bool | Whether or not all offering saves were successful
110 savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesWereSuccessful is False then this list will be empty.
111 failedSavedOfferings : List[(int, str), ...] | Tuples containing the indicies of failed saved offerings and the associated validation error message.
112 """
113 savedOfferings = []
114 failedSavedOfferings = []
115 allSavesWereSuccessful = True
117 seriesId = calculateNewSeriesId()
119 # Create separate event data inheriting from the original eventData
120 seriesData = eventData.get('seriesData')
121 isRepeating = bool(eventData.get('isRepeating'))
122 with mainDB.atomic() as transaction:
123 for index, event in enumerate(seriesData):
124 eventInfo = eventData.copy()
125 eventInfo.update({
126 'name': event['eventName'],
127 'startDate': event['eventDate'],
128 'timeStart': event['startTime'],
129 'timeEnd': event['endTime'],
130 'seriesId': seriesId,
131 'isRepeating': bool(isRepeating)
132 })
133 # Try to save each offering
134 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles)
135 if validationErrorMessage:
136 failedSavedOfferings.append((index, validationErrorMessage))
137 allSavesWereSuccessful = False
138 else:
139 savedEvent = savedEvents[0]
140 savedOfferings.append(savedEvent)
141 if not allSavesWereSuccessful:
142 savedOfferings = []
143 transaction.rollback()
145 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings
148def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
149 """
150 Tries to save an event to the database:
151 Checks that the event data is valid and if it is, it continues to save the new
152 event to the database and adds files if there are any.
153 If it is not valid it will return a validation error.
155 Returns:
156 The saved event, created events and an error message if an error occurred.
157 """
159 # Manually set the value of RSVP Limit if it is and empty string since it is
160 # automatically changed from "" to 0
161 if eventData["rsvpLimit"] == "":
162 eventData["rsvpLimit"] = None
164 newEventData = preprocessEventData(eventData)
166 isValid, validationErrorMessage = validateNewEventData(newEventData)
167 if not isValid:
168 return [], validationErrorMessage
170 events = saveEventToDb(newEventData, renewedEvent)
171 if attachmentFiles:
172 for event in events:
173 addFile = FileHandler(attachmentFiles, eventId=event.id)
174 addFile.saveFiles(saveOriginalFile=events[0])
175 return events, ""
178def saveEventToDb(newEventData, renewedEvent = False):
180 if not newEventData.get('valid', False) and not renewedEvent:
181 raise Exception("Unvalidated data passed to saveEventToDb")
183 isNewEvent = ('id' not in newEventData)
185 eventRecords = []
186 with mainDB.atomic():
188 eventData = {
189 "term": newEventData['term'],
190 "name": newEventData['name'],
191 "description": newEventData['description'],
192 "timeStart": newEventData['timeStart'],
193 "timeEnd": newEventData['timeEnd'],
194 "location": newEventData['location'],
195 "isFoodProvided" : newEventData['isFoodProvided'],
196 "isTraining": newEventData['isTraining'],
197 "isEngagement": newEventData['isEngagement'],
198 "isRsvpRequired": newEventData['isRsvpRequired'],
199 "isService": newEventData['isService'],
200 "startDate": newEventData['startDate'],
201 "rsvpLimit": newEventData['rsvpLimit'],
202 "contactEmail": newEventData['contactEmail'],
203 "contactName": newEventData['contactName']
204 }
206 # The three fields below are only relevant during event creation so we only set/change them when
207 # it is a new event.
208 if isNewEvent:
209 eventData['program'] = newEventData['program']
210 eventData['seriesId'] = newEventData.get('seriesId')
211 eventData['isRepeating'] = bool(newEventData.get('isRepeating'))
212 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining']
213 eventRecord = Event.create(**eventData)
214 else:
215 eventRecord = Event.get_by_id(newEventData['id'])
216 Event.update(**eventData).where(Event.id == eventRecord).execute()
218 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
219 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
221 eventRecords.append(eventRecord)
222 return eventRecords
224def getStudentLedEvents(term):
225 studentLedEvents = list(Event.select(Event, Program)
226 .join(Program)
227 .where(Program.isStudentLed,
228 Event.term == term, Event.deletionDate == None)
229 .order_by(Event.startDate, Event.timeStart)
230 .execute())
232 programs = {}
234 for event in studentLedEvents:
235 programs.setdefault(event.program, []).append(event)
237 return programs
239def getUpcomingStudentLedCount(term, currentTime):
240 """
241 Return a count of all upcoming events for each student led program.
242 """
244 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
245 .join(Event, on=(Program.id == Event.program_id))
246 .where(Program.isStudentLed,
247 Event.term == term, Event.deletionDate == None,
248 (Event.startDate > currentTime) | ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime)),
249 Event.isCanceled == False)
250 .group_by(Program.id))
252 programCountDict = {}
254 for programCount in upcomingCount:
255 programCountDict[programCount.id] = programCount.eventCount
256 return programCountDict
258def getTrainingEvents(term, user):
259 """
260 The allTrainingsEvent query is designed to select and count eventId's after grouping them
261 together by id's of similiar value. The query will then return the event that is associated
262 with the most programs (highest count) by doing this we can ensure that the event being
263 returned is the All Trainings Event.
264 term: expected to be the ID of a term
265 user: expected to be the current user
266 return: a list of all trainings the user can view
267 """
268 trainingQuery = (Event.select(Event).distinct()
269 .join(Program, JOIN.LEFT_OUTER)
270 .where(Event.isTraining == True,
271 Event.term == term, Event.deletionDate == None)
272 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart))
274 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
275 if hideBonner:
276 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
278 return list(trainingQuery.execute())
280def getBonnerEvents(term):
281 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id"))
282 .join(Program)
283 .where(Program.isBonnerScholars,
284 Event.term == term, Event.deletionDate == None)
285 .order_by(Event.startDate, Event.timeStart)
286 .execute())
287 return bonnerScholarsEvents
289def getOtherEvents(term):
290 """
292 Get the list of the events not caught by other functions to be displayed in
293 the Other Events section of the Events List page.
294 :return: A list of Other Event objects
295 """
296 # Gets all events that are not associated with a program and are not trainings
297 # Gets all events that have a program but don't fit anywhere
299 otherEvents = list(Event.select(Event, Program)
300 .join(Program, JOIN.LEFT_OUTER)
301 .where(Event.term == term, Event.deletionDate == None,
302 Event.isTraining == False,
303 Event.isAllVolunteerTraining == False,
304 ((Program.isOtherCeltsSponsored) |
305 ((Program.isStudentLed == False) &
306 (Program.isBonnerScholars == False))))
307 .order_by(Event.startDate, Event.timeStart, Event.id)
308 .execute())
310 return otherEvents
312def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):
313 """
314 Get the list of upcoming events that the user is interested in as long
315 as they are not banned from the program that the event is a part of.
316 :param user: a username or User object
317 :param asOf: The date to use when determining future and past events.
318 Used in testing, defaults to the current timestamp.
319 :return: A list of Event objects
320 """
322 events = (Event.select().distinct()
323 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user)))
324 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program))
325 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event))
326 .where(Event.deletionDate == None, Event.startDate >= asOf,
327 (Interest.user == user) | (EventRsvp.user == user),
328 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf)))
330 if program:
331 events = events.where(Event.program == program)
333 events = events.order_by(Event.startDate, Event.timeStart)
335 eventsList = []
336 seriesEventsList = []
338 # removes all events in series except for the next upcoming one
339 for event in events:
340 if event.seriesId:
341 if not event.isCanceled:
342 if event.seriesId not in seriesEventsList:
343 eventsList.append(event)
344 seriesEventsList.append(event.seriesId)
345 else:
346 if not event.isCanceled:
347 eventsList.append(event)
349 return eventsList
351def getParticipatedEventsForUser(user):
352 """
353 Get all the events a user has participated in.
354 :param user: a username or User object
355 :param asOf: The date to use when determining future and past events.
356 Used in testing, defaults to the current timestamp.
357 :return: A list of Event objects
358 """
360 participatedEvents = (Event.select(Event, Program.programName)
361 .join(Program, JOIN.LEFT_OUTER).switch()
362 .join(EventParticipant)
363 .where(EventParticipant.user == user,
364 Event.isAllVolunteerTraining == False)
365 .order_by(Event.startDate, Event.name))
367 allVolunteer = (Event.select(Event, "")
368 .join(EventParticipant)
369 .where(Event.isAllVolunteerTraining == True,
370 EventParticipant.user == user))
371 union = participatedEvents.union_all(allVolunteer)
372 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())
374 return unionParticipationWithVolunteer
376def validateNewEventData(data):
377 """
378 Confirm that the provided data is valid for an event.
380 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
382 Returns 3 values: (boolean success, the validation error message, the data object)
383 """
385 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating']]:
386 return (False, "Raw form data passed to validate method. Preprocess first.")
388 if data['timeEnd'] <= data['timeStart']:
389 return (False, "Event end time must be after start time.")
391 # Validation if we are inserting a new event
392 if 'id' not in data:
394 sameEventList = list((Event.select().where((Event.name == data['name']) &
395 (Event.location == data['location']) &
396 (Event.startDate == data['startDate']) &
397 (Event.timeStart == data['timeStart'])).execute()))
399 sameEventListCopy = sameEventList.copy()
401 for event in sameEventListCopy:
402 if event.isCanceled or (event.seriesId and event.isRepeating):
403 sameEventList.remove(event)
405 try:
406 Term.get_by_id(data['term'])
407 except DoesNotExist as e:
408 return (False, f"Not a valid term: {data['term']}")
409 if sameEventList:
410 return (False, "This event already exists")
412 data['valid'] = True
413 return (True, "All inputs are valid.")
415def calculateNewSeriesId():
416 """
417 Gets the max series ID so that new seriesId can be assigned.
418 """
419 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar()
420 if maxSeriesId:
421 return maxSeriesId + 1
422 return 1
424def getPreviousSeriesEventData(seriesId):
425 """
426 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.
428 """
429 previousEventVolunteers = (User.select(User).distinct()
430 .join(EventParticipant)
431 .join(Event)
432 .where(Event.seriesId==seriesId))
433 return previousEventVolunteers
435def getRepeatingEventsData(eventData):
436 """
437 Calculate the events to create based on a repeating event start and end date. Takes a
438 dictionary of event data.
440 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
442 Return a list of events to create from the event data.
443 """
445 return [ {'name': f"{eventData['name']} Week {counter+1}",
446 'date': eventData['startDate'] + timedelta(days=7*counter),
447 "week": counter+1}
448 for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]
450def preprocessEventData(eventData):
451 """
452 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
454 - dates should exist and be date objects if there is a value
455 - checkboxes should be True or False
456 - if term is given, convert it to a model object
457 - times should exist be strings in 24 hour format example: 14:40
458 - seriesData should be a JSON string
459 - Look up matching certification requirement if necessary
460 """
461 ## Process checkboxes
462 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining']
464 for checkBox in eventCheckBoxes:
465 if checkBox not in eventData:
466 eventData[checkBox] = False
467 else:
468 eventData[checkBox] = bool(eventData[checkBox])
470 ## Process dates
471 eventDates = ['startDate', 'endDate']
472 for eventDate in eventDates:
473 if eventDate not in eventData: # There is no date given
474 eventData[eventDate] = ''
475 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
476 eventData[eventDate] = parser.parse(eventData[eventDate])
477 elif not isinstance(eventData[eventDate], date): # The date is not a date object
478 eventData[eventDate] = ''
480 # Process multipleOfferingData
481 if 'multipleOfferingData' not in eventData:
482 eventData['multipleOfferingData'] = json.dumps([])
483 elif type(eventData['multipleOfferingData']) is str:
484 try:
485 multipleOfferingData = json.loads(eventData['multipleOfferingData'])
486 eventData['multipleOfferingData'] = multipleOfferingData
487 if type(multipleOfferingData) != list:
488 eventData['multipleOfferingData'] = json.dumps([])
489 except json.decoder.JSONDecodeError as e:
490 eventData['multipleOfferingData'] = json.dumps([])
491 if type(eventData['multipleOfferingData']) is list:
492 # validate the list data. Make sure there is 'eventName', 'startDate', 'timeStart', 'timeEnd', and 'isDuplicate' data
493 multipleOfferingData = eventData['multipleOfferingData']
494 for offeringDatum in multipleOfferingData:
495 for attribute in ['eventName', 'startDate', 'timeStart', 'timeEnd']:
496 if type(offeringDatum.get(attribute)) != str:
497 offeringDatum[attribute] = ''
498 if type(offeringDatum.get('isDuplicate')) != bool:
499 offeringDatum['isDuplicate'] = False
501 eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData'])
503 # Process seriesData
504 if 'seriesData' not in eventData:
505 eventData['seriesData'] = json.dumps([])
507 # Process terms
508 if 'term' in eventData:
509 try:
510 eventData['term'] = Term.get_by_id(eventData['term'])
511 except DoesNotExist:
512 eventData['term'] = ''
514 # Process requirement
515 if 'certRequirement' in eventData:
516 try:
517 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement'])
518 except DoesNotExist:
519 eventData['certRequirement'] = ''
520 elif 'id' in eventData:
521 # look up requirement
522 match = RequirementMatch.get_or_none(event=eventData['id'])
523 if match:
524 eventData['certRequirement'] = match.requirement
525 if 'timeStart' in eventData:
526 eventData['timeStart'] = format24HourTime(eventData['timeStart'])
528 if 'timeEnd' in eventData:
529 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
531 return eventData
533def getTomorrowsEvents():
534 """Grabs each event that occurs tomorrow"""
535 tomorrowDate = date.today() + timedelta(days=1)
536 events = list(Event.select().where(Event.startDate==tomorrowDate))
537 return events
539def addEventView(viewer,event):
540 """This checks if the current user already viewed the event. If not, insert a recored to EventView table"""
541 if not viewer.isCeltsAdmin:
542 EventView.get_or_create(user = viewer, event = event)
544def getEventRsvpCountsForTerm(term):
545 """
546 Get all of the RSVPs for the events that exist in the term.
547 Returns a dictionary with the event id as the key and the amount of
548 current RSVPs to that event as the pair.
549 """
550 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count'))
551 .join(EventRsvp, JOIN.LEFT_OUTER)
552 .where(Event.term == term, Event.deletionDate == None)
553 .group_by(Event.id))
555 amountAsDict = {event.id: event.count for event in amount}
557 return amountAsDict
559def getEventRsvpCount(eventId):
560 """
561 Returns the number of RSVP'd participants for a given eventId.
562 """
563 return len(EventRsvp.select().where(EventRsvp.event_id == eventId))
565def getCountdownToEvent(event, *, currentDatetime=None):
566 """
567 Given an event, this function returns a string that conveys the amount of time left
568 until the start of the event.
570 Note about dates:
571 Natural language is unintuitive. There are two major rules that govern how we discuss dates.
572 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens
573 tomorrow with no mention of the hour.
574 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days
575 and hours in actual time.
577 E.g. if the current time of day is greater than the event start's time of day, we give a number of days
578 relative to this morning and exclude all hours and minutes
580 On the other hand, if the current time of day is less or equal to the event's start of day we can produce
581 the real difference in days and hours without the aforementioned simplifying language.
582 """
584 if currentDatetime is None:
585 currentDatetime = datetime.now().replace(second=0, microsecond=0)
586 currentMorning = currentDatetime.replace(hour=0, minute=0)
588 eventStart = datetime.combine(event.startDate, event.timeStart)
589 eventEnd = datetime.combine(event.startDate, event.timeEnd)
591 if eventEnd < currentDatetime:
592 return "Already passed"
593 elif eventStart <= currentDatetime <= eventEnd:
594 return "Happening now"
596 timeUntilEvent = relativedelta(eventStart, currentDatetime)
597 calendarDelta = relativedelta(eventStart, currentMorning)
598 calendarYearsUntilEvent = calendarDelta.years
599 calendarMonthsUntilEvent = calendarDelta.months
600 calendarDaysUntilEvent = calendarDelta.days
602 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}"
603 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}"
604 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}"
605 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}"
606 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}"
608 # Years until
609 if calendarYearsUntilEvent:
610 if calendarMonthsUntilEvent:
611 return f"{yearString} and {monthString}"
612 return f"{yearString}"
613 # Months until
614 if calendarMonthsUntilEvent:
615 if calendarDaysUntilEvent:
616 return f"{monthString} and {dayString}"
617 return f"{monthString}"
618 # Days until
619 if calendarDaysUntilEvent:
620 if eventStart.time() < currentDatetime.time():
621 if calendarDaysUntilEvent == 1:
622 return "Tomorrow"
623 return f"{dayString}"
624 if timeUntilEvent.hours:
625 return f"{dayString} and {hourString}"
626 return f"{dayString}"
627 # Hours until
628 if timeUntilEvent.hours:
629 if timeUntilEvent.minutes:
630 return f"{hourString} and {minuteString}"
631 return f"{hourString}"
632 # Minutes until
633 elif timeUntilEvent.minutes > 1:
634 return f"{minuteString}"
635 # Seconds until
636 return "<1 minute"
638def copyRsvpToNewEvent(priorEvent, newEvent):
639 """
640 Copies rvsps from priorEvent to newEvent
641 """
642 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
644 for student in rsvpInfo:
645 newRsvp = EventRsvp(
646 user = student.user,
647 event = newEvent,
648 rsvpWaitlist = student.rsvpWaitlist
649 )
650 newRsvp.save()
651 numRsvps = len(rsvpInfo)
652 if numRsvps:
653 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")