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

338 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-02-27 13:40 +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 

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 seriesData = sorted(eventData.get('seriesData'), key=lambda x: datetime.strptime(f"{x['eventDate']} {x['startTime']}",'%Y-%m-%d %H:%M')) 

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

125 with mainDB.atomic() as transaction: 

126 for index, event in enumerate(seriesData): 

127 eventInfo = eventData.copy() 

128 eventInfo.update({ 

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

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

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

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

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

134 'seriesId': seriesId, 

135 'isRepeating': bool(isRepeating), 

136 }) 

137 # Try to save each offering 

138 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles) 

139 if validationErrorMessage: 

140 failedSavedOfferings.append((index, validationErrorMessage)) 

141 allSavesWereSuccessful = False 

142 else: 

143 savedEvent = savedEvents[0] 

144 savedOfferings.append(savedEvent) 

145 if not allSavesWereSuccessful: 

146 savedOfferings = [] 

147 transaction.rollback() 

148 

149 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

150 

151 

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

153 """ 

154 Tries to save an event to the database: 

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

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

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

158 

159 Returns: 

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

161 """ 

162 

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

164 # automatically changed from "" to 0 

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

166 eventData["rsvpLimit"] = None 

167 

168 newEventData = preprocessEventData(eventData) 

169 

170 isValid, validationErrorMessage = validateNewEventData(newEventData) 

171 if not isValid: 

172 return [], validationErrorMessage 

173 

174 events = saveEventToDb(newEventData, renewedEvent) 

175 if attachmentFiles: 

176 for event in events: 

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

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

179 return events, "" 

180 

181 

182def saveEventToDb(newEventData, renewedEvent = False): 

183 

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

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

186 

187 isNewEvent = ('id' not in newEventData) 

188 

189 eventRecords = [] 

190 with mainDB.atomic(): 

191 

192 eventData = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

209 } 

210 

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

212 # it is a new event.  

213 if isNewEvent: 

214 eventData['program'] = newEventData['program'] 

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

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

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

218 eventRecord = Event.create(**eventData) 

219 else: 

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

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

222 

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

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

225 

226 eventRecords.append(eventRecord) 

227 return eventRecords 

228 

229def getVolunteerOpportunities(term): 

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

231 .join(Program) 

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

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

234 (Event.isService == True) & 

235 ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) 

236 ) 

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

238 .execute()) 

239 

240 programs = {} 

241 

242 for event in volunteerOpportunities: 

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

244 

245 return programs 

246 

247def getEngagementEvents(term): 

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

249 .join(Program) 

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

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

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

253 .execute()) 

254 return engagementEvents 

255 

256def getUpcomingVolunteerOpportunitiesCount(term, currentTime): 

257 """ 

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

259 """ 

260 

