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

344 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-11-23 03:00 +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, Event.deletionDate.is_null(True)).order_by(Event.id)) # orders for tests 

50 eventDeleted = False 

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

52 for recurringEvent in recurringEvents: 

53 if eventDeleted: 

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

55 newEventName = recurringEvent.name 

56 

57 if recurringEvent == event: 

58 newEventName = recurringEvent.name 

59 eventDeleted = True 

60 

61 program = event.program 

62 

63 if program: 

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

65 else: 

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

67 

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

69 

70 

71def deleteEventAndAllFollowing(eventId): 

72 """ 

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

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

75 """ 

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

77 if event: 

78 if event.recurringId: 

79 recurringId = event.recurringId 

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

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

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

83 return deletedEventList 

84 

85def deleteAllRecurringEvents(eventId): 

86 """ 

87 Deletes all recurring events. 

88 Modified to also apply for events with multiple offerings 

89 """ 

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

91 if event: 

92 if event.recurringId: 

93 recurringId = event.recurringId 

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

95 eventId = allRecurringEvents[0].id 

96 return deleteEventAndAllFollowing(eventId) 

97 

98def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): 

99 """ 

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

101 Creates separate event data inheriting from the original eventData 

102 with the specifics of each offering. 

103 Calls attemptSaveEvent on each of the newly created datum 

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

105 

106 Returns: 

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

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

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

110 """ 

111 savedOfferings = [] 

112 failedSavedOfferings = [] 

113 allSavesWereSuccessful = True 

114 

115 # Creates a shared multipleOfferingId for all offerings to have 

116 multipleOfferingId = calculateNewMultipleOfferingId() 

117 

118 # Create separate event data inheriting from the original eventData 

119 multipleOfferingData = eventData.get('multipleOfferingData') 

120 with mainDB.atomic() as transaction: 

121 for index, event in enumerate(multipleOfferingData): 

122 multipleOfferingDict = eventData.copy() 

123 multipleOfferingDict.update({ 

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

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

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

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

128 'multipleOfferingId': multipleOfferingId 

129 }) 

130 # Try to save each offering 

131 savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles) 

132 if validationErrorMessage: 

133 failedSavedOfferings.append((index, validationErrorMessage)) 

134 allSavesWereSuccessful = False 

135 else: 

136 savedEvent = savedEvents[0] 

137 savedOfferings.append(savedEvent) 

138 if not allSavesWereSuccessful: 

139 savedOfferings = [] 

140 transaction.rollback() 

141 

142 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

143 

144 

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

146 """ 

147 Tries to save an event to the database: 

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

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

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

151 

152 Returns: 

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

154 """ 

155 

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

157 # automatically changed from "" to 0 

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

159 eventData["rsvpLimit"] = None 

160 

161 newEventData = preprocessEventData(eventData) 

162 

163 isValid, validationErrorMessage = validateNewEventData(newEventData) 

164 if not isValid: 

165 return [], validationErrorMessage 

166 

167 try: 

168 events = saveEventToDb(newEventData, renewedEvent) 

169 if attachmentFiles: 

170 for event in events: 

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

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

173 return events, "" 

174 except Exception as e: 

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

176 return [], e 

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 

184 isNewEvent = ('id' not in newEventData) 

185 

186 

187 eventsToCreate = [] 

188 recurringSeriesId = None 

189 multipleSeriesId = None 

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

191 eventsToCreate = getRecurringEventsData(newEventData) 

192 recurringSeriesId = calculateNewrecurringId() 

193 

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

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

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

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

198 "week":1}) 

199 multipleSeriesId = newEventData['multipleOfferingId'] 

200 

201 else: 

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

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

204 "week":1}) 

205 if renewedEvent: 

206 recurringSeriesId = newEventData.get('recurringId') 

207 eventRecords = [] 

208 for eventInstance in eventsToCreate: 

209 with mainDB.atomic(): 

210 

211 eventData = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

226 "contactName": newEventData['contactName'] 

227 } 

228 

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

230 # it is a new event.  

231 if isNewEvent: 

232 eventData['program'] = newEventData['program'] 

233 eventData['recurringId'] = recurringSeriesId 

234 eventData['multipleOfferingId'] = multipleSeriesId 

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

236 eventRecord = Event.create(**eventData) 

237 else: 

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

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

240 

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

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

243 

244 eventRecords.append(eventRecord) 

245 return eventRecords 

246 

247def getStudentLedEvents(term): 

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

249 .join(Program) 

250 .where(Program.isStudentLed, 

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

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

253 .execute()) 

254 

255 programs = {} 

256 

257 for event in studentLedEvents: 

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

259 

260 return programs 

261 

262def getUpcomingStudentLedCount(term, currentTime): 

263 """ 

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

265 """ 

266 

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

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

