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

350 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-08-24 19:35 +0000

1from flask import url_for, g, session 

2from peewee import DoesNotExist, fn, JOIN, Case, Value 

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 

20from app.models.eventCohort import EventCohort 

21 

22from app.logic.bonner import rsvpForBonnerCohort, addBonnerCohortToRsvpLog 

23from app.logic.createLogs import createActivityLog, createRsvpLog 

24from app.logic.utils import format24HourTime 

25from app.logic.fileHandler import FileHandler 

26from app.logic.certification import updateCertRequirementForEvent 

27 

28def cancelEvent(eventId): 

29 """ 

30 Cancels an event. 

31 """ 

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

33 if event: 

34 event.isCanceled = True 

35 event.save() 

36 

37 program = event.program 

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

39 

40#NEEDS FIXING: process not working properly for repeating events when two events are deleted consecutively 

41def deleteEvent(eventId): 

42 """ 

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

44 to make sure there is no gap in weeks. 

45  

46 """ 

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

48 

49 if event: 

50 if event.isRepeating: 

51 seriesId = event.seriesId 

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

53 eventDeleted = False 

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

55 for repeatingEvent in repeatingEvents: 

56 if eventDeleted: 

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

58 newEventName = repeatingEvent.name 

59 

60 if repeatingEvent == event: 

61 newEventName = repeatingEvent.name 

62 eventDeleted = True 

63 

64 program = event.program 

65 

66 if program: 

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

68 else: 

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

70 

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

72 

73def deleteEventAndAllFollowing(eventId): 

74 """ 

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

76  

77 """ 

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

79 if event: 

80 if event.seriesId: 

81 seriesId = event.seriesId 

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

83 deletedEventList = [event.id for event in eventSeries] 

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

85 return deletedEventList 

86 

87def deleteAllEventsInSeries(eventId): 

88 """ 

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

90  

91 """ 

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

93 if event: 

94 if event.seriesId: 

95 seriesId = event.seriesId 

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

97 eventId = allSeriesEvents[0].id 

98 return deleteEventAndAllFollowing(eventId) 

99 else: 

100 raise ValueError(f"Event with id {event.id} does not belong to a series (seriesId is None).") 

101 

102def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): 

103 """ 

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

105 Creates separate event data inheriting from the original eventData 

106 with the specifics of each offering. 

107 Calls attemptSaveEvent on each of the newly created datum 

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

109 

110 Returns: 

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

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

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

114 """ 

115 savedOfferings = [] 

116 failedSavedOfferings = [] 

117 allSavesWereSuccessful = True 

118 

119 seriesId = calculateNewSeriesId() 

120 

121 # Create separate event data for each event in the series, inheriting from the original eventData 

122 

123 # Reformat dates from Jul 10, 2026 to 2026-07-10 for easier sorting 

124 eventData2 = [] 

125 for ed in eventData['seriesData']: 

126 try: 

127 ed['eventDate'] = datetime.strptime(ed['eventDate'], "%b %d, %Y").strftime("%Y-%m-%d") 

128 except: 

129 pass # eventDate comes into the system differently for recurring weekly events and recurring events (non-weekly). This should format it correctly for both cases 

130 eventData2.append(ed) 

131 eventData['seriesData'] = eventData2 

132 

133 seriesData = sorted(eventData.get('seriesData'), key=lambda x: datetime.strptime(x['eventDate'].split(' ')[0] + ' ' + x['startTime'], '%Y-%m-%d %H:%M')) 

134 # sorts the events in the series by date and time so that the events are created in order and the naming convention of Week 1, Week 2, etc. is consistent with the order of the events. 

135 isRepeating = bool(eventData.get('isRepeating')) 

136 with mainDB.atomic() as transaction: 

137 for index, event in enumerate(seriesData): 

138 eventInfo = eventData.copy() 

139 eventInfo.update({ 

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

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

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

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

144 'location': event['eventLocation'], 

145 'seriesId': seriesId, 

146 'isRepeating': bool(isRepeating), 

147 }) 

