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

346 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-11-22 21:05 +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 "isEngagement": newEventData['isEngagement'], 

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

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

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

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

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

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

228 "contactName": newEventData['contactName'] 

229 } 

230 

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

232 # it is a new event.  

233 if isNewEvent: 

234 eventData['program'] = newEventData['program'] 

235 eventData['recurringId'] = recurringSeriesId 

236 eventData['multipleOfferingId'] = multipleSeriesId 

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

238 eventRecord = Event.create(**eventData) 

239 else: 

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

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

242 

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

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

245 

246 eventRecords.append(eventRecord) 

247 return eventRecords 

248 

249def getStudentLedEvents(term): 

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

251 .join(Program) 

252 .where(Program.isStudentLed, 

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

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

255 .execute()) 

256 

257 programs = {} 

258 

259 for event in studentLedEvents: 

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

261 

262 return programs 

263 

264def getUpcomingStudentLedCount(term, currentTime): 

265 """ 

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

267 """ 

268 

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

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

271 .where(Program.isStudentLed, 

272 Event.term == term, Event.deletionDate == None, 

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

274 Event.isCanceled == False) 

275 .group_by(Program.id)) 

276 

277 programCountDict = {} 

278 

279 for programCount in upcomingCount: 

280 programCountDict[programCount.id] = programCount.eventCount 

281 return programCountDict 

282 

283def getTrainingEvents(term, user): 

284 """ 

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

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

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

288 returned is the All Trainings Event. 

289 term: expected to be the ID of a term 

290 user: expected to be the current user 

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

292 """ 

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

294 .join(Program, JOIN.LEFT_OUTER) 

295 .where(Event.isTraining == True, 

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

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

298 

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

300 if hideBonner: 

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

302 

303 return list(trainingQuery.execute()) 

304 

305def getBonnerEvents(term): 

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

307 .join(Program) 

308 .where(Program.isBonnerScholars, 

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

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

311 .execute()) 

312 return bonnerScholarsEvents 

313 

314def getOtherEvents(term): 

315 """ 

316  

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

318 the Other Events section of the Events List page. 

319 :return: A list of Other Event objects 

320 """ 

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

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

323 

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

325 .join(Program, JOIN.LEFT_OUTER) 

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

327 Event.isTraining == False, 

328 Event.isAllVolunteerTraining == False, 

329 ((Program.isOtherCeltsSponsored) | 

330 ((Program.isStudentLed == False) & 

331 (Program.isBonnerScholars == False)))) 

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

333 .execute()) 

334 

335 return otherEvents 

336 

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

338 """ 

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

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

341 :param user: a username or User object 

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

343 Used in testing, defaults to the current timestamp. 

344 :return: A list of Event objects 

345 """ 

346 

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

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

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

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

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

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

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

354 

355 if program: 

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

357 

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

359 

360 eventsList = [] 

361 shownRecurringEventList = [] 

362 shownMultipleOfferingEventList = [] 

363 

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

365 for event in events: 

366 if event.recurringId or event.multipleOfferingId: 

367 if not event.isCanceled: 

368 if event.recurringId not in shownRecurringEventList: 

369 eventsList.append(event) 

370 shownRecurringEventList.append(event.recurringId) 

371 if event.multipleOfferingId not in shownMultipleOfferingEventList: 

372 eventsList.append(event) 

373 shownMultipleOfferingEventList.append(event.multipleOfferingId) 

374 else: 

375 if not event.isCanceled: 

376 eventsList.append(event) 

377 

378 return eventsList 

379 

380def getParticipatedEventsForUser(user): 

381 """ 

382 Get all the events a user has participated in. 

383 :param user: a username or User object 

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

385 Used in testing, defaults to the current timestamp. 

386 :return: A list of Event objects 

387 """ 

388 

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

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

391 .join(EventParticipant) 

392 .where(EventParticipant.user == user, 

393 Event.isAllVolunteerTraining == False) 

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

395 

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

397 .join(EventParticipant) 

398 .where(Event.isAllVolunteerTraining == True, 

399 EventParticipant.user == user)) 

400 union = participatedEvents.union_all(allVolunteer) 

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

402 

403 return unionParticipationWithVolunteer 

404 

405def validateNewEventData(data): 

406 """ 

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

408 

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

410 

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

412 """ 

413 

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

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

416 

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

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

419 

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

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

422 

423 # Validation if we are inserting a new event 

424 if 'id' not in data: 

425 

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

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

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

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

430 

431 sameEventListCopy = sameEventList.copy() 

432 

433 for event in sameEventListCopy: 

434 if event.isCanceled or event.recurringId: 

435 sameEventList.remove(event) 

436 

437 try: 

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

439 except DoesNotExist as e: 

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

441 if sameEventList: 

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

443 

444 data['valid'] = True 

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

446 

447def calculateNewrecurringId(): 

448 """ 

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

450 """ 

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

452 if recurringId: 

453 return recurringId + 1 

454 else: 

455 return 1 

456def calculateNewMultipleOfferingId(): 

457 """ 

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

459 """ 

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

461 if multipleOfferingId: 

462 return multipleOfferingId + 1 

463 else: 

464 return 1 

465 

466def getPreviousRecurringEventData(recurringId): 

467 """ 

468 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 

469 """ 

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

471 .join(EventParticipant) 