269 .where(Program.isStudentLed, 

270 Event.term == term, Event.deletionDate == None, 

271 (Event.startDate > currentTime) | ((Event.startDate == currentTime) & (Event.timeEnd >= currentTime)), 

272 Event.isCanceled == False) 

273 .group_by(Program.id)) 

274 

275 programCountDict = {} 

276 

277 for programCount in upcomingCount: 

278 programCountDict[programCount.id] = programCount.eventCount 

279 return programCountDict 

280 

281def getTrainingEvents(term, user): 

282 """ 

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

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

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

286 returned is the All Trainings Event. 

287 term: expected to be the ID of a term 

288 user: expected to be the current user 

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

290 """ 

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

292 .join(Program, JOIN.LEFT_OUTER) 

293 .where(Event.isTraining == True, 

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

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

296 

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

298 if hideBonner: 

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

300 

301 return list(trainingQuery.execute()) 

302 

303def getBonnerEvents(term): 

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

305 .join(Program) 

306 .where(Program.isBonnerScholars, 

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

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

309 .execute()) 

310 return bonnerScholarsEvents 

311 

312def getOtherEvents(term): 

313 """ 

314  

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

316 the Other Events section of the Events List page. 

317 :return: A list of Other Event objects 

318 """ 

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

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

321 

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

323 .join(Program, JOIN.LEFT_OUTER) 

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

325 Event.isTraining == False, 

326 Event.isAllVolunteerTraining == False, 

327 ((Program.isOtherCeltsSponsored) | 

328 ((Program.isStudentLed == False) & 

329 (Program.isBonnerScholars == False)))) 

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

331 .execute()) 

332 

333 return otherEvents 

334 

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

336 """ 

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

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

339 :param user: a username or User object 

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

341 Used in testing, defaults to the current timestamp. 

342 :return: A list of Event objects 

343 """ 

344 

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

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

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

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

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

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

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

352 

353 if program: 

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

355 

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

357 

358 eventsList = [] 

359 shownRecurringEventList = [] 

360 shownMultipleOfferingEventList = [] 

361 

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

363 for event in events: 

364 if event.recurringId or event.multipleOfferingId: 

365 if not event.isCanceled: 

366 if event.recurringId not in shownRecurringEventList: 

367 eventsList.append(event) 

368 shownRecurringEventList.append(event.recurringId) 

369 if event.multipleOfferingId not in shownMultipleOfferingEventList: 

370 eventsList.append(event) 

371 shownMultipleOfferingEventList.append(event.multipleOfferingId) 

372 else: 

373 if not event.isCanceled: 

374 eventsList.append(event) 

375 

376 return eventsList 

377 

378def getParticipatedEventsForUser(user): 

379 """ 

380 Get all the events a user has participated in. 

381 :param user: a username or User object 

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

383 Used in testing, defaults to the current timestamp. 

384 :return: A list of Event objects 

385 """ 

386 

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

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

389 .join(EventParticipant) 

390 .where(EventParticipant.user == user, 

391 Event.isAllVolunteerTraining == False) 

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

393 

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

395 .join(EventParticipant) 

396 .where(Event.isAllVolunteerTraining == True, 

397 EventParticipant.user == user)) 

398 union = participatedEvents.union_all(allVolunteer) 

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

400 

401 return unionParticipationWithVolunteer 

402 

403def validateNewEventData(data): 

404 """ 

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

406 

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

408 

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

410 """ 

411 

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

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

414 

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

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

417 

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

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

420 

421 # Validation if we are inserting a new event 

422 if 'id' not in data: 

423 

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

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

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

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

428 

429 sameEventListCopy = sameEventList.copy() 

430 

431 for event in sameEventListCopy: 

432 if event.isCanceled or event.recurringId: 

433 sameEventList.remove(event) 

434 

435 try: 

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

437 except DoesNotExist as e: 

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

439 if sameEventList: 

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

441 

442 data['valid'] = True 

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

444 

445def calculateNewrecurringId(): 

446 """ 

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

448 """ 

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

450 if recurringId: 

451 return recurringId + 1 

452 else: 

453 return 1 

454def calculateNewMultipleOfferingId(): 

455 """ 

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

457 """ 

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

459 if multipleOfferingId: 

460 return multipleOfferingId + 1 

461 else: 

462 return 1 

463 

464def getPreviousRecurringEventData(recurringId): 

465 """ 

466 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 

467 """ 

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

469 .join(EventParticipant) 

470 .join(Event) 

471 .where(Event.recurringId==recurringId)) 

472 return previousEventVolunteers 

473 

474def getPreviousMultipleOfferingEventData(multipleOfferingId): 

475 """ 

476 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 

477 """ 

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

479 .join(EventParticipant) 

480 .join(Event) 

481 .where(Event.multipleOfferingId == multipleOfferingId)) 

482 return previousEventVolunteers 

