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

346 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-10-24 20: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 

39def deleteEvent(eventId): 

40 """ 

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

42 to make sure there is no gap in weeks. 

43 """ 

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

45 

46 if event: 

47 if event.recurringId: 

48 recurringId = event.recurringId 

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

50 eventDeleted = False 

51 

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

53 for recurringEvent in recurringEvents: 

54 if eventDeleted: 

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

56 newEventName = recurringEvent.name 

57 

58 if recurringEvent == event: 

59 newEventName = recurringEvent.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 

71 

72def deleteEventAndAllFollowing(eventId): 

73 """ 

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

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

76 """ 

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

78 if event: 

79 if event.recurringId: 

80 recurringId = event.recurringId 

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

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

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

84 return deletedEventList 

85 

86def deleteAllRecurringEvents(eventId): 

87 """ 

88 Deletes all recurring events. 

89 Modified to also apply for events with multiple offerings 

90 """ 

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

92 if event: 

93 if event.recurringId: 

94 recurringId = event.recurringId 

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

96 eventId = allRecurringEvents[0].id 

97 return deleteEventAndAllFollowing(eventId) 

98 

99def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): 

100 """ 

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

102 Creates separate event data inheriting from the original eventData 

103 with the specifics of each offering. 

104 Calls attemptSaveEvent on each of the newly created datum 

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

106 

107 Returns: 

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

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

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

111 """ 

112 savedOfferings = [] 

113 failedSavedOfferings = [] 

114 allSavesWereSuccessful = True 

115 

116 # Creates a shared multipleOfferingId for all offerings to have 

117 multipleOfferingId = calculateNewMultipleOfferingId() 

118 

119 # Create separate event data inheriting from the original eventData 

120 multipleOfferingData = eventData.get('multipleOfferingData') 

121 with mainDB.atomic() as transaction: 

122 for index, event in enumerate(multipleOfferingData): 

123 multipleOfferingDict = eventData.copy() 

124 multipleOfferingDict.update({ 

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

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

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

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

129 'multipleOfferingId': multipleOfferingId 

130 }) 

131 # Try to save each offering 

132 savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles) 

133 if validationErrorMessage: 

134 failedSavedOfferings.append((index, validationErrorMessage)) 

135 allSavesWereSuccessful = False 

136 else: 

137 savedEvent = savedEvents[0] 

138 savedOfferings.append(savedEvent) 

139 if not allSavesWereSuccessful: 

140 savedOfferings = [] 

141 transaction.rollback() 

142 

143 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

144 

145 

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

147 """ 

148 Tries to save an event to the database: 

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

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

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

152 

153 Returns: 

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

155 """ 

156 

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

158 # automatically changed from "" to 0 

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

160 eventData["rsvpLimit"] = None 

161 

162 newEventData = preprocessEventData(eventData) 

163 

164 isValid, validationErrorMessage = validateNewEventData(newEventData) 

165 if not isValid: 

166 return [], validationErrorMessage 

167 

168 try: 

169 events = saveEventToDb(newEventData, renewedEvent) 

170 if attachmentFiles: 

171 for event in events: 

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

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

174 return events, "" 

175 except Exception as e: 

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

177 return [], e 

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 

185 isNewEvent = ('id' not in newEventData) 

186 

187 

188 eventsToCreate = [] 

189 recurringSeriesId = None 

190 multipleSeriesId = None 

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

192 eventsToCreate = getRecurringEventsData(newEventData) 

193 recurringSeriesId = calculateNewrecurringId() 

194 

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

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

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

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

199 "week":1}) 

200 multipleSeriesId = newEventData['multipleOfferingId'] 

201 

202 else: 

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

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

205 "week":1}) 

206 if renewedEvent: 

207 recurringSeriesId = newEventData.get('recurringId') 

208 eventRecords = [] 

209 for eventInstance in eventsToCreate: 

210 with mainDB.atomic(): 

211 

212 eventData = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

227 "contactName": newEventData['contactName'] 

228 } 

229 

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

231 # it is a new event.  

232 if isNewEvent: 

233 eventData['program'] = newEventData['program'] 

234 eventData['recurringId'] = recurringSeriesId 

235 eventData['multipleOfferingId'] = multipleSeriesId 

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

237 eventRecord = Event.create(**eventData) 

238 else: 

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

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

241 

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

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

244 

245 eventRecords.append(eventRecord) 

246 return eventRecords 

247 

248def getStudentLedEvents(term): 

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

250 .join(Program) 

251 .where(Program.isStudentLed, 

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

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

254 .execute()) 

255 

256 programs = {} 

257 

258 for event in studentLedEvents: 

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

260 

261 return programs 

262 

263def getUpcomingStudentLedCount(term, currentTime): 

264 """ 

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

266 """ 

267 

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

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

270 .where(Program.isStudentLed, 

271 Event.term == term, Event.deletionDate == None, 

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

273 Event.isCanceled == False) 

274 .group_by(Program.id)) 

275 

276 programCountDict = {} 

277 

278 for programCount in upcomingCount: 

