Coverage for app/logic/events.py: 93%

306 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-09-06 19:35 +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 

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 

19 

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 

24 

25def cancelEvent(eventId): 

26 """ 

27 Cancels an event. 

28 """ 

29 event = Event.get_or_none(Event.id == eventId) 

30 if event: 

31 event.isCanceled = True 

32 event.save() 

33 

34 program = event.program 

35 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')}.") 

36 

37 

38def deleteEvent(eventId): 

39 """ 

40 Deletes an event, if it is a recurring event, rename all following events 

41 to make sure there is no gap in weeks. 

42 """ 

43 event = Event.get_or_none(Event.id == eventId) 

44 

45 if event: 

46 if event.recurringId: 

47 recurringId = event.recurringId 

48 recurringEvents = list(Event.select().where(Event.recurringId==recurringId).order_by(Event.id)) # orders for tests 

49 eventDeleted = False 

50 

51 # once the deleted event is detected, change all other names to the previous event's name 

52 for recurringEvent in recurringEvents: 

53 if eventDeleted: 

54 Event.update({Event.name:newEventName}).where(Event.id==recurringEvent.id).execute() 

55 newEventName = recurringEvent.name 

56 

57 if recurringEvent == event: 

58 newEventName = recurringEvent.name 

59 eventDeleted = True 

60 

61 program = event.program 

62 

63 if program: 

64 createActivityLog(f"Deleted \"{event.name}\" for {program.programName}, which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.") 

65 else: 

66 createActivityLog(f"Deleted a non-program event, \"{event.name}\", which had a start date of {datetime.strftime(event.startDate, '%m/%d/%Y')}.") 

67 

68 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where(Event.id == event.id).execute() 

69 

70 

71def deleteEventAndAllFollowing(eventId): 

72 """ 

73 Deletes a recurring event and all the recurring events after it. 

74 Modified to also apply to the case of events with multiple offerings 

75 """ 

76 event = Event.get_or_none(Event.id == eventId) 

77 if event: 

78 if event.recurringId: 

79 recurringId = event.recurringId 

80 recurringSeries = list(Event.select(Event.id).where((Event.recurringId == recurringId) & (Event.startDate >= event.startDate))) 

81 deletedEventList = [recurringEvent.id for recurringEvent in recurringSeries] 

82 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where((Event.recurringId == recurringId) & (Event.startDate >= event.startDate)).execute() 

83 return deletedEventList 

84 

85def deleteAllRecurringEvents(eventId): 

86 """ 

87 Deletes all recurring events. 

88 Modified to also apply for events with multiple offerings 

89 """ 

90 event = Event.get_or_none(Event.id == eventId) 

91 if event: 

92 if event.recurringId: 

93 recurringId = event.recurringId 

94 allRecurringEvents = list(Event.select(Event.id).where(Event.recurringId == recurringId).order_by(Event.startDate)) 

95 eventId = allRecurringEvents[0].id 

96 return deleteEventAndAllFollowing(eventId) 

97 

98 

99 

100def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): 

101 """ 

102 Tries to save an event to the database: 

103 Checks that the event data is valid and if it is, it continues to save the new 

104 event to the database and adds files if there are any. 

105 If it is not valid it will return a validation error. 

106 

107 Returns: 

108 Created events and an error message. 

109 """ 

110 

111 # Manually set the value of RSVP Limit if it is and empty string since it is 

112 # automatically changed from "" to 0 

113 if eventData["rsvpLimit"] == "": 

114 eventData["rsvpLimit"] = None 

115 

116 newEventData = preprocessEventData(eventData) 

117 

118 isValid, validationErrorMessage = validateNewEventData(newEventData) 

119 

120 if not isValid: 

121 return False, validationErrorMessage 

122 

123 try: 

124 events = saveEventToDb(newEventData, renewedEvent) 

125 if attachmentFiles: 

126 for event in events: 

127 addFile = FileHandler(attachmentFiles, eventId=event.id) 

128 addFile.saveFiles(saveOriginalFile=events[0]) 

129 return events, "" 

130 except Exception as e: 

131 print(f'Failed attemptSaveEvent() with Exception: {e}') 

