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

294 statements  

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

20 

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 

25 

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

34 

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

37 

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. 

43  

44 """ 

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

46 

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 

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

54 for repeatingEvent in repeatingEvents: 

55 if eventDeleted: 

56 Event.update({Event.name:newEventName}).where(Event.id==repeatingEvent.id).execute() 

57 newEventName = repeatingEvent.name 

58 

59 if repeatingEvent == event: 

60 newEventName = repeatingEvent.name 

61 eventDeleted = True 

62 

63 program = event.program 

64 

65 if program: 

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

67 else: 

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

69 

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

71 

72def deleteEventAndAllFollowing(eventId): 

73 """ 

74 Deletes an event in the series and all events after it 

75  

76 """ 

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

78 if event: 

79 if event.seriesId: 

80 seriesId = event.seriesId 

81 eventSeries = list(Event.select(Event.id).where((Event.seriesId == seriesId) & (Event.startDate >= event.startDate))) 

82 deletedEventList = [event.id for event in eventSeries] 

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

84 return deletedEventList 

85 

86def deleteAllEventsInSeries(eventId): 

87 """ 

88 Deletes all events in a series by getting the first event in the series and calling deleteEventAndAllFollowing(). 

89  

90 """ 

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

92 if event: 

93 if event.seriesId: 

94 seriesId = event.seriesId 

95 allSeriesEvents = list(Event.select(Event.id).where(Event.seriesId == seriesId).order_by(Event.startDate)) 

96 eventId = allSeriesEvents[0].id 

97 return deleteEventAndAllFollowing(eventId) 

98 else: 

99 raise ValueError(f"Event with id {event.id} does not belong to a series (seriesId is None).") 

100 

101def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): 

102 """ 

103 Tries to save an event with multiple offerings to the database: 

104 Creates separate event data inheriting from the original eventData 

105 with the specifics of each offering. 

106 Calls attemptSaveEvent on each of the newly created datum 

107 If any data is not valid it will return a validation error. 

108 

109 Returns: 

110 allSavesWereSuccessful : bool | Whether or not all offering saves were successful 

111 savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesWereSuccessful is False then this list will be empty. 

112 failedSavedOfferings : List[(int, str), ...] | Tuples containing the indicies of failed saved offerings and the associated validation error message.  

113 """ 

114 savedOfferings = [] 

115 failedSavedOfferings = [] 

116 allSavesWereSuccessful = True 

117 

118 seriesId = calculateNewSeriesId() 

119 

120 # Create separate event data inheriting from the original eventData 

121 seriesData = eventData.get('seriesData') 

122 isRepeating = bool(eventData.get('isRepeating')) 

123 with mainDB.atomic() as transaction: 

124 for index, event in enumerate(seriesData): 

125 eventInfo = eventData.copy() 

126 eventInfo.update({ 

127 'name': event['eventName'], 

128 'startDate': event['eventDate'], 

129 'timeStart': event['startTime'], 

130 'timeEnd': event['endTime'], 

131 'seriesId': seriesId, 

132 'isRepeating': bool(isRepeating) 

133 }) 

134 # Try to save each offering 

135 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles) 

136 if validationErrorMessage: 

137 failedSavedOfferings.append((index, validationErrorMessage)) 

138 allSavesWereSuccessful = False 

139 else: 

140 savedEvent = savedEvents[0] 

141 savedOfferings.append(savedEvent) 

142 if not allSavesWereSuccessful: 

143 savedOfferings = [] 

144 transaction.rollback() 

145 

146 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

147 

148 

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

150 """ 

151 Tries to save an event to the database: 

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

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

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

155 

156 Returns: 

157 The saved event, created events and an error message if an error occurred. 

158 """ 

159 

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

161 # automatically changed from "" to 0 

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

163 eventData["rsvpLimit"] = None 

164 

165 newEventData = preprocessEventData(eventData) 

166 

167 isValid, validationErrorMessage = validateNewEventData(newEventData) 

168 if not isValid: 

169 return [], validationErrorMessage 

170 

171 events = saveEventToDb(newEventData, renewedEvent) 

172 if attachmentFiles: 

173 for event in events: 

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

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

176 return events, "" 

177 

178 

179def saveEventToDb(newEventData, renewedEvent = False): 

180 

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

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

183 

184 isNewEvent = ('id' not in newEventData) 

185 

186 eventRecords = [] 

187 with mainDB.atomic(): 

188 

189 eventData = { 

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

191 "name": newEventData['name'], 

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

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

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

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

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

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

198 "isEngagement": newEventData['isEngagement'], 

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

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

201 "startDate": newEventData['startDate'], 

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

203 "endDate": newEventData['startDate'], 

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

205 "contactName": newEventData['contactName'] 

206 } 

207 

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

209 # it is a new event.  

210 if isNewEvent: 

211 eventData['program'] = newEventData['program'] 

212 eventData['seriesId'] = newEventData.get('seriesId') 

213 eventData['isRepeating'] = bool(newEventData.get('isRepeating')) 

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

215 eventRecord = Event.create(**eventData) 

216 else: 

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

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

219 

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

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

222 

223 eventRecords.append(eventRecord) 

224 return eventRecords 

225 

226def getStudentLedEvents(term): 

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

228 .join(Program) 

229 .where(Program.isStudentLed, 

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

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

232 .execute()) 

233 

234 programs = {} 

235 

236 for event in studentLedEvents: 

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

238 

239 return programs 

240 

241def getUpcomingStudentLedCount(term, currentTime): 

242 """ 

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