148 # Try to save each offering 

149 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles) 

150 if validationErrorMessage: 

151 failedSavedOfferings.append((index, validationErrorMessage)) 

152 allSavesWereSuccessful = False 

153 else: 

154 savedEvent = savedEvents[0] 

155 savedOfferings.append(savedEvent) 

156 if not allSavesWereSuccessful: 

157 savedOfferings = [] 

158 transaction.rollback() 

159 

160 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

161 

162 

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

164 """ 

165 Tries to save an event to the database: 

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

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

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

169 

170 Returns: 

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

172 """ 

173 

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

175 # automatically changed from "" to 0 

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

177 eventData["rsvpLimit"] = None 

178 newEventData = preprocessEventData(eventData) 

179 

180 isValid, validationErrorMessage = validateNewEventData(newEventData) 

181 if not isValid: 

182 return [], validationErrorMessage 

183 

184 events = saveEventToDb(newEventData, renewedEvent) 

185 if attachmentFiles: 

186 for event in events: 

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

188 addFile.saveFiles(parentEvent=events[0]) 

189 return events, "" 

190 

191 

192def saveEventToDb(newEventData, renewedEvent = False): 

193 

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

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

196 

197 isNewEvent = ('id' not in newEventData) 

198 eventRecords = [] 

199 with mainDB.atomic(): 

200 eventData = { 

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

202 "name": newEventData['name'], 

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

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

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

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

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

208 "isLaborOnly" : newEventData['isLaborOnly'], 

209 "allowsLabor" : newEventData['allowsLabor'], 

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

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

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

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

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

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

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

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

218 } 

219 

220 # These fields below are only relevant during event creation so we only set/change them when  

221 # it is a new event.  

222 if isNewEvent: 

223 eventData['program'] = newEventData['program'] 

224 eventData['seriesId'] = newEventData.get('seriesId') 

225 eventData['isRepeating'] = bool(newEventData.get('isRepeating')) 

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

227 eventData["isCeltsTraining"] = bool(newEventData.get('isCeltsTraining', False)) 

228 eventRecord = Event.create(**eventData) 

229 else: 

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

231 for key, value in eventData.items(): 

232 setattr(eventRecord, key, value) 

233 eventRecord.save() 

234 

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

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

237 

238 eventRecords.append(eventRecord) 

239 return eventRecords 

240 

241def getVolunteerOpportunities(term): 

242 volunteerOpportunities = list(Event.select(Event, Program) 

243 .join(Program) 

244 .where((Event.term == term) & 

245 (Event.deletionDate.is_null()) & 

246 (Event.isService == True) & 

247 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null()) 

248 ) 

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

250 .execute()) 

251 

252 programs = {} 

253 

254 for event in volunteerOpportunities: 

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

256 

257 return programs 

258 

259def getEngagementEvents(term): 

260 engagementEvents = list(Event.select(Event, Program) 

261 .join(Program) 

262 .where(Event.isEngagement, Event.isLaborOnly == False, 

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

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

265 .execute()) 

266 return engagementEvents 

267 

268def getUpcomingVolunteerOpportunitiesCount(term, currentDate): 

269 """ 

270 Return a count of all upcoming events for each volunteer opportunitiesprogram. 

271 """ 

272 

273 upcomingCount = ( 

274 Program 

275 .select(Program.id, fn.COUNT(Event.id).alias("eventCount")) 

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

277 .where( 

278 (Event.term == term) & 

279 (Event.deletionDate.is_null(True)) & 

280 (Event.isService == True) & 

281 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) & 

282 ((Event.startDate > currentDate) | 

283 ((Event.startDate == currentDate) & (Event.timeEnd >= currentDate.time()))) & 

284 (Event.isCanceled == False) 

285 ) 

286 .group_by(Program.id) 

287 ) 

288 

289 programCountDict = {} 

290 for programCount in upcomingCount: 

291 programCountDict[programCount.id] = programCount.eventCount 

292 return programCountDict 

293 