279 programCountDict[programCount.id] = programCount.eventCount 

280 return programCountDict 

281 

282def getTrainingEvents(term, user): 

283 """ 

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

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

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

287 returned is the All Trainings Event. 

288 term: expected to be the ID of a term 

289 user: expected to be the current user 

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

291 """ 

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

293 .join(Program, JOIN.LEFT_OUTER) 

294 .where(Event.isTraining == True, 

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

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

297 

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

299 if hideBonner: 

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

301 

302 return list(trainingQuery.execute()) 

303 

304def getBonnerEvents(term): 

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

306 .join(Program) 

307 .where(Program.isBonnerScholars, 

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

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

310 .execute()) 

311 return bonnerScholarsEvents 

312 

313def getOtherEvents(term): 

314 """ 

315  

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

317 the Other Events section of the Events List page. 

318 :return: A list of Other Event objects 

319 """ 

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

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

322 

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

324 .join(Program, JOIN.LEFT_OUTER) 

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

326 Event.isTraining == False, 

327 Event.isAllVolunteerTraining == False, 

328 ((Program.isOtherCeltsSponsored) | 

329 ((Program.isStudentLed == False) & 

330 (Program.isBonnerScholars == False)))) 

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

332 .execute()) 

333 

334 return otherEvents 

335 

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

337 """ 

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

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

340 :param user: a username or User object 

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

342 Used in testing, defaults to the current timestamp. 

343 :return: A list of Event objects 

344 """ 

345 

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

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

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

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

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

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

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

353 

354 if program: 

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

356 

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

358 

359 eventsList = [] 

360 shownRecurringEventList = [] 

361 shownMultipleOfferingEventList = [] 

362 

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

364 for event in events: 

365 if event.recurringId or event.multipleOfferingId: 

366 if not event.isCanceled: 

367 if event.recurringId not in shownRecurringEventList: 

368 eventsList.append(event) 

369 shownRecurringEventList.append(event.recurringId) 

370 if event.multipleOfferingId not in shownMultipleOfferingEventList: 

371 eventsList.append(event) 

372 shownMultipleOfferingEventList.append(event.multipleOfferingId) 

373 else: 

374 if not event.isCanceled: 

375 eventsList.append(event) 

376 

377 return eventsList 

378 

379def getParticipatedEventsForUser(user): 

380 """ 

381 Get all the events a user has participated in. 

382 :param user: a username or User object 

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

384 Used in testing, defaults to the current timestamp. 

385 :return: A list of Event objects 

386 """ 

387 

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

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

390 .join(EventParticipant) 

391 .where(EventParticipant.user == user, 

392 Event.isAllVolunteerTraining == False) 

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

394 

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

396 .join(EventParticipant) 

397 .where(Event.isAllVolunteerTraining == True, 

398 EventParticipant.user == user)) 

399 union = participatedEvents.union_all(allVolunteer) 

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

401 

402 return unionParticipationWithVolunteer 

403 

404def validateNewEventData(data): 

405 """ 

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

407 

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

409 

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

411 """ 

412 

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

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

415 

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

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

418 

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

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

421 

422 # Validation if we are inserting a new event 

423 if 'id' not in data: 

424 

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

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

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

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

429 

430 sameEventListCopy = sameEventList.copy() 

431 

432 for event in sameEventListCopy: 

433 if event.isCanceled or event.recurringId: 

434 sameEventList.remove(event) 

435 

436 try: 

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

438 except DoesNotExist as e: 

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

440 if sameEventList: 

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

442 

443 data['valid'] = True 

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

445 

446def calculateNewrecurringId(): 

447 """ 

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

449 """ 

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

451 if recurringId: 

452 return recurringId + 1 

453 else: 

454 return 1 

455def calculateNewMultipleOfferingId(): 

456 """ 

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

458 """ 

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

460 if multipleOfferingId: 

461 return multipleOfferingId + 1 

462 else: 

463 return 1 

464 

465def getPreviousRecurringEventData(recurringId): 

466 """ 

467 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 

468 """ 

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

470 .join(EventParticipant) 

471 .join(Event) 

472 .where(Event.recurringId==recurringId)) 

473 return previousEventVolunteers 

474 

475def getPreviousMultipleOfferingEventData(multipleOfferingId): 

476 """ 

477 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 

478 """ 

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

480 .join(EventParticipant) 

481 .join(Event) 

482 .where(Event.multipleOfferingId == multipleOfferingId)) 

483 return previousEventVolunteers 

484 

485def getRecurringEventsData(eventData): 

486 """ 

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

488 dictionary of event data. 

489 

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

491 

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

493 """ 

494 if not isinstance(eventData['endDate'], date) or not isinstance(eventData['startDate'], date): 

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

496 

497 if eventData['endDate'] == eventData['startDate']: 

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

499 

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

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

502 "week": counter+1} 

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

504 

505def preprocessEventData(eventData): 

506 """ 

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

508 

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

510 - checkboxes should be True or False 

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

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

513 - multipleOfferingData should be a JSON string 

514 - Look up matching certification requirement if necessary 

515 """ 