244 """ 

245 

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

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

248 .where(Program.isStudentLed, 

249 Event.term == term, Event.deletionDate == None, 

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

251 Event.isCanceled == False) 

252 .group_by(Program.id)) 

253 

254 programCountDict = {} 

255 

256 for programCount in upcomingCount: 

257 programCountDict[programCount.id] = programCount.eventCount 

258 return programCountDict 

259 

260def getTrainingEvents(term, user): 

261 """ 

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

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

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

265 returned is the All Trainings Event. 

266 term: expected to be the ID of a term 

267 user: expected to be the current user 

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

269 """ 

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

271 .join(Program, JOIN.LEFT_OUTER) 

272 .where(Event.isTraining == True, 

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

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

275 

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

277 if hideBonner: 

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

279 

280 return list(trainingQuery.execute()) 

281 

282def getBonnerEvents(term): 

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

284 .join(Program) 

285 .where(Program.isBonnerScholars, 

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

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

288 .execute()) 

289 return bonnerScholarsEvents 

290 

291def getOtherEvents(term): 

292 """ 

293  

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

295 the Other Events section of the Events List page. 

296 :return: A list of Other Event objects 

297 """ 

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

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

300 

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

302 .join(Program, JOIN.LEFT_OUTER) 

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

304 Event.isTraining == False, 

305 Event.isAllVolunteerTraining == False, 

306 ((Program.isOtherCeltsSponsored) | 

307 ((Program.isStudentLed == False) & 

308 (Program.isBonnerScholars == False)))) 

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

310 .execute()) 

311 

312 return otherEvents 

313 

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

315 """ 

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

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

318 :param user: a username or User object 

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

320 Used in testing, defaults to the current timestamp. 

321 :return: A list of Event objects 

322 """ 

323 

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

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

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

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

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

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

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

331 

332 if program: 

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

334 

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

336 

337 eventsList = [] 

338 seriesEventsList = [] 

339 

340 # removes all events in series except for the next upcoming one 

341 for event in events: 

342 if event.seriesId: 

343 if not event.isCanceled: 

344 if event.seriesId not in seriesEventsList: 

345 eventsList.append(event) 

346 seriesEventsList.append(event.seriesId) 

347 else: 

348 if not event.isCanceled: 

349 eventsList.append(event) 

350 

351 return eventsList 

352 

353def getParticipatedEventsForUser(user): 

354 """ 

355 Get all the events a user has participated in. 

356 :param user: a username or User object 

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

358 Used in testing, defaults to the current timestamp. 

359 :return: A list of Event objects 

360 """ 

361 

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

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

364 .join(EventParticipant) 

365 .where(EventParticipant.user == user, 

366 Event.isAllVolunteerTraining == False) 

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

368 

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

370 .join(EventParticipant) 

371 .where(Event.isAllVolunteerTraining == True, 

372 EventParticipant.user == user)) 

373 union = participatedEvents.union_all(allVolunteer) 

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

375 

376 return unionParticipationWithVolunteer 

377 

378def validateNewEventData(data): 

379 """ 

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

381 

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

383 

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

385 """ 

386 

387 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating']]: 

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

389 

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

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

392 

393 # Validation if we are inserting a new event 

394 if 'id' not in data: 

395 

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

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

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

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

400 

401 sameEventListCopy = sameEventList.copy() 

402 

403 for event in sameEventListCopy: 

404 if event.isCanceled or (event.seriesId and event.isRepeating): 

405 sameEventList.remove(event) 

406 

407 try: 

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

409 except DoesNotExist as e: 

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

411 if sameEventList: 

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

413 

414 data['valid'] = True 

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

416 

417def calculateNewSeriesId(): 

418 """ 

419 Gets the max series ID so that new seriesId can be assigned. 

420 """ 

421 maxSeriesId = Event.select(fn.MAX(Event.seriesId)).scalar() 

422 if maxSeriesId: 

423 return maxSeriesId + 1 

424 return 1 

425 

426def getPreviousSeriesEventData(seriesId): 

427 """ 

428 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. 

429  