294def getPastVolunteerOpportunitiesCount(term, currentDate): 

295 """ 

296 Return a count of all past events for each volunteer opportunities program. 

297 """ 

298 

299 pastCount = ( 

300 Program 

301 .select(Program.id, fn.COUNT(Event.id).alias("eventCount")) 

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

303 .where( 

304 (Event.term == term) & 

305 (Event.deletionDate.is_null(True)) & 

306 (Event.isService == True) & 

307 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) & 

308 ((Event.startDate < currentDate) | 

309 ((Event.startDate == currentDate) & (Event.timeStart <= currentDate.time()))) & 

310 (Event.isCanceled == False) 

311 ) 

312 .group_by(Program.id) 

313 ) 

314 

315 programCountDict = {} 

316 for programCount in pastCount: 

317 programCountDict[programCount.id] = programCount.eventCount 

318 return programCountDict 

319 

320def getTrainingEvents(term, user): 

321 """ 

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

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

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

325 returned is the All Trainings Event. 

326 term: expected to be the ID of a term 

327 user: expected to be the current user 

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

329 """ 

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

331 .join(Program, JOIN.LEFT_OUTER) 

332 .where(Event.isTraining == True, Event.isLaborOnly == False, 

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

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

335 

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

337 if hideBonner: 

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

339 

340 return list(trainingQuery.execute()) 

341 

342def getBonnerEvents(term): 

343 bonnerScholarsEvents = list( 

344 Event.select(Event, Program.id.alias("program_id")) 

345 .join(Program) 

346 .where( 

347 Program.isBonnerScholars, 

348 Event.term == term, 

349 Event.deletionDate == None 

350 ) 

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

352 .execute() 

353 ) 

354 return bonnerScholarsEvents 

355 

356def getCeltsLabor(term): 

357 """ 

358 Labor tab: events explicitly marked as Labor Only. 

359 """ 

360 celtsLabor = list(Event.select() 

361 .where(Event.term == term, Event.deletionDate == None, Event.isLaborOnly == True) 

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

363 .execute()) 

364 return celtsLabor 

365 

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

367 """ 

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

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

370 :param user: a username or User object 

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

372 Used in testing, defaults to the current timestamp. 

373 :return: A list of Event objects 

374 """ 

375 

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

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

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

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

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

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

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

383 

384 if program: 

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

386 

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

388 

389 eventsList = [] 

390 seriesEventsList = [] 

391 

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

393 for event in events: 

394 if event.seriesId: 

395 if not event.isCanceled: 

396 if event.seriesId not in seriesEventsList: 

397 eventsList.append(event) 

398 seriesEventsList.append(event.seriesId) 

399 else: 

400 if not event.isCanceled: 

401 eventsList.append(event) 

402 

403 return eventsList 

404 

405def getParticipatedEventsForUser(user): 

406 """ 

407 Get all the events a user has participated in. 

408 :param user: a username or User object 

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

410 Used in testing, defaults to the current timestamp. 

411 :return: A list of Event objects 

412 """ 

413 

414 # Does this handle labor only and/or includes labor events? 

415 participatedEvents = (Event.select(Event, Program.programName, Case(None, ( 

416 ((Event.allowsLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"), 

417 ((Event.allowsLabor | Event.isLaborOnly | Event.name.contains("Labor")), "Labor"), 

418 (Event.isService, "Volunteer")), "Attendee").alias("participatedType")) 

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

420 .join(EventParticipant) 

421 .where(EventParticipant.user == user, 

422 Event.isAllVolunteerTraining == False, Event.deletionDate == None, Event.isCeltsTraining == False) 

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

424 allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType")) 

425 .join(EventParticipant) 

426 .where(Event.isAllVolunteerTraining == True, 

427 EventParticipant.user == user)) 

428 union = participatedEvents.union_all(allVolunteer) 

429 unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType).order_by(union.c.startDate, union.c.name).execute()) 

430 return unionParticipationWithVolunteer 

431 

432def validateNewEventData(data): 

433 """ 

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

435 

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

437 

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

439 """ 

440 

441 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['allowsLabor']]: 

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