261 upcomingCount = ( 

262 Program 

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

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

265 .where( 

266 (Event.term == term) & 

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

268 (Event.isService == True) & 

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

270 ((Event.startDate > currentTime) | 

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

272 (Event.isCanceled == False) 

273 ) 

274 .group_by(Program.id) 

275 ) 

276 

277 programCountDict = {} 

278 for programCount in upcomingCount: 

279 programCountDict[programCount.id] = programCount.eventCount 

280 return programCountDict 

281 

282def getTrainingEvents(term, user): 

283 """ 

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

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

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

287 returned is the All Trainings Event. 

288 term: expected to be the ID of a term 

289 user: expected to be the current user 

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

291 """ 

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

293 .join(Program, JOIN.LEFT_OUTER) 

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

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

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

297 

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

299 if hideBonner: 

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

301 

302 return list(trainingQuery.execute()) 

303 

304def getBonnerEvents(term): 

305 bonnerScholarsEvents = list( 

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

307 .join(Program) 

308 .where( 

309 Program.isBonnerScholars, 

310 Event.term == term, 

311 Event.deletionDate == None 

312 ) 

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

314 .execute() 

315 ) 

316 return bonnerScholarsEvents 

317 

318def getCeltsLabor(term): 

319 """ 

320 Labor tab: events explicitly marked as Labor Only. 

321 """ 

322 celtsLabor = list(Event.select() 

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

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

325 .execute()) 

326 return celtsLabor 

327 

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

329 """ 

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

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

332 :param user: a username or User object 

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

334 Used in testing, defaults to the current timestamp. 

335 :return: A list of Event objects 

336 """ 

337 

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

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

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

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

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

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

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

345 

346 if program: 

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

348 

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

350 

351 eventsList = [] 

352 seriesEventsList = [] 

353 

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

355 for event in events: 

356 if event.seriesId: 

357 if not event.isCanceled: 

358 if event.seriesId not in seriesEventsList: 

359 eventsList.append(event) 

360 seriesEventsList.append(event.seriesId) 

361 else: 

362 if not event.isCanceled: 

363 eventsList.append(event) 

364 

365 return eventsList 

366 

367def excludeLaborMeeting(): 

368 eventName = fn.LOWER(Event.name) 

369 eventDescription = fn.LOWER(Event.description) 

370 

371 return ( 

372 eventName.contains("labor meeting") | 

373 eventName.contains("celts labor meeting") | 

374 eventDescription.contains("labor meeting") 

375 ) 

376 

377def getParticipatedEventsForUser(user): 

378 """ 

379 Get all the events a user has participated in. 

380 :param user: a username or User object 

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

382 Used in testing, defaults to the current timestamp. 

383 :return: A list of Event objects 

384 """ 

385 

386 excLaborMeeting = excludeLaborMeeting() 

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, Event.deletionDate == None, ~((Event.isLaborOnly == True) & excLaborMeeting), ~((Event.isTraining == True) & excLaborMeeting)) 

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, ~((Event.isLaborOnly == True) & excLaborMeeting), ~((Event.isTraining == True) & excLaborMeeting))) 

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['isRepeating'], data['isLaborOnly']]: 

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

414 

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

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

417 

418 # Validation if we are inserting a new event 

419 if 'id' not in data: 

420 

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

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

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

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

425 

426 sameEventListCopy = sameEventList.copy() 

427 

428 for event in sameEventListCopy: 

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

430 sameEventList.remove(event) 

431 

432 try: 

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

434 except DoesNotExist as e: 

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

436 if sameEventList: 

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

438 

439 data['valid'] = True 

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

441 

442def calculateNewSeriesId(): 

443 """ 

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

445 """ 

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

447 if maxSeriesId: 

448 return maxSeriesId + 1 

449 return 1 

450 

451def getPreviousSeriesEventData(seriesId): 

452 """ 

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

454  

455 """ 

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

457 .join(EventParticipant) 

458 .join(Event) 

459 .where(Event.seriesId==seriesId)) 

460 return previousEventVolunteers 

461 

462def getRepeatingEventsData(eventData): 

463 """ 

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

465 dictionary of event data. 

466  

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

468 

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

470 """ 

471 

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

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

474 "week": counter+1, 

475 'location': eventData['location'] 

476 } 

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

478 

479def preprocessEventData(eventData): 

480 """ 

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

482 

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

484 - checkboxes should be True or False 

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

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

487 - seriesData should be a JSON string 

488 - Look up matching certification requirement if necessary 

489 """ 

490 ## Process checkboxes 

491 eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'isLaborOnly'] 

492 

493 for checkBox in eventCheckBoxes: 

494 if checkBox not in eventData: 

495 eventData[checkBox] = False 

496 else: 

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

498 

499 ## Process dates 

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

501 for eventDate in eventDates: 

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

503 eventData[eventDate] = '' 

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

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

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

507 eventData[eventDate] = '' 

508 

509 # Process seriesData 

510 if 'seriesData' not in eventData: 

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

512 

513 # Process terms 

514 if 'term' in eventData: 

515 try: 

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

517 except DoesNotExist: 

518 eventData['term'] = '' 

519 

520 # Process requirement 

521 if 'certRequirement' in eventData: 

522 try: 

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

524 except DoesNotExist: 

525 eventData['certRequirement'] = '' 

526 elif 'id' in eventData: 

527 # look up requirement 

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

529 if match: 

530 eventData['certRequirement'] = match.requirement 

531 if 'timeStart' in eventData: 

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

