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

473 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-09-13 18:43 +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 

7from app.models import mainDB 

8from app.models.user import User 

9from app.models.event import Event 

10from app.models.eventParticipant import EventParticipant 

11from app.models.program import Program 

12from app.models.term import Term 

13from app.models.programBan import ProgramBan 

14from app.models.interest import Interest 

15from app.models.eventRsvp import EventRsvp 

16from app.models.requirementMatch import RequirementMatch 

17from app.models.certificationRequirement import CertificationRequirement 

18from app.models.eventViews import EventView 

19 

20from app.logic.createLogs import createActivityLog, createRsvpLog 

21from app.logic.utils import format24HourTime 

22from app.logic.fileHandler import FileHandler 

23from app.logic.certification import updateCertRequirementForEvent 

24 

25def cancelEvent(eventId): 

26 """ 

27 Cancels an event. 

28 """ 

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

30 if event: 

31 event.isCanceled = True 

32 event.save() 

33 

34 program = event.program 

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

36 

37 

38def deleteEvent(eventId): 

39 """ 

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

41 to make sure there is no gap in weeks. 

42 """ 

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

44 

45 if event: 

46 if event.recurringId: 

47 recurringId = event.recurringId 

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

49 eventDeleted = False 

50 

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 

71# RepeatingImplementation: Remove function above; remove "NEW" from the function name 

72def NEWdeleteEvent(eventId): 

73 """ 

74 Deletes an event, if it is a repeating event, rename all following events 

75 to make sure there is no gap in weeks. 

76 """ 

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

78 

79 if event: 

80 if event.isRepeating: 

81 seriesId = event.seriesId 

82 repeatingEvents = list(Event.select().where(Event.seriesId==seriesId).order_by(Event.id)) # orders for tests 

83 eventDeleted = False 

84 

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

86 for repeatingEvent in repeatingEvents: 

87 if eventDeleted: 

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

89 newEventName = repeatingEvent.name 

90 

91 if repeatingEvent == event: 

92 newEventName = repeatingEvent.name 

93 eventDeleted = True 

94 

95 program = event.program 

96 

97 if program: 

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

99 else: 

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

101 

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

103 

104def deleteEventAndAllFollowing(eventId): 

105 """ 

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

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

108 """ 

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

110 if event: 

111 if event.recurringId: 

112 recurringId = event.recurringId 

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

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

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

116 return deletedEventList 

117 

118# RepeatingImplementation: Remove function above; remove "NEW" from the function name  

119def NEWdeleteEventAndAllFollowing(eventId): 

120 """ 

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

122 """ 

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

124 if event: 

125 if event.seriesId: 

126 seriesId = event.seriesId 

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

128 deletedEventList = [event.id for events in eventSeries] 

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

130 return deletedEventList 

131 

132def deleteAllRecurringEvents(eventId): 

133 """ 

134 Deletes all recurring events. 

135 Modified to also apply for events with multiple offerings 

136 """ 

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

138 if event: 

139 if event.recurringId: 

140 recurringId = event.recurringId 

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

142 eventId = allRecurringEvents[0].id 

143 return deleteEventAndAllFollowing(eventId) 

144 

145# RepeatingImplementation: Remove function above; remove "NEW" from the function name  

146def deleteAllEventsInSeries(eventId): 

147 """ 

148 Deletes all events in a series 

149 """ 

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

151 if event: 

152 if event.seriesId: 

153 seriesId = event.seriesId 

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

155 eventId = allSeriesEvents[0].id 

156 return deleteEventAndAllFollowing(eventId) 

157 

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

159 """ 

160 Tries to save an event to the database: 

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

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

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

164 

165 Returns: 

166 Created events and an error message. 

167 """ 

168 

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

170 # automatically changed from "" to 0 

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

172 eventData["rsvpLimit"] = None 

173 

174 newEventData = preprocessEventData(eventData) 

175 

176 isValid, validationErrorMessage = validateNewEventData(newEventData) 

177 

178 if not isValid: 

179 return False, validationErrorMessage 

180 

181 try: 

182 events = saveEventToDb(newEventData, renewedEvent) 

183 if attachmentFiles: 

184 for event in events: 

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

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

187 return events, "" 

188 except Exception as e: 

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

190 return False, e 

191 

192# RepeatingImplementation: Remove function above; remove "NEW" from the function name 

193def NEWattemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): 