516 ## Process checkboxes 

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

518 

519 for checkBox in eventCheckBoxes: 

520 if checkBox not in eventData: 

521 eventData[checkBox] = False 

522 else: 

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

524 

525 ## Process dates 

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

527 for eventDate in eventDates: 

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

529 eventData[eventDate] = '' 

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

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

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

533 eventData[eventDate] = '' 

534 

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

536 if not eventData['isRecurring']: 

537 eventData['endDate'] = eventData['startDate'] 

538 

539 # Process multipleOfferingData 

540 if 'multipleOfferingData' not in eventData: 

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

542 elif type(eventData['multipleOfferingData']) is str: 

543 try: 

544 multipleOfferingData = json.loads(eventData['multipleOfferingData']) 

545 eventData['multipleOfferingData'] = multipleOfferingData 

546 if type(multipleOfferingData) != list: 

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

548 except json.decoder.JSONDecodeError as e: 

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

550 if type(eventData['multipleOfferingData']) is list: 

551 # validate the list data. Make sure there is 'eventName', 'startDate', 'timeStart', 'timeEnd', and 'isDuplicate' data 

552 multipleOfferingData = eventData['multipleOfferingData'] 

553 for offeringDatum in multipleOfferingData: 

554 for attribute in ['eventName', 'startDate', 'timeStart', 'timeEnd']: 

555 if type(offeringDatum.get(attribute)) != str: 

556 offeringDatum[attribute] = '' 

557 if type(offeringDatum.get('isDuplicate')) != bool: 

558 offeringDatum['isDuplicate'] = False 

559 

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

561 

562 # Process terms 

563 if 'term' in eventData: 

564 try: 

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

566 except DoesNotExist: 

567 eventData['term'] = '' 

568 

569 # Process requirement 

570 if 'certRequirement' in eventData: 

571 try: 

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

573 except DoesNotExist: 

574 eventData['certRequirement'] = '' 

575 elif 'id' in eventData: 

576 # look up requirement 

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

578 if match: 

579 eventData['certRequirement'] = match.requirement 

580 if 'timeStart' in eventData: 

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

582 

583 if 'timeEnd' in eventData: 

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

585 

586 return eventData 

587 

588def getTomorrowsEvents(): 

589 """Grabs each event that occurs tomorrow""" 

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

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

592 return events 

593 

594def addEventView(viewer,event): 

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

596 if not viewer.isCeltsAdmin: 

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

598 

599def getEventRsvpCountsForTerm(term): 

600 """ 

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

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

603 current RSVPs to that event as the pair. 

604 """ 

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

606 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

608 .group_by(Event.id)) 

609 

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

611 

612 return amountAsDict 

613 

614def getEventRsvpCount(eventId): 

615 """ 

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

617 """ 

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

619 

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

621 """ 

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

623 until the start of the event. 

624 

625 Note about dates: 

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

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

628 tomorrow with no mention of the hour.  

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

630 and hours in actual time. 

631 

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

633 relative to this morning and exclude all hours and minutes 

634 

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

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

637 """ 

638 

639 if currentDatetime is None: 

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

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

642 

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

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

645 

646 if eventEnd < currentDatetime: 

647 return "Already passed" 

648 elif eventStart <= currentDatetime <= eventEnd: 

649 return "Happening now" 

650 

651 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

652 calendarDelta = relativedelta(eventStart, currentMorning) 

653 calendarYearsUntilEvent = calendarDelta.years 

654 calendarMonthsUntilEvent = calendarDelta.months 

655 calendarDaysUntilEvent = calendarDelta.days 

656 

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

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

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

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

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

662 

663 # Years until 

664 if calendarYearsUntilEvent: 

665 if calendarMonthsUntilEvent: 

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

667 return f"{yearString}" 

668 # Months until 

669 if calendarMonthsUntilEvent: 

670 if calendarDaysUntilEvent: 

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

672 return f"{monthString}" 

673 # Days until 

674 if calendarDaysUntilEvent: 

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

676 if calendarDaysUntilEvent == 1: 

677 return "Tomorrow" 

678 return f"{dayString}" 

679 if timeUntilEvent.hours: 

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

681 return f"{dayString}" 

682 # Hours until 

683 if timeUntilEvent.hours: 

684 if timeUntilEvent.minutes: 

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

686 return f"{hourString}" 

687 # Minutes until 

688 elif timeUntilEvent.minutes > 1: 

689 return f"{minuteString}" 

690 # Seconds until 

691 return "<1 minute" 

692 

693def copyRsvpToNewEvent(priorEvent, newEvent): 

694 """ 

695 Copies rvsps from priorEvent to newEvent 

696 """ 

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

698 

699 for student in rsvpInfo: 

700 newRsvp = EventRsvp( 

701 user = student.user, 

702 event = newEvent, 

703 rsvpWaitlist = student.rsvpWaitlist 

704 ) 

705 newRsvp.save() 

706 numRsvps = len(rsvpInfo) 

707 if numRsvps: 

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