533 

534 if 'timeEnd' in eventData: 

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

536 

537 return eventData 

538 

539def getTomorrowsEvents(): 

540 """Grabs each event that occurs tomorrow""" 

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

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

543 return events 

544 

545def addEventView(viewer,event): 

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

547 if not viewer.isCeltsAdmin: 

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

549 

550def getEventRsvpCountsForTerm(term): 

551 """ 

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

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

554 current RSVPs to that event as the pair. 

555 """ 

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

557 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

559 .group_by(Event.id)) 

560 

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

562 

563 return amountAsDict 

564 

565def getEventRsvpCount(eventId): 

566 """ 

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

568 """ 

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

570 

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

572 """ 

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

574 until the start of the event. 

575 

576 Note about dates: 

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

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

579 tomorrow with no mention of the hour.  

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

581 and hours in actual time. 

582 

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

584 relative to this morning and exclude all hours and minutes 

585 

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

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

588 """ 

589 

590 if currentDatetime is None: 

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

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

593 

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

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

596 

597 if eventEnd < currentDatetime: 

598 return "Already passed" 

599 elif eventStart <= currentDatetime <= eventEnd: 

600 return "Happening now" 

601 

602 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

603 calendarDelta = relativedelta(eventStart, currentMorning) 

604 calendarYearsUntilEvent = calendarDelta.years 

605 calendarMonthsUntilEvent = calendarDelta.months 

606 calendarDaysUntilEvent = calendarDelta.days 

607 

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

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

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

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

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

613 

614 # Years until 

615 if calendarYearsUntilEvent: 

616 if calendarMonthsUntilEvent: 

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

618 return f"{yearString}" 

619 # Months until 

620 if calendarMonthsUntilEvent: 

621 if calendarDaysUntilEvent: 

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

623 return f"{monthString}" 

624 # Days until 

625 if calendarDaysUntilEvent: 

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

627 if calendarDaysUntilEvent == 1: 

628 return "Tomorrow" 

629 return f"{dayString}" 

630 if timeUntilEvent.hours: 

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

632 return f"{dayString}" 

633 # Hours until 

634 if timeUntilEvent.hours: 

635 if timeUntilEvent.minutes: 

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

637 return f"{hourString}" 

638 # Minutes until 

639 elif timeUntilEvent.minutes > 1: 

640 return f"{minuteString}" 

641 # Seconds until 

642 return "<1 minute" 

643 

644def copyRsvpToNewEvent(priorEvent, newEvent): 

645 """ 

646 Copies rvsps from priorEvent to newEvent 

647 """ 

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

649 

650 for student in rsvpInfo: 

651 newRsvp = EventRsvp( 

652 user = student.user, 

653 event = newEvent, 

654 rsvpWaitlist = student.rsvpWaitlist 

655 ) 

656 newRsvp.save() 

657 numRsvps = len(rsvpInfo) 

658 if numRsvps: 

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

660 

661 

662def inviteCohortsToEvent(event, cohortYears): 

663 """ 

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

665 """ 

666 invitedCohorts = [] 

667 try: 

668 for year in cohortYears: 

669 year = int(year) 

670 EventCohort.get_or_create( 

671 event=event, 

672 year=year, 

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

674 ) 

675 

676 addBonnerCohortToRsvpLog(year, event.id) 

677 rsvpForBonnerCohort(year, event.id) 

678 invitedCohorts.append(year) 

679 

680 if invitedCohorts: 

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

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

683 

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

685 

686 except Exception as e: 

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

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

689 

690def updateEventCohorts(event, cohortYears): 

691 """ 

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

693 """ 

694 invitedCohorts = [] 

695 try: 

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

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

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

699 

700 for year in yearsToAdd: 

701 EventCohort.get_or_create( 

702 event=event, 

703 year=year, 

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

705 ) 

706 

707 addBonnerCohortToRsvpLog(year, event.id) 

708 rsvpForBonnerCohort(year, event.id) 

709 invitedCohorts.append(year) 

710 

711 if yearsToAdd: 

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

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

714 

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

716 

717 except Exception as e: 

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

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

720 

721