132 return False, e 

133 

134def saveEventToDb(newEventData, renewedEvent = False): 

135 

136 if not newEventData.get('valid', False) and not renewedEvent: 

137 raise Exception("Unvalidated data passed to saveEventToDb") 

138 

139 

140 isNewEvent = ('id' not in newEventData) 

141 

142 

143 eventsToCreate = [] 

144 recurringSeriesId = None 

145 multipleSeriesId = None 

146 if (isNewEvent and newEventData['isRecurring']) and not renewedEvent: 

147 eventsToCreate = calculateRecurringEventFrequency(newEventData) 

148 recurringSeriesId = calculateNewrecurringId() 

149 

150 #temporarily applying the append for single events for now to tests  

151 elif(isNewEvent and newEventData['isMultipleOffering']) and not renewedEvent: 

152 eventsToCreate.append({'name': f"{newEventData['name']}", 

153 'date':newEventData['startDate'], 

154 "week":1}) 

155 multipleSeriesId = newEventData['multipleOfferingId'] 

156 

157 else: 

158 eventsToCreate.append({'name': f"{newEventData['name']}", 

159 'date':newEventData['startDate'], 

160 "week":1}) 

161 if renewedEvent: 

162 recurringSeriesId = newEventData.get('recurringId') 

163 eventRecords = [] 

164 for eventInstance in eventsToCreate: 

165 with mainDB.atomic(): 

166 

167 eventData = { 

168 "term": newEventData['term'], 

169 "name": eventInstance['name'], 

170 "description": newEventData['description'], 

171 "timeStart": newEventData['timeStart'], 

172 "timeEnd": newEventData['timeEnd'], 

173 "location": newEventData['location'], 

174 "isFoodProvided" : newEventData['isFoodProvided'], 

175 "isTraining": newEventData['isTraining'], 

176 "isRsvpRequired": newEventData['isRsvpRequired'], 

177 "isService": newEventData['isService'], 

178 "startDate": eventInstance['date'], 

179 "rsvpLimit": newEventData['rsvpLimit'], 

180 "endDate": eventInstance['date'], 

181 "contactEmail": newEventData['contactEmail'], 

182 "contactName": newEventData['contactName'] 

183 } 

184 

185 # The three fields below are only relevant during event creation so we only set/change them when  

186 # it is a new event.  

187 if isNewEvent: 

188 eventData['program'] = newEventData['program'] 

189 eventData['recurringId'] = recurringSeriesId 

190 eventData['multipleOfferingId'] = multipleSeriesId 

191 eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining'] 

192 eventRecord = Event.create(**eventData) 

193 else: 

194 eventRecord = Event.get_by_id(newEventData['id']) 

195 Event.update(**eventData).where(Event.id == eventRecord).execute() 

196 

197 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "": 

198 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement']) 

199 

200 eventRecords.append(eventRecord) 

201 return eventRecords 

202 

203def getStudentLedEvents(term): 

204 studentLedEvents = list(Event.select(Event, Program) 

205 .join(Program) 

206 .where(Program.isStudentLed, 

207 Event.term == term, Event.deletionDate == None) 

208 .order_by(Event.startDate, Event.timeStart) 

209 .execute()) 

210 

211 programs = {} 

212 

213 for event in studentLedEvents: 

214 programs.setdefault(event.program, []).append(event) 

215 

216 return programs 

217 

218def getUpcomingStudentLedCount(term, currentTime): 

219 """ 

220 Return a count of all upcoming events for each student led program. 

221 """ 

222 

223 upcomingCount = (Program.select(Program.id, fn.COUNT(Event.id).alias("eventCount")) 

224 .join(Event, on=(Program.id == Event.program_id)) 

225 .where(Program.isStudentLed, 

226 Event.term == term, Event.deletionDate == None, 

227 (Event.endDate > currentTime) | ((Event.endDate == currentTime) & (Event.timeEnd >= currentTime)), 

228 Event.isCanceled == False) 

229 .group_by(Program.id)) 

230 

231 programCountDict = {} 

232 

233 for programCount in upcomingCount: 

234 programCountDict[programCount.id] = programCount.eventCount 

235 return programCountDict 