443 

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

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

446 

447 # Validation if we are inserting a new event 

448 if 'id' not in data: 

449 

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

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

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

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

454 

455 sameEventListCopy = sameEventList.copy() 

456 

457 for event in sameEventListCopy: 

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

459 sameEventList.remove(event) 

460 

461 try: 

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

463 except DoesNotExist as e: 

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

465 if sameEventList: 

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

467 

468 data['valid'] = True 

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

470 

471def calculateNewSeriesId(): 

472 """ 

473 Gets the max series ID so that new seriesId can be assigned. 

474 """ 

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

476 if maxSeriesId: 

477 return maxSeriesId + 1 

478 return 1 

479 

480def getPreviousSeriesEventData(seriesId): 

481 """ 

482 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. 

483  

484 """ 

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

486 .join(EventParticipant) 

487 .join(Event) 

488 .where(Event.seriesId==seriesId)) 

489 return previousEventVolunteers 

490 

491def getRepeatingEventsData(eventData): 

492 """ 

493 Calculate the events to create based on a repeating event start and end date. Takes a 

494 dictionary of event data. 

495  

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

497 

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

499 """ 

500 

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

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

503 "week": counter+1, 

504 'location': eventData['location'] 

505 } 

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

507 

508def preprocessEventData(eventData): 

509 """ 

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

511 

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

513 - checkboxes should be True or False 

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

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

516 - seriesData should be a JSON string 

517 - Look up matching certification requirement if necessary 

518 """ 

519 

520 ## Process checkboxes and templateData 

521 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'allowsLabor', 'isLaborOnly', 'isCeltsTraining'] 

522 

523 for checkBox in eventCheckBoxes: 

524 if checkBox not in eventData: 

525 eventData[checkBox] = False 

526 else: 

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

528 

529 ## Process dates 

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

531 for eventDate in eventDates: 

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

533 eventData[eventDate] = '' 

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

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

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

537 eventData[eventDate] = '' 

538 

539 # Process seriesData 

540 if 'seriesData' not in eventData: 

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

542 

543 # Process terms 

544 if 'term' in eventData: 

545 try: 

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

547 except DoesNotExist: 

548 eventData['term'] = '' 

549 

550 # Process requirement 

551 if 'certRequirement' in eventData: 

552 try: 

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

554 except DoesNotExist: 

555 eventData['certRequirement'] = '' 

556 elif 'id' in eventData: 

557 # look up requirement 

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

559 if match: 

560 eventData['certRequirement'] = match.requirement 

561 if 'timeStart' in eventData: 

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

563 

564 if 'timeEnd' in eventData: 

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

566 

567 return eventData 

568 

569def getTomorrowsEvents(): 

570 """Grabs each event that occurs tomorrow""" 

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

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

573 return events 

574 

575def addEventView(viewer,event): 

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

577 if not viewer.isCeltsAdmin: 

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

579 

580def getEventRsvpCountsForTerm(term): 

581 """ 

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

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

584 current RSVPs to that event as the pair. 

585 """ 

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

587 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

589 .group_by(Event.id)) 

590 

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

592 

593 return amountAsDict 

594 

595def getEventRsvpCount(eventId): 

596 """ 

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

598 """ 

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

600 

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

602 """ 

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

604 until the start of the event. 

605 

606 Note about dates: 

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

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

609 tomorrow with no mention of the hour.  

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

611 and hours in actual time. 

612 

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

614 relative to this morning and exclude all hours and minutes 

615 

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

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

618 """ 

619 

620 if currentDatetime is None: 

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

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

623 

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

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

626 

627 if eventEnd < currentDatetime: 

628 return "Already passed" 

629 elif eventStart <= currentDatetime <= eventEnd: 

630 return "Happening now" 

631 

632 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

633 calendarDelta = relativedelta(eventStart, currentMorning) 

634 calendarYearsUntilEvent = calendarDelta.years 

