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

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 # 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 

57 

58 if repeatingEvent == event: 

59 newEventName = repeatingEvent.name 

60 eventDeleted = True 

61 

62 program = event.program 

63 

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

68 

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

70 

71def deleteEventAndAllFollowing(eventId): 

72 """ 

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

74  

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 

84 

85def deleteAllEventsInSeries(eventId): 

86 """ 

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

88  

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

99 

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. 

107 

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 

116 

117 seriesId = calculateNewSeriesId() 

118 

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

144 

145 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

146 

147 

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. 

154 

155 Returns: 

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

157 """ 

158 

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 

163 

164 newEventData = preprocessEventData(eventData) 

165 

166 isValid, validationErrorMessage = validateNewEventData(newEventData) 

167 if not isValid: 

168 return [], validationErrorMessage 

169 

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, "" 

176 

177 

178def saveEventToDb(newEventData, renewedEvent = False): 

179 

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

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

182 

183 isNewEvent = ('id' not in newEventData) 

184 

185 eventRecords = [] 

186 with mainDB.atomic(): 

187 

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 } 

205 

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

217 

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

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

220 

221 eventRecords.append(eventRecord) 

222 return eventRecords 

223 

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

231 

232 programs = {} 

233 

234 for event in studentLedEvents: 

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

236 

237 return programs 

238 

239def getUpcomingStudentLedCount(term, currentTime): 

240 """ 

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

242 """ 

243 

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

251 

252 programCountDict = {} 

253 

254 for programCount in upcomingCount: 

255 programCountDict[programCount.id] = programCount.eventCount 

256 return programCountDict 

257 

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

273 

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

275 if hideBonner: 

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

277 

278 return list(trainingQuery.execute()) 

279 

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 

288 

289def getOtherEvents(term): 

290 """ 

291  

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 

298 

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

309 

310 return otherEvents 

311 

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 """ 

321 

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

329 

330 if program: 

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

332 

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

334 

335 eventsList = [] 

336 seriesEventsList = [] 

337 

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) 

348 

349 return eventsList 

350 

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 """ 

359 

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

366 

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

373 

374 return unionParticipationWithVolunteer 

375 

376def validateNewEventData(data): 

377 """ 

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

379 

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

381 

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

383 """ 

384 

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

387 

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

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

390 

391 # Validation if we are inserting a new event 

392 if 'id' not in data: 

393 

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

398 

399 sameEventListCopy = sameEventList.copy() 

400 

401 for event in sameEventListCopy: 

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

403 sameEventList.remove(event) 

404 

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

411 

412 data['valid'] = True 

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

414 

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 

423 

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. 

427  

428 """ 

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

430 .join(EventParticipant) 

431 .join(Event) 

432 .where(Event.seriesId==seriesId)) 

433 return previousEventVolunteers 

434 

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. 

439  

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

441 

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

443 """ 

444 

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

449 

450def preprocessEventData(eventData): 

451 """ 

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

453 

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'] 

463 

464 for checkBox in eventCheckBoxes: 

465 if checkBox not in eventData: 

466 eventData[checkBox] = False 

467 else: 

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

469 

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] = '' 

479 

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 

500 

501 eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData']) 

502 

503 # Process seriesData 

504 if 'seriesData' not in eventData: 

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

506 

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'] = '' 

513 

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']) 

527 

528 if 'timeEnd' in eventData: 

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

530 

531 return eventData 

532 

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 

538 

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) 

543 

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

554 

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

556 

557 return amountAsDict 

558 

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

564 

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. 

569 

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. 

576 

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 

579 

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 """ 

583 

584 if currentDatetime is None: 

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

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

587 

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

589 eventEnd = datetime.combine(event.startDate, event.timeEnd) 

590 

591 if eventEnd < currentDatetime: 

592 return "Already passed" 

593 elif eventStart <= currentDatetime <= eventEnd: 

594 return "Happening now" 

595 

596 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

597 calendarDelta = relativedelta(eventStart, currentMorning) 

598 calendarYearsUntilEvent = calendarDelta.years 

599 calendarMonthsUntilEvent = calendarDelta.months 

600 calendarDaysUntilEvent = calendarDelta.days 

601 

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

607 

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" 

637 

638def copyRsvpToNewEvent(priorEvent, newEvent): 

639 """ 

640 Copies rvsps from priorEvent to newEvent 

641 """ 

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

643 

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