236 

237def getTrainingEvents(term, user): 

238 """ 

239 The allTrainingsEvent query is designed to select and count eventId's after grouping them 

240 together by id's of similiar value. The query will then return the event that is associated 

241 with the most programs (highest count) by doing this we can ensure that the event being 

242 returned is the All Trainings Event. 

243 term: expected to be the ID of a term 

244 user: expected to be the current user 

245 return: a list of all trainings the user can view 

246 """ 

247 trainingQuery = (Event.select(Event).distinct() 

248 .join(Program, JOIN.LEFT_OUTER) 

249 .where(Event.isTraining == True, 

250 Event.term == term, Event.deletionDate == None) 

251 .order_by(Event.isAllVolunteerTraining.desc(), Event.startDate, Event.timeStart)) 

252 

253 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar) 

254 if hideBonner: 

255 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False) 

256 

257 return list(trainingQuery.execute()) 

258 

259def getBonnerEvents(term): 

260 bonnerScholarsEvents = list(Event.select(Event, Program.id.alias("program_id")) 

261 .join(Program) 

262 .where(Program.isBonnerScholars, 

263 Event.term == term, Event.deletionDate == None) 

264 .order_by(Event.startDate, Event.timeStart) 

265 .execute()) 

266 return bonnerScholarsEvents 

267 

268def getOtherEvents(term): 

269 """ 

270  

271 Get the list of the events not caught by other functions to be displayed in 

272 the Other Events section of the Events List page. 

273 :return: A list of Other Event objects 

274 """ 

275 # Gets all events that are not associated with a program and are not trainings 

276 # Gets all events that have a program but don't fit anywhere 

277 

278 otherEvents = list(Event.select(Event, Program) 

279 .join(Program, JOIN.LEFT_OUTER) 

280 .where(Event.term == term, Event.deletionDate == None, 

281 Event.isTraining == False, 

282 Event.isAllVolunteerTraining == False, 

283 ((Program.isOtherCeltsSponsored) | 

284 ((Program.isStudentLed == False) & 

285 (Program.isBonnerScholars == False)))) 

286 .order_by(Event.startDate, Event.timeStart, Event.id) 

287 .execute()) 

288 

289 return otherEvents 

290 

291def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None): 

292 """ 

293 Get the list of upcoming events that the user is interested in as long 

294 as they are not banned from the program that the event is a part of. 

295 :param user: a username or User object 

296 :param asOf: The date to use when determining future and past events. 

297 Used in testing, defaults to the current timestamp. 

298 :return: A list of Event objects 

299 """ 

300 

301 events = (Event.select().distinct() 

302 .join(ProgramBan, JOIN.LEFT_OUTER, on=((ProgramBan.program == Event.program) & (ProgramBan.user == user))) 

303 .join(Interest, JOIN.LEFT_OUTER, on=(Event.program == Interest.program)) 

304 .join(EventRsvp, JOIN.LEFT_OUTER, on=(Event.id == EventRsvp.event)) 

305 .where(Event.deletionDate == None, Event.startDate >= asOf, 

306 (Interest.user == user) | (EventRsvp.user == user), 

307 ProgramBan.user.is_null(True) | (ProgramBan.endDate < asOf))) 

308 

309 if program: 

310 events = events.where(Event.program == program) 

311 

312 events = events.order_by(Event.startDate, Event.timeStart) 

313 

314 events_list = [] 

315 shown_recurring_event_list = [] 

316 shown_multiple_offering_event_list = [] 

317 

318 # removes all recurring events except for the next upcoming one 

319 for event in events: 

320 if event.recurringId or event.multipleOfferingId: 

321 if not event.isCanceled: 

322 if event.recurringId not in shown_recurring_event_list: 

323 events_list.append(event) 

324 shown_recurring_event_list.append(event.recurringId) 

325 if event.multipleOfferingId not in shown_multiple_offering_event_list: 

326 events_list.append(event) 

327 shown_multiple_offering_event_list.append(event.multipleOfferingId) 

328 else: 

329 if not event.isCanceled: 

330 events_list.append(event) 

331 

332 return events_list 

333 

334def getParticipatedEventsForUser(user): 