194 """ 

195 Tries to save an event to the database: 

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

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

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

199 

200 Returns: 

201 Created events and an error message. 

202 """ 

203 

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

205 # automatically changed from "" to 0 

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

207 eventData["rsvpLimit"] = None 

208 

209 newEventData = NEWpreprocessEventData(eventData) 

210 

211 isValid, validationErrorMessage = NEWvalidateNewEventData(newEventData) 

212 

213 if not isValid: 

214 return False, validationErrorMessage 

215 

216 try: 

217 events = NEWsaveEventToDb(newEventData, renewedEvent) 

218 if attachmentFiles: 

219 for event in events: 

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

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

222 return events, "" 

223 except Exception as e: 

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

225 return False, e 

226 

227def saveEventToDb(newEventData, renewedEvent = False): 

228 

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

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

231 

232 

233 isNewEvent = ('id' not in newEventData) 

234 

235 

236 eventsToCreate = [] 

237 recurringSeriesId = None 

238 multipleSeriesId = None 

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

240 eventsToCreate = calculateRecurringEventFrequency(newEventData) 

241 recurringSeriesId = calculateNewrecurringId() 

242 

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

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

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

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

247 "week":1}) 

248 multipleSeriesId = newEventData['multipleOfferingId'] 

249 

250 else: 

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

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

253 "week":1}) 

254 if renewedEvent: 

255 recurringSeriesId = newEventData.get('recurringId') 

256 eventRecords = [] 

257 for eventInstance in eventsToCreate: 

258 with mainDB.atomic(): 

259 

260 eventData = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

275 "contactName": newEventData['contactName'] 

276 } 

277 

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

279 # it is a new event.  

280 if isNewEvent: 

281 eventData['program'] = newEventData['program'] 

282 eventData['recurringId'] = recurringSeriesId 

283 eventData['multipleOfferingId'] = multipleSeriesId 

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

285 eventRecord = Event.create(**eventData) 

286 else: 

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

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

289 

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

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

292 

293 eventRecords.append(eventRecord) 

294 return eventRecords 

295 

296# RepeatingImplementation: Remove function above; remove "NEW" from the function name 

297def NEWsaveEventToDb(newEventData, renewedEvent = False): 

298 

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

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

301 

302 isNewEvent = ('id' not in newEventData) 

303 

304 eventsToCreate = [] 

305 seriesId = None 

306 

307 # RepeatingImplementation: How can I merge this logic? 

308 if (isNewEvent and newEventData['isRepeating']) and not renewedEvent: 

309 eventsToCreate = calculateRepeatingEventFrequency(newEventData) 

310 seriesId = calculateNewSeriesId() 

311 

312 elif(isNewEvent and newEventData['isSeries']) and not renewedEvent: 

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

314 'date':newEventData['startDate'] 

315 }) 

316 

317 else: 

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

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

320 "week":1}) 

321 if renewedEvent: 

322 seriesId = newEventData.get('seriesId') 

323 print (seriesId, "theSeriesId") 

324 eventRecords = [] 

325 print (eventsToCreate, "gbayi") 

326 for eventInstance in eventsToCreate: 

327 with mainDB.atomic(): 

328 

329 eventData = { 

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

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

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

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

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

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

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

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

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

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

340 "isRepeating": newEventData['isRepeating'], 

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

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

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

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

345 "contactName": newEventData['contactName'] 

346 } 

347 

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

349 # it is a new event.  

350 if isNewEvent: 

351 eventData['program'] = newEventData['program'] 

352 eventData['seriesId'] = seriesId if seriesId else newEventData.get('seriesId') 

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

354 eventRecord = Event.create(**eventData) 

355 else: 

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

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

358 

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

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

361 

362 eventRecords.append(eventRecord) 

363 return eventRecords 

364 

365 

366def getStudentLedEvents(term): 

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

368 .join(Program) 

369 .where(Program.isStudentLed, 

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

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

372 .execute()) 

373 

374 programs = {} 

375 

376 for event in studentLedEvents: 

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

378 

379 return programs 

380 

381def getUpcomingStudentLedCount(term, currentTime): 

382 """ 

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

384 """ 

385 

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

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

388 .where(Program.isStudentLed, 

389 Event.term == term, Event.deletionDate == None, 

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

391 Event.isCanceled == False) 

392 .group_by(Program.id)) 

393 

394 programCountDict = {} 

395 

396 for programCount in upcomingCount: 

397 programCountDict[programCount.id] = programCount.eventCount 