430 """ 

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

432 .join(EventParticipant) 

433 .join(Event) 

434 .where(Event.seriesId==seriesId)) 

435 return previousEventVolunteers 

436 

437def getRepeatingEventsData(eventData): 

438 """ 

439 Calculate the events to create based on a repeating event start and end date. Takes a 

440 dictionary of event data. 

441  

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

443 

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

445 """ 

446 

447 return [ {'name': f"{eventData['name']} Week {counter+1}", 

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

449 "week": counter+1} 

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

451 

452def preprocessEventData(eventData): 

453 """ 

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

455 

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

457 - checkboxes should be True or False 

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

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

460 - seriesData should be a JSON string 

461 - Look up matching certification requirement if necessary 

462 """ 

463 ## Process checkboxes 

464 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining'] 

465 

466 for checkBox in eventCheckBoxes: 

467 if checkBox not in eventData: 

468 eventData[checkBox] = False 

469 else: 

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

471 

472 ## Process dates 

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

474 for eventDate in eventDates: 

475 if eventDate not in eventData: # There is no date given 

476 eventData[eventDate] = '' 

477 elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string  

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

479 elif not isinstance(eventData[eventDate], date): # The date is not a date object 

480 eventData[eventDate] = '' 

481 

482 # Process seriesData 

483 if 'seriesData' not in eventData: 

484 eventData['seriesData'] = json.dumps([]) 

485 

486 # Process terms 

487 if 'term' in eventData: 

488 try: 

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

490 except DoesNotExist: 

491 eventData['term'] = '' 

492 

493 # Process requirement 

494 if 'certRequirement' in eventData: 

495 try: 

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

497 except DoesNotExist: 

498 eventData['certRequirement'] = '' 

499 elif 'id' in eventData: 

500 # look up requirement 

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

502 if match: 

503 eventData['certRequirement'] = match.requirement 

504 if 'timeStart' in eventData: 

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

506 

507 if 'timeEnd' in eventData: 

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

509 

510 return eventData 

511 

512def getTomorrowsEvents(): 

513 """Grabs each event that occurs tomorrow""" 

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

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

516 return events 

517 

518def addEventView(viewer,event): 

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

520 if not viewer.isCeltsAdmin: 

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

522 

523def getEventRsvpCountsForTerm(term): 

524 """ 

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

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

527 current RSVPs to that event as the pair. 

528 """ 

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

530 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

532 .group_by(Event.id)) 

533 

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

535 

536 return amountAsDict 

537 

538def getEventRsvpCount(eventId): 

539 """ 

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

541 """ 

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

543 

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

545 """ 

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

547 until the start of the event. 

548 

549 Note about dates: 

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

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

552 tomorrow with no mention of the hour.  

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

554 and hours in actual time. 

555 

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

557 relative to this morning and exclude all hours and minutes 

558 

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

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

561 """ 

562 

563 if currentDatetime is None: 

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

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

566 

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

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

569 

570 if eventEnd < currentDatetime: 

571 return "Already passed" 

572 elif eventStart <= currentDatetime <= eventEnd: 

573 return "Happening now" 

574 

575 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

576 calendarDelta = relativedelta(eventStart, currentMorning) 

577 calendarYearsUntilEvent = calendarDelta.years 

578 calendarMonthsUntilEvent = calendarDelta.months 

579 calendarDaysUntilEvent = calendarDelta.days 

580 

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

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

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

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

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

586 

587 # Years until 

588 if calendarYearsUntilEvent: 

589 if calendarMonthsUntilEvent: 

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

591 return f"{yearString}" 

592 # Months until 

593 if calendarMonthsUntilEvent: 

594 if calendarDaysUntilEvent: 

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

596 return f"{monthString}" 

597 # Days until 

598 if calendarDaysUntilEvent: 

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

600 if calendarDaysUntilEvent == 1: 

601 return "Tomorrow" 

602 return f"{dayString}" 

603 if timeUntilEvent.hours: 

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

605 return f"{dayString}" 

606 # Hours until 

607 if timeUntilEvent.hours: 

608 if timeUntilEvent.minutes: 

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

610 return f"{hourString}" 

611 # Minutes until 

612 elif timeUntilEvent.minutes > 1: 

613 return f"{minuteString}" 

614 # Seconds until 

615 return "<1 minute" 

616 

617def copyRsvpToNewEvent(priorEvent, newEvent): 

618 """ 

619 Copies rvsps from priorEvent to newEvent 

620 """ 

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

622 

623 for student in rsvpInfo: 

624 newRsvp = EventRsvp( 

625 user = student.user, 

626 event = newEvent, 

627 rsvpWaitlist = student.rsvpWaitlist 

628 ) 

629 newRsvp.save() 

630 numRsvps = len(rsvpInfo) 

631 if numRsvps: 

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