472 .join(Event) 

473 .where(Event.recurringId==recurringId)) 

474 return previousEventVolunteers 

475 

476def getPreviousMultipleOfferingEventData(multipleOfferingId): 

477 """ 

478 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 

479 """ 

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

481 .join(EventParticipant) 

482 .join(Event) 

483 .where(Event.multipleOfferingId == multipleOfferingId)) 

484 return previousEventVolunteers 

485 

486def getRecurringEventsData(eventData): 

487 """ 

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

489 dictionary of event data. 

490 

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

492 

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

494 """ 

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

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

497 

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

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

500 

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

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

503 "week": counter+1} 

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

505 

506def preprocessEventData(eventData): 

507 """ 

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

509 

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

511 - checkboxes should be True or False 

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

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

514 - multipleOfferingData should be a JSON string 

515 - Look up matching certification requirement if necessary 

516 """ 

517 ## Process checkboxes 

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

519 

520 for checkBox in eventCheckBoxes: 

521 if checkBox not in eventData: 

522 eventData[checkBox] = False 

523 else: 

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

525 

526 ## Process dates 

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

528 for eventDate in eventDates: 

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

530 eventData[eventDate] = '' 

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

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

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

534 eventData[eventDate] = '' 

535 

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

537 if not eventData['isRecurring']: 

538 eventData['endDate'] = eventData['startDate'] 

539 

540 # Process multipleOfferingData 

541 if 'multipleOfferingData' not in eventData: 

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

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

544 try: 

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

546 eventData['multipleOfferingData'] = multipleOfferingData 

547 if type(multipleOfferingData) != list: 

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

549 except json.decoder.JSONDecodeError as e: 

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

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

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

553 multipleOfferingData = eventData['multipleOfferingData'] 

554 for offeringDatum in multipleOfferingData: 

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

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

557 offeringDatum[attribute] = '' 

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

559 offeringDatum['isDuplicate'] = False 

560 

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

562 

563 # Process terms 

564 if 'term' in eventData: 

565 try: 

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

567 except DoesNotExist: 

568 eventData['term'] = '' 

569 

570 # Process requirement 

571 if 'certRequirement' in eventData: 

572 try: 

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

574 except DoesNotExist: 

575 eventData['certRequirement'] = '' 

576 elif 'id' in eventData: 

577 # look up requirement 

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

579 if match: 

580 eventData['certRequirement'] = match.requirement 

581 if 'timeStart' in eventData: 

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

583 

584 if 'timeEnd' in eventData: 

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

586 

587 return eventData 

588 

589def getTomorrowsEvents(): 

590 """Grabs each event that occurs tomorrow""" 

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

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

593 return events 

594 

595def addEventView(viewer,event): 

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

597 if not viewer.isCeltsAdmin: 

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

599 

600def getEventRsvpCountsForTerm(term): 

601 """ 

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

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

604 current RSVPs to that event as the pair. 

605 """ 

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

607 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

609 .group_by(Event.id)) 

610 

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

612 

613 return amountAsDict 

614 

615def getEventRsvpCount(eventId): 

616 """ 

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

618 """ 

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

620 

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

622 """ 

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

624 until the start of the event. 

625 

626 Note about dates: 

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

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

629 tomorrow with no mention of the hour.  

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

631 and hours in actual time. 

632 

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

634 relative to this morning and exclude all hours and minutes 

635 

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

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

638 """ 

639 

640 if currentDatetime is None: 

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

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

643 

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

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

646 

647 if eventEnd < currentDatetime: 

648 return "Already passed" 

649 elif eventStart <= currentDatetime <= eventEnd: 

650 return "Happening now" 

651 

652 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

653 calendarDelta = relativedelta(eventStart, currentMorning) 

654 calendarYearsUntilEvent = calendarDelta.years 

655 calendarMonthsUntilEvent = calendarDelta.months 

656 calendarDaysUntilEvent = calendarDelta.days 

657 

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

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

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

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

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

663 

664 # Years until 

665 if calendarYearsUntilEvent: 

666 if calendarMonthsUntilEvent: 

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

668 return f"{yearString}" 

669 # Months until 

670 if calendarMonthsUntilEvent: 

671 if calendarDaysUntilEvent: 

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

673 return f"{monthString}" 

674 # Days until 

675 if calendarDaysUntilEvent: 

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

677 if calendarDaysUntilEvent == 1: 

678 return "Tomorrow" 

679 return f"{dayString}" 

680 if timeUntilEvent.hours: 

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

682 return f"{dayString}" 

683 # Hours until 

684 if timeUntilEvent.hours: 

685 if timeUntilEvent.minutes: 

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

687 return f"{hourString}" 

688 # Minutes until 

689 elif timeUntilEvent.minutes > 1: 

690 return f"{minuteString}" 

691 # Seconds until 

692 return "<1 minute" 

693 

694def copyRsvpToNewEvent(priorEvent, newEvent): 

695 """ 

696 Copies rvsps from priorEvent to newEvent 

697 """ 

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

699 

700 for student in rsvpInfo: 

701 newRsvp = EventRsvp( 

702 user = student.user, 

703 event = newEvent, 

704 rsvpWaitlist = student.rsvpWaitlist 

705 ) 

706 newRsvp.save() 

707 numRsvps = len(rsvpInfo) 

708 if numRsvps: 

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