398 return programCountDict 

399 

400def getTrainingEvents(term, user): 

401 """ 

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

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

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

405 returned is the All Trainings Event. 

406 term: expected to be the ID of a term 

407 user: expected to be the current user 

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

409 """ 

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

411 .join(Program, JOIN.LEFT_OUTER) 

412 .where(Event.isTraining == True, 

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

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

415 

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

417 if hideBonner: 

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

419 

420 return list(trainingQuery.execute()) 

421 

422def getBonnerEvents(term): 

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

424 .join(Program) 

425 .where(Program.isBonnerScholars, 

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

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

428 .execute()) 

429 return bonnerScholarsEvents 

430 

431def getOtherEvents(term): 

432 """ 

433  

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

435 the Other Events section of the Events List page. 

436 :return: A list of Other Event objects 

437 """ 

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

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

440 

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

442 .join(Program, JOIN.LEFT_OUTER) 

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

444 Event.isTraining == False, 

445 Event.isAllVolunteerTraining == False, 

446 ((Program.isOtherCeltsSponsored) | 

447 ((Program.isStudentLed == False) & 

448 (Program.isBonnerScholars == False)))) 

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

450 .execute()) 

451 

452 return otherEvents 

453 

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

455 """ 

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

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

458 :param user: a username or User object 

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

460 Used in testing, defaults to the current timestamp. 

461 :return: A list of Event objects 

462 """ 

463 

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

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

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

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

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

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

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

471 

472 if program: 

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

474 

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

476 

477 events_list = [] 

478 shown_recurring_event_list = [] 

479 shown_multiple_offering_event_list = [] 

480 

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

482 for event in events: 

483 if event.recurringId or event.multipleOfferingId: 

484 if not event.isCanceled: 

485 if event.recurringId not in shown_recurring_event_list: 

486 events_list.append(event) 

487 shown_recurring_event_list.append(event.recurringId) 

488 if event.multipleOfferingId not in shown_multiple_offering_event_list: 

489 events_list.append(event) 

490 shown_multiple_offering_event_list.append(event.multipleOfferingId) 

491 else: 

492 if not event.isCanceled: 

493 events_list.append(event) 

494 

495 return events_list 

496 

497# RepeatingImplementation: Remove function above; remove "NEW" from the function name 

498def NEWgetUpcomingEventsForUser(user, asOf=datetime.now(), program=None): 

499 """ 

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

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

502 :param user: a username or User object 

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

504 Used in testing, defaults to the current timestamp. 

505 :return: A list of Event objects 

506 """ 

507 

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

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

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

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

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

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

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

515 

516 if program: 

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

518 

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

520 

521 eventsList = [] 

522 seriesEventsList = [] 

523 

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

525 for event in events: 

526 if event.seriesId: 

527 if not event.isCanceled: 

528 if event.seriesId not in seriesEventsList: 

529 eventsList.append(event) 

530 seriesEventsList.append(event.seriesId) 

531 else: 

532 if not event.isCanceled: 

533 eventsList.append(event) 

534 

535 return eventsList 

536 

537def getParticipatedEventsForUser(user): 

538 """ 

539 Get all the events a user has participated in. 

540 :param user: a username or User object 

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

542 Used in testing, defaults to the current timestamp. 

543 :return: A list of Event objects 

544 """ 

545 

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

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

548 .join(EventParticipant) 

549 .where(EventParticipant.user == user, 

550 Event.isAllVolunteerTraining == False) 

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

552 

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

554 .join(EventParticipant) 

555 .where(Event.isAllVolunteerTraining == True, 

556 EventParticipant.user == user)) 

557 union = participatedEvents.union_all(allVolunteer) 

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

559 

560 return unionParticipationWithVolunteer 

561 

562def validateNewEventData(data): 

563 """ 

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

565 

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

567 

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

569 """ 

570 

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

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

573 

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

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

576 

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

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

579 

580 # Validation if we are inserting a new event 

581 if 'id' not in data: 

582 

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

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

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

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

587 

588 sameEventListCopy = sameEventList.copy() 

589 

590 for event in sameEventListCopy: 

591 if event.isCanceled or event.recurringId: 

592 sameEventList.remove(event) 

593 

594 try: 

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

596 except DoesNotExist as e: 

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

598 if sameEventList: 

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

600 

601 data['valid'] = True 

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

603 

604# RepeatingImplementation: Remove function above; remove "NEW" from the function name 