635 calendarMonthsUntilEvent = calendarDelta.months 

636 calendarDaysUntilEvent = calendarDelta.days 

637 

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

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

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

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

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

643 

644 # Years until 

645 if calendarYearsUntilEvent: 

646 if calendarMonthsUntilEvent: 

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

648 return f"{yearString}" 

649 # Months until 

650 if calendarMonthsUntilEvent: 

651 if calendarDaysUntilEvent: 

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

653 return f"{monthString}" 

654 # Days until 

655 if calendarDaysUntilEvent: 

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

657 if calendarDaysUntilEvent == 1: 

658 return "Tomorrow" 

659 return f"{dayString}" 

660 if timeUntilEvent.hours: 

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

662 return f"{dayString}" 

663 # Hours until 

664 if timeUntilEvent.hours: 

665 if timeUntilEvent.minutes: 

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

667 return f"{hourString}" 

668 # Minutes until 

669 elif timeUntilEvent.minutes > 1: 

670 return f"{minuteString}" 

671 # Seconds until 

672 return "<1 minute" 

673 

674def copyRsvpToNewEvent(priorEvent, newEvent): 

675 """ 

676 Copies rvsps from priorEvent to newEvent 

677 """ 

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

679 

680 for student in rsvpInfo: 

681 newRsvp = EventRsvp( 

682 user = student.user, 

683 event = newEvent, 

684 rsvpWaitlist = student.rsvpWaitlist 

685 ) 

686 newRsvp.save() 

687 numRsvps = len(rsvpInfo) 

688 if numRsvps: 

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

690 

691 

692def inviteCohortsToEvent(event, cohortYears): 

693 """ 

694 Invites cohorts to a newly created event by associating the cohorts directly. 

695 """ 

696 invitedCohorts = [] 

697 try: 

698 for year in cohortYears: 

699 year = int(year) 

700 EventCohort.get_or_create( 

701 event=event, 

702 year=year, 

703 defaults={'invited_at': datetime.now()} 

704 ) 

705 

706 addBonnerCohortToRsvpLog(year, event.id) 

707 rsvpForBonnerCohort(year, event.id) 

708 invitedCohorts.append(year) 

709 

710 if invitedCohorts: 

711 cohortList = ', '.join(map(str, invitedCohorts)) 

712 createActivityLog(f"Added Bonner cohorts {cohortList} for newly created event {event.name}") 

713 

714 return True, "Cohorts successfully added to new event", invitedCohorts 

715 

716 except Exception as e: 

717 print(f"Error inviting cohorts to new event: {e}") 

718 return False, f"Error adding cohorts to new event: {e}", [] 

719 

720def updateEventCohorts(event, cohortYears): 

721 """ 

722 Updates the cohorts for an existing event by adding new ones and removing outdated ones. 

723 """ 

724 invitedCohorts = [] 

725 try: 

726 precedentInvitedCohorts = list(EventCohort.select().where(EventCohort.event == event)) 

727 precedentInvitedYears = [precedentCohort.year for precedentCohort in precedentInvitedCohorts] 

728 yearsToAdd = [year for year in cohortYears if int(year) not in precedentInvitedYears] 

729 

730 for year in yearsToAdd: 

731 EventCohort.get_or_create( 

732 event=event, 

733 year=year, 

734 defaults={'invited_at': datetime.now()} 

735 ) 

736 

737 addBonnerCohortToRsvpLog(year, event.id) 

738 rsvpForBonnerCohort(year, event.id) 

739 invitedCohorts.append(year) 

740 

741 if yearsToAdd: 

742 cohortList = ', '.join(map(str, invitedCohorts)) 

743 createActivityLog(f"Updated Bonner cohorts for event {event.name}. Added: {yearsToAdd}") 

744 

745 return True, "Cohorts successfully updated for event", invitedCohorts 

746 

747 except Exception as e: 

748 print(f"Error updating cohorts for event: {e}") 

749 return False, f"Error updating cohorts for event: {e}", []