483 

484def getRecurringEventsData(eventData): 

485 """ 

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

487 dictionary of event data. 

488 

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

490 

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

492 """ 

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

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

495 

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

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

498 

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

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

501 "week": counter+1} 

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

503 

504def preprocessEventData(eventData): 

505 """ 

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

507 

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

509 - checkboxes should be True or False 

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

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

512 - multipleOfferingData should be a JSON string 

513 - Look up matching certification requirement if necessary 

514 """ 

515 ## Process checkboxes 

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

517 

518 for checkBox in eventCheckBoxes: 

519 if checkBox not in eventData: 

520 eventData[checkBox] = False 

521 else: 

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

523 

524 ## Process dates 

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

526 for eventDate in eventDates: 

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

528 eventData[eventDate] = '' 

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

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

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

532 eventData[eventDate] = '' 

533 

534 # Process multipleOfferingData 

535 if 'multipleOfferingData' not in eventData: 

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

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

538 try: 

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

540 eventData['multipleOfferingData'] = multipleOfferingData 

541 if type(multipleOfferingData) != list: 

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

543 except json.decoder.JSONDecodeError as e: 

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

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

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

547 multipleOfferingData = eventData['multipleOfferingData'] 

548 for offeringDatum in multipleOfferingData: 

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

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

551 offeringDatum[attribute] = '' 

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

553 offeringDatum['isDuplicate'] = False 

554 

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

556 

557 # Process terms 

558 if 'term' in eventData: 

559 try: 

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

561 except DoesNotExist: 

562 eventData['term'] = '' 

563 

564 # Process requirement 

565 if 'certRequirement' in eventData: 

566 try: 

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

568 except DoesNotExist: 

569 eventData['certRequirement'] = '' 

570 elif 'id' in eventData: 

571 # look up requirement 

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

573 if match: 

574 eventData['certRequirement'] = match.requirement 

575 if 'timeStart' in eventData: 

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

577 

578 if 'timeEnd' in eventData: 

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

580 

581 return eventData 

582 

583def getTomorrowsEvents(): 

584 """Grabs each event that occurs tomorrow""" 

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

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

587 return events 

588 

589def addEventView(viewer,event): 

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

591 if not viewer.isCeltsAdmin: 

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

593 

594def getEventRsvpCountsForTerm(term): 

595 """ 

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

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

598 current RSVPs to that event as the pair. 

599 """ 

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

601 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

603 .group_by(Event.id)) 

604 

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

606 

607 return amountAsDict 

608 

609def getEventRsvpCount(eventId): 

610 """ 

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

612 """ 

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

614 

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

616 """ 

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

618 until the start of the event. 

619 

620 Note about dates: 

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

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

623 tomorrow with no mention of the hour.  

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

625 and hours in actual time. 

626 

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

628 relative to this morning and exclude all hours and minutes 

629 

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

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

632 """ 

633 

634 if currentDatetime is None: 

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

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

637 

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

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

640 

641 if eventEnd < currentDatetime: 

642 return "Already passed" 

643 elif eventStart <= currentDatetime <= eventEnd: 

644 return "Happening now" 

645 

646 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

647 calendarDelta = relativedelta(eventStart, currentMorning) 

648 calendarYearsUntilEvent = calendarDelta.years 

649 calendarMonthsUntilEvent = calendarDelta.months 

650 calendarDaysUntilEvent = calendarDelta.days 

651 

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

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

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

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

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

657 

658 # Years until 

659 if calendarYearsUntilEvent: 

660 if calendarMonthsUntilEvent: 

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

662 return f"{yearString}" 

663 # Months until 

664 if calendarMonthsUntilEvent: 

665 if calendarDaysUntilEvent: 

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

667 return f"{monthString}" 

668 # Days until 

669 if calendarDaysUntilEvent: 

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

671 if calendarDaysUntilEvent == 1: 

672 return "Tomorrow" 

673 return f"{dayString}" 

674 if timeUntilEvent.hours: 

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

676 return f"{dayString}" 

677 # Hours until 

678 if timeUntilEvent.hours: 

679 if timeUntilEvent.minutes: 

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

681 return f"{hourString}" 

682 # Minutes until 

683 elif timeUntilEvent.minutes > 1: 

684 return f"{minuteString}" 

685 # Seconds until 

686 return "<1 minute" 

687 

688def copyRsvpToNewEvent(priorEvent, newEvent): 

689 """ 

690 Copies rvsps from priorEvent to newEvent 

691 """ 

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

693 

694 for student in rsvpInfo: 

695 newRsvp = EventRsvp( 

696 user = student.user, 

697 event = newEvent, 

698 rsvpWaitlist = student.rsvpWaitlist 

699 ) 

700 newRsvp.save() 

701 numRsvps = len(rsvpInfo) 

702 if numRsvps: 

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