605def NEWvalidateNewEventData(data): 

606 """ 

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

608 

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

610 

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

612 """ 

613 

614 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isService'], data['isRepeating'], data['isSeries']]: 

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

616 

617 if data['isRepeating'] and data['endDate'] < data['startDate']: 

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

619 

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

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

622 

623 # Validation if we are inserting a new event 

624 if 'id' not in data: 

625 

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

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

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

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

630 

631 sameEventListCopy = sameEventList.copy() 

632 

633 for event in sameEventListCopy: 

634 if event.isCanceled or event.seriesId: 

635 sameEventList.remove(event) 

636 

637 try: 

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

639 except DoesNotExist as e: 

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

641 if sameEventList: 

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

643 

644 data['valid'] = True 

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

646 

647def calculateNewrecurringId(): 

648 """ 

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

650 """ 

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

652 if recurringId: 

653 return recurringId + 1 

654 else: 

655 return 1 

656def calculateNewMultipleOfferingId(): 

657 """ 

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

659 """ 

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

661 if multipleOfferingId: 

662 return multipleOfferingId + 1 

663 else: 

664 return 1 

665 

666# RepeatingImplementation: Remove function above 

667def calculateNewSeriesId(): 

668 """ 

669 Gets the max series ID to determine the ID for a new series. 

670 """ 

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

672 if maxSeriesId: 

673 return maxSeriesId + 1 

674 return 1 

675 

676def getPreviousRecurringEventData(recurringId): 

677 """ 

678 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 

679 """ 

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

681 .join(EventParticipant) 

682 .join(Event) 

683 .where(Event.recurringId==recurringId)) 

684 return previousEventVolunteers 

685 

686# RepeatingImplementation: remove function above 

687def getPreviousRepeatingEventData(seriesId): 

688 """ 

689 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 

690 """ 

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

692 .join(EventParticipant) 

693 .join(Event) 

694 .where(Event.seriesId==seriesId)) 

695 return previousEventVolunteers 

696 

697def calculateRecurringEventFrequency(event): 

698 """ 

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

700 dictionary of event data. 

701 

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

703 

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

705 """ 

706 if not isinstance(event['endDate'], date) or not isinstance(event['startDate'], date): 

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

708 

709 if event['endDate'] == event['startDate']: 

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

711 