335 """ 

336 Get all the events a user has participated in. 

337 :param user: a username or User object 

338 :param asOf: The date to use when determining future and past events. 

339 Used in testing, defaults to the current timestamp. 

340 :return: A list of Event objects 

341 """ 

342 

343 participatedEvents = (Event.select(Event, Program.programName) 

344 .join(Program, JOIN.LEFT_OUTER).switch() 

345 .join(EventParticipant) 

346 .where(EventParticipant.user == user, 

347 Event.isAllVolunteerTraining == False) 

348 .order_by(Event.startDate, Event.name)) 

349 

350 allVolunteer = (Event.select(Event, "") 

351 .join(EventParticipant) 

352 .where(Event.isAllVolunteerTraining == True, 

353 EventParticipant.user == user)) 

354 union = participatedEvents.union_all(allVolunteer) 

355 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()) 

356 

357 return unionParticipationWithVolunteer 

358 

359def validateNewEventData(data): 

360 """ 

361 Confirm that the provided data is valid for an event. 

362 

363 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data 

364 

365 Returns 3 values: (boolean success, the validation error message, the data object) 

366 """ 

367 

368 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isService'], data['isRecurring'], data['isMultipleOffering']]: 

369 return (False, "Raw form data passed to validate method. Preprocess first.") 

370 

371 if data['isRecurring'] and data['endDate'] < data['startDate']: 

372 return (False, "Event start date is after event end date.") 

373 

374 if data['timeEnd'] <= data['timeStart']: 

375 return (False, "Event end time must be after start time.") 

376 

377 # Validation if we are inserting a new event 

378 if 'id' not in data: 

379 

380 sameEventList = list((Event.select().where((Event.name == data['name']) & 

381 (Event.location == data['location']) & 

382 (Event.startDate == data['startDate']) & 

383 (Event.timeStart == data['timeStart'])).execute())) 

384 

385 sameEventListCopy = sameEventList.copy() 

386 

387 for event in sameEventListCopy: 

388 if event.isCanceled or event.recurringId: 

389 sameEventList.remove(event) 

390 

391 try: 

392 Term.get_by_id(data['term']) 

393 except DoesNotExist as e: 

394 return (False, f"Not a valid term: {data['term']}") 

395 if sameEventList: 

396 return (False, "This event already exists") 

397 

398 data['valid'] = True 

399 return (True, "All inputs are valid.") 

400 

401def calculateNewrecurringId(): 

402 """ 

403 Gets the highest recurring Id so that a new recurring Id can be assigned 

404 """ 

405 recurringId = Event.select(fn.MAX(Event.recurringId)).scalar() 

406 if recurringId: 

407 return recurringId + 1 

408 else: 

409 return 1 

410def calculateNewMultipleOfferingId(): 

411 """ 

412 Gets the highest recurring Id so that a new recurring Id can be assigned 

413 """ 

414 multipleOfferingId = Event.select(fn.MAX(Event.multipleOfferingId)).scalar() 

415 if multipleOfferingId: 

416 return multipleOfferingId + 1 

417 else: 

418 return 1 

419 

420def getPreviousRecurringEventData(recurringId): 

421 """ 

422 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 

423 """ 

424 previousEventVolunteers = (User.select(User).distinct() 

425 .join(EventParticipant) 

426 .join(Event) 

427 .where(Event.recurringId==recurringId)) 

428 return previousEventVolunteers 

429 

430def getPreviousMultipleOfferingEventData(multipleOfferingId): 

431 """ 

432 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 

433 """ 

434 previousEventVolunteers = (User.select(User).distinct() 

435 .join(EventParticipant) 

436 .join(Event) 

437 .where(Event.multipleOfferingId == multipleOfferingId)) 

438 return previousEventVolunteers 

439 

440def calculateRecurringEventFrequency(event): 

441 """ 

442 Calculate the events to create based on a recurring event start and end date. Takes a 

443 dictionary of event data. 

444 

445 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data. 

446 

447 Return a list of events to create from the event data. 

448 """ 

449 if not isinstance(event['endDate'], date) or not isinstance(event['startDate'], date): 