712 return [ {'name': f"{event['name']} Week {counter+1}", 

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

714 "week": counter+1} 

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

716 

717# RepeatingImplementation: remove function above 

718def calculateRepeatingEventFrequency(event): 

719 """ 

720 Calculate the events to create based on a repating event start and end date. Takes a 

721 dictionary of event data. 

722 

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

724 

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

726 """ 

727 if not isinstance(event['endDate'], date) or not isinstance(event['startDate'], date): 

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

729 

730 if event['endDate'] == event['startDate']: 

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

732 

733 return [ {'name': f"{event['name']} Week {counter+1}", 

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

735 "isRepeating": True, "week": counter+1} 

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

737 

738def preprocessEventData(eventData): 

739 """ 

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

741 

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

743 - checkboxes should be True or False 

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

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

746 - Look up matching certification requirement if necessary 

747 """ 

748 ## Process checkboxes 

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

750 

751 for checkBox in eventCheckBoxes: 

752 if checkBox not in eventData: 

753 eventData[checkBox] = False 

754 else: 

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

756 

757 ## Process dates 

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

759 for eventDate in eventDates: 

760 if eventDate not in eventData: 

761 eventData[eventDate] = '' 

762 elif type(eventData[eventDate]) is str and eventData[eventDate]: 

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

764 elif not isinstance(eventData[eventDate], date): 

765 eventData[eventDate] = '' 

766 

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

768 if not eventData['isRecurring']: 

769 eventData['endDate'] = eventData['startDate'] 

770 

771 # Process terms 

772 if 'term' in eventData: 

773 try: 

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

775 except DoesNotExist: 

776 eventData['term'] = '' 

777 

778 # Process requirement 

779 if 'certRequirement' in eventData: 

780 try: 

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

782 except DoesNotExist: 

783 eventData['certRequirement'] = '' 

784 elif 'id' in eventData: 

785 # look up requirement 

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

787 if match: 

788 eventData['certRequirement'] = match.requirement 

789 if 'timeStart' in eventData: 

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

791 

792 if 'timeEnd' in eventData: 

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

794 

795 return eventData 

796 

797# RepeatingImplementation: Remove function above; remove "NEW" from the function name 

798def NEWpreprocessEventData(eventData): 

799 """ 

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

801 

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

803 - checkboxes should be True or False 

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

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

806 - Look up matching certification requirement if necessary 

807 """ 

808 ## Process checkboxes 

809 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isRepeating', 'isSeries', 'isAllVolunteerTraining'] 

810 

811 for checkBox in eventCheckBoxes: 

812 if checkBox not in eventData: 

813 eventData[checkBox] = False 

814 else: 

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

816 

817 ## Process dates 

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

819 for eventDate in eventDates: 

820 if eventDate not in eventData: 

821 eventData[eventDate] = '' 

822 elif type(eventData[eventDate]) is str and eventData[eventDate]: 

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

824 elif not isinstance(eventData[eventDate], date): 

825 eventData[eventDate] = '' 

826 

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

828 if not eventData['isRepeating']: 

829 eventData['endDate'] = eventData['startDate'] 

830 

831 # Process terms 

832 if 'term' in eventData: 

833 try: 

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

835 except DoesNotExist: 

836 eventData['term'] = '' 

837 

838 # Process requirement 

839 if 'certRequirement' in eventData: 

840 try: 

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

842 except DoesNotExist: 

843 eventData['certRequirement'] = '' 

844 elif 'id' in eventData: 

845 # look up requirement 

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

847 if match: 

848 eventData['certRequirement'] = match.requirement 

849 if 'timeStart' in eventData: 

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

851 

852 if 'timeEnd' in eventData: 

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

854 

855 return eventData 

856 

857def getTomorrowsEvents(): 

858 """Grabs each event that occurs tomorrow""" 

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

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

861 return events 

862 

863def addEventView(viewer,event): 

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

865 if not viewer.isCeltsAdmin: 

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

867 

868def getEventRsvpCountsForTerm(term): 

869 """ 

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

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

872 current RSVPs to that event as the pair. 

873 """ 

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

875 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

877 .group_by(Event.id)) 

878 

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

880 

881 return amountAsDict 

882 

883def getEventRsvpCount(eventId): 

884 """ 

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

886 """ 

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

888 

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

890 """ 

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

892 until the start of the event. 

893 

894 Note about dates: 

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

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

897 tomorrow with no mention of the hour.  

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

899 and hours in actual time. 

900 

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

902 relative to this morning and exclude all hours and minutes 

903 

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

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

906 """ 

907 

908 if currentDatetime is None: 

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

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

911 

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

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

914 

915 if eventEnd < currentDatetime: 

916 return "Already passed" 

917 elif eventStart <= currentDatetime <= eventEnd: 

918 return "Happening now" 

919 

920 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

921 calendarDelta = relativedelta(eventStart, currentMorning) 

922 calendarYearsUntilEvent = calendarDelta.years 

923 calendarMonthsUntilEvent = calendarDelta.months 

924 calendarDaysUntilEvent = calendarDelta.days 

925 

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

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

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

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

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

931 

932 # Years until 

933 if calendarYearsUntilEvent: 

934 if calendarMonthsUntilEvent: 

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

936 return f"{yearString}" 

937 # Months until 

938 if calendarMonthsUntilEvent: 

939 if calendarDaysUntilEvent: 

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

941 return f"{monthString}" 

942 # Days until 

943 if calendarDaysUntilEvent: 

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

945 if calendarDaysUntilEvent == 1: 

946 return "Tomorrow" 

947 return f"{dayString}" 

948 if timeUntilEvent.hours: 

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

950 return f"{dayString}" 

951 # Hours until 

952 if timeUntilEvent.hours: 

953 if timeUntilEvent.minutes: 

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

955 return f"{hourString}" 

956 # Minutes until 

957 elif timeUntilEvent.minutes > 1: 

958 return f"{minuteString}" 

959 # Seconds until 

960 return "<1 minute" 

961 

962def copyRsvpToNewEvent(priorEvent, newEvent): 

963 """ 

964 Copies rvsps from priorEvent to newEvent 

965 """ 

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

967 

968 for student in rsvpInfo: 

969 newRsvp = EventRsvp( 

970 user = student.user, 

971 event = newEvent, 

972 rsvpWaitlist = student.rsvpWaitlist 

973 ) 

974 newRsvp.save() 

975 numRsvps = len(rsvpInfo) 

976 if numRsvps: 

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