450 raise Exception("startDate and endDate must be datetime.date objects.") 

451 

452 if event['endDate'] == event['startDate']: 

453 raise Exception("This event is not a recurring event") 

454 

455 return [ {'name': f"{event['name']} Week {counter+1}", 

456 'date': event['startDate'] + timedelta(days=7*counter), 

457 "week": counter+1} 

458 for counter in range(0, ((event['endDate']-event['startDate']).days//7)+1)] 

459 

460def preprocessEventData(eventData): 

461 """ 

462 Ensures that the event data dictionary is consistent before it reaches the template or event logic. 

463 

464 - dates should exist and be date objects if there is a value 

465 - checkboxes should be True or False 

466 - if term is given, convert it to a model object 

467 - times should exist be strings in 24 hour format example: 14:40 

468 - Look up matching certification requirement if necessary 

469 """ 

470 ## Process checkboxes 

471 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isRecurring', 'isMultipleOffering', 'isAllVolunteerTraining'] 

472 

473 for checkBox in eventCheckBoxes: 

474 if checkBox not in eventData: 

475 eventData[checkBox] = False 

476 else: 

477 eventData[checkBox] = bool(eventData[checkBox]) 

478 

479 ## Process dates 

480 eventDates = ['startDate', 'endDate'] 

481 for eventDate in eventDates: 

482 if eventDate not in eventData: 

483 eventData[eventDate] = '' 

484 elif type(eventData[eventDate]) is str and eventData[eventDate]: 

485 eventData[eventDate] = parser.parse(eventData[eventDate]) 

486 elif not isinstance(eventData[eventDate], date): 

487 eventData[eventDate] = '' 

488 

489 # If we aren't recurring, all of our events are single-day or mutliple offerings, which also have the same start and end date 

490 if not eventData['isRecurring']: 

491 eventData['endDate'] = eventData['startDate'] 

492 

493 # Process terms 

494 if 'term' in eventData: 

495 try: 

496 eventData['term'] = Term.get_by_id(eventData['term']) 

497 except DoesNotExist: 

498 eventData['term'] = '' 

499 

500 # Process requirement 

501 if 'certRequirement' in eventData: 

502 try: 

503 eventData['certRequirement'] = CertificationRequirement.get_by_id(eventData['certRequirement']) 

504 except DoesNotExist: 

505 eventData['certRequirement'] = '' 

506 elif 'id' in eventData: 

507 # look up requirement 

508 match = RequirementMatch.get_or_none(event=eventData['id']) 

509 if match: 

510 eventData['certRequirement'] = match.requirement 

511 if 'timeStart' in eventData: 

512 eventData['timeStart'] = format24HourTime(eventData['timeStart']) 

513 

514 if 'timeEnd' in eventData: 

515 eventData['timeEnd'] = format24HourTime(eventData['timeEnd']) 

516 

517 return eventData 

518 

519def getTomorrowsEvents(): 

520 """Grabs each event that occurs tomorrow""" 

521 tomorrowDate = date.today() + timedelta(days=1) 

522 events = list(Event.select().where(Event.startDate==tomorrowDate)) 

523 return events 

524 

525def addEventView(viewer,event): 

526 """This checks if the current user already viewed the event. If not, insert a recored to EventView table""" 

527 if not viewer.isCeltsAdmin: 

528 EventView.get_or_create(user = viewer, event = event) 

529 

530def getEventRsvpCountsForTerm(term): 

531 """ 

532 Get all of the RSVPs for the events that exist in the term. 

533 Returns a dictionary with the event id as the key and the amount of 

534 current RSVPs to that event as the pair. 

535 """ 

536 amount = (Event.select(Event, fn.COUNT(EventRsvp.event_id).alias('count')) 

537 .join(EventRsvp, JOIN.LEFT_OUTER) 

538 .where(Event.term == term, Event.deletionDate == None) 

539 .group_by(Event.id)) 

540 

541 amountAsDict = {event.id: event.count for event in amount} 

542 

543 return amountAsDict 

544 

545def getEventRsvpCount(eventId): 

546 """ 

547 Returns the number of RSVP'd participants for a given eventId. 

548 """ 

549 return len(EventRsvp.select().where(EventRsvp.event_id == eventId)) 

550 

551def getCountdownToEvent(event, *, currentDatetime=None): 

552 """ 

553 Given an event, this function returns a string that conveys the amount of time left 

554 until the start of the event. 

555 

556 Note about dates: 

557 Natural language is unintuitive. There are two major rules that govern how we discuss dates. 

558 - If an event happens tomorrow but less than 24 hours away from us we still say that it happens  

559 tomorrow with no mention of the hour.  

560 - If an event happens tomorrow but more than 24 hours away from us, we'll count the number of days  

561 and hours in actual time. 

562 

563 E.g. if the current time of day is greater than the event start's time of day, we give a number of days  

564 relative to this morning and exclude all hours and minutes 

565 

566 On the other hand, if the current time of day is less or equal to the event's start of day we can produce  

567 the real difference in days and hours without the aforementioned simplifying language. 

568 """ 

569 

570 if currentDatetime is None: 

571 currentDatetime = datetime.now().replace(second=0, microsecond=0) 

572 currentMorning = currentDatetime.replace(hour=0, minute=0) 

573 

574 eventStart = datetime.combine(event.startDate, event.timeStart) 

575 eventEnd = datetime.combine(event.endDate, event.timeEnd) 

576 

577 if eventEnd < currentDatetime: 

578 return "Already passed" 

579 elif eventStart <= currentDatetime <= eventEnd: 

580 return "Happening now" 

581 

582 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

583 calendarDelta = relativedelta(eventStart, currentMorning) 

584 calendarYearsUntilEvent = calendarDelta.years 

585 calendarMonthsUntilEvent = calendarDelta.months 

586 calendarDaysUntilEvent = calendarDelta.days 

587 

588 yearString = f"{calendarYearsUntilEvent} year{'s' if calendarYearsUntilEvent > 1 else ''}" 

589 monthString = f"{calendarMonthsUntilEvent} month{'s' if calendarMonthsUntilEvent > 1 else ''}" 

590 dayString = f"{calendarDaysUntilEvent} day{'s' if calendarDaysUntilEvent > 1 else ''}" 

591 hourString = f"{timeUntilEvent.hours} hour{'s' if timeUntilEvent.hours > 1 else ''}" 

592 minuteString = f"{timeUntilEvent.minutes} minute{'s' if timeUntilEvent.minutes > 1 else ''}" 

593 

594 # Years until 

595 if calendarYearsUntilEvent: 

596 if calendarMonthsUntilEvent: 

597 return f"{yearString} and {monthString}" 

598 return f"{yearString}" 

599 # Months until 

600 if calendarMonthsUntilEvent: 

601 if calendarDaysUntilEvent: 

602 return f"{monthString} and {dayString}" 

603 return f"{monthString}" 

604 # Days until 

605 if calendarDaysUntilEvent: 

606 if eventStart.time() < currentDatetime.time(): 

607 if calendarDaysUntilEvent == 1: 

608 return "Tomorrow" 

609 return f"{dayString}" 

610 if timeUntilEvent.hours: 

611 return f"{dayString} and {hourString}" 

612 return f"{dayString}" 

613 # Hours until 

614 if timeUntilEvent.hours: 

615 if timeUntilEvent.minutes: 

616 return f"{hourString} and {minuteString}" 

617 return f"{hourString}" 

618 # Minutes until 

619 elif timeUntilEvent.minutes > 1: 

620 return f"{minuteString}" 

621 # Seconds until 

622 return "<1 minute" 

623 

624def copyRsvpToNewEvent(priorEvent, newEvent): 

625 """ 

626 Copies rvsps from priorEvent to newEvent 

627 """ 

628 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute()) 

629 

630 for student in rsvpInfo: 

631 newRsvp = EventRsvp( 

632 user = student.user, 

633 event = newEvent, 

634 rsvpWaitlist = student.rsvpWaitlist 

635 ) 

636 newRsvp.save() 

637 numRsvps = len(rsvpInfo) 

638 if numRsvps: 

639 createRsvpLog(newEvent, f"Copied {numRsvps} Rsvps from {priorEvent['name']} to {newEvent.name}")