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

339 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-12-25 19:18 +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 inheriting from the original eventData 

122 seriesData = eventData.get('seriesData') 

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

124 with mainDB.atomic() as transaction: 

125 for index, event in enumerate(seriesData): 

126 eventInfo = eventData.copy() 

127 eventInfo.update({ 

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

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

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

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

132 'seriesId': seriesId, 

133 'isRepeating': bool(isRepeating) 

134 }) 

135 # Try to save each offering 

136 savedEvents, validationErrorMessage = attemptSaveEvent(eventInfo, attachmentFiles) 

137 if validationErrorMessage: 

138 failedSavedOfferings.append((index, validationErrorMessage)) 

139 allSavesWereSuccessful = False 

140 else: 

141 savedEvent = savedEvents[0] 

142 savedOfferings.append(savedEvent) 

143 if not allSavesWereSuccessful: 

144 savedOfferings = [] 

145 transaction.rollback() 

146 

147 return allSavesWereSuccessful, savedOfferings, failedSavedOfferings 

148 

149 

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

151 """ 

152 Tries to save an event to the database: 

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

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

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

156 

157 Returns: 

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

159 """ 

160 

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

162 # automatically changed from "" to 0 

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

164 eventData["rsvpLimit"] = None 

165 

166 newEventData = preprocessEventData(eventData) 

167 

168 isValid, validationErrorMessage = validateNewEventData(newEventData) 

169 if not isValid: 

170 return [], validationErrorMessage 

171 

172 events = saveEventToDb(newEventData, renewedEvent) 

173 if attachmentFiles: 

174 for event in events: 

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

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

177 return events, "" 

178 

179 

180def saveEventToDb(newEventData, renewedEvent = False): 

181 

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

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

184 

185 isNewEvent = ('id' not in newEventData) 

186 

187 eventRecords = [] 

188 with mainDB.atomic(): 

189 

190 eventData = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

207 } 

208 

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

210 # it is a new event.  

211 if isNewEvent: 

212 eventData['program'] = newEventData['program'] 

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

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

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

216 eventRecord = Event.create(**eventData) 

217 else: 

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

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

220 

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

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

223 

224 eventRecords.append(eventRecord) 

225 return eventRecords 

226 

227def getVolunteerOpportunities(term): 

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

229 .join(Program) 

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

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

232 (Event.isService == True) & 

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

234 ) 

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

236 .execute()) 

237 

238 programs = {} 

239 

240 for event in volunteerOpportunities: 

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

242 

243 return programs 

244 

245def getEngagementEvents(term): 

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

247 .join(Program) 

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

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

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

251 .execute()) 

252 return engagementEvents 

253 

254def getUpcomingVolunteerOpportunitiesCount(term, currentTime): 

255 """ 

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

257 """ 

258 

259 upcomingCount = ( 

260 Program 

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

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

263 .where( 

264 (Event.term == term) & 

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

266 (Event.isService == True) & 

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

268 ((Event.startDate > currentTime) | 

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

270 (Event.isCanceled == False) 

271 ) 

272 .group_by(Program.id) 

273 ) 

274 

275 programCountDict = {} 

276 for programCount in upcomingCount: 

277 programCountDict[programCount.id] = programCount.eventCount 

278 return programCountDict 

279 

280def getpastVolunteerOpportunitiesCount(term, currentTime): 

281 """ 

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

283 """ 

284 

285 pastCount = ( 

286 Program 

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

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

289 .where( 

290 (Event.term == term) & 

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

292 (Event.isService == True) & 

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

294 ((Event.startDate < currentTime) | 

295 ((Event.startDate == currentTime) & (Event.timeStart <= currentTime))) & 

296 (Event.isCanceled == False) 

297 ) 

298 .group_by(Program.id) 

299 ) 

300 

301 programCountDict = {} 

302 for programCount in pastCount: 

303 programCountDict[programCount.id] = programCount.eventCount 

304 return programCountDict 

305 

306def getTrainingEvents(term, user): 

307 """ 

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

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

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

311 returned is the All Trainings Event. 

312 term: expected to be the ID of a term 

313 user: expected to be the current user 

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

315 """ 

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

317 .join(Program, JOIN.LEFT_OUTER) 

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

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

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

321 

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

323 if hideBonner: 

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

325 

326 return list(trainingQuery.execute()) 

327 

328def getBonnerEvents(term): 

329 bonnerScholarsEvents = list( 

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

331 .join(Program) 

332 .where( 

333 Program.isBonnerScholars, 

334 Event.term == term, 

335 Event.deletionDate == None 

336 ) 

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

338 .execute() 

339 ) 

340 return bonnerScholarsEvents 

341 

342def getCeltsLabor(term): 

343 """ 

344 Labor tab: events explicitly marked as Labor Only. 

345 """ 

346 celtsLabor = list(Event.select() 

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

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

349 .execute()) 

350 return celtsLabor 

351 

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

353 """ 

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

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

356 :param user: a username or User object 

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

358 Used in testing, defaults to the current timestamp. 

359 :return: A list of Event objects 

360 """ 

361 

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

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

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

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

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

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

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

369 

370 if program: 

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

372 

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

374 

375 eventsList = [] 

376 seriesEventsList = [] 

377 

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

379 for event in events: 

380 if event.seriesId: 

381 if not event.isCanceled: 

382 if event.seriesId not in seriesEventsList: 

383 eventsList.append(event) 

384 seriesEventsList.append(event.seriesId) 

385 else: 

386 if not event.isCanceled: 

387 eventsList.append(event) 

388 

389 return eventsList 

390 

391def getParticipatedEventsForUser(user): 

392 """ 

393 Get all the events a user has participated in. 

394 :param user: a username or User object 

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

396 Used in testing, defaults to the current timestamp. 

397 :return: A list of Event objects 

398 """ 

399 

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

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

402 .join(EventParticipant) 

403 .where(EventParticipant.user == user, 

404 Event.isAllVolunteerTraining == False, Event.deletionDate == None) 

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

406 

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

408 .join(EventParticipant) 

409 .where(Event.isAllVolunteerTraining == True, 

410 EventParticipant.user == user)) 

411 union = participatedEvents.union_all(allVolunteer) 

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

413 

414 return unionParticipationWithVolunteer 

415 

416def validateNewEventData(data): 

417 """ 

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

419 

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

421 

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

423 """ 

424 

425 if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['isLaborOnly']]: 

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

427 

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

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

430 

431 # Validation if we are inserting a new event 

432 if 'id' not in data: 

433 

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

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

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

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

438 

439 sameEventListCopy = sameEventList.copy() 

440 

441 for event in sameEventListCopy: 

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

443 sameEventList.remove(event) 

444 

445 try: 

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

447 except DoesNotExist as e: 

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

449 if sameEventList: 

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

451 

452 data['valid'] = True 

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

454 

455def calculateNewSeriesId(): 

456 """ 

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

458 """ 

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

460 if maxSeriesId: 

461 return maxSeriesId + 1 

462 return 1 

463 

464def getPreviousSeriesEventData(seriesId): 

465 """ 

466 Joins the User db table and Event Participant db table so that we can get the information of a participant if they attended an event. 

467  

468 """ 

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

470 .join(EventParticipant) 

471 .join(Event) 

472 .where(Event.seriesId==seriesId)) 

473 return previousEventVolunteers 

474 

475def getRepeatingEventsData(eventData): 

476 """ 

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

478 dictionary of event data. 

479  

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

481 

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

483 """ 

484 

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

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

487 "week": counter+1} 

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

489 

490def preprocessEventData(eventData): 

491 """ 

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

493 

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

495 - checkboxes should be True or False 

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

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

498 - seriesData should be a JSON string 

499 - Look up matching certification requirement if necessary 

500 """ 

501 ## Process checkboxes 

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

503 

504 for checkBox in eventCheckBoxes: 

505 if checkBox not in eventData: 

506 eventData[checkBox] = False 

507 else: 

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

509 

510 ## Process dates 

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

512 for eventDate in eventDates: 

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

514 eventData[eventDate] = '' 

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

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

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

518 eventData[eventDate] = '' 

519 

520 # Process seriesData 

521 if 'seriesData' not in eventData: 

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

523 

524 # Process terms 

525 if 'term' in eventData: 

526 try: 

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

528 except DoesNotExist: 

529 eventData['term'] = '' 

530 

531 # Process requirement 

532 if 'certRequirement' in eventData: 

533 try: 

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

535 except DoesNotExist: 

536 eventData['certRequirement'] = '' 

537 elif 'id' in eventData: 

538 # look up requirement 

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

540 if match: 

541 eventData['certRequirement'] = match.requirement 

542 if 'timeStart' in eventData: 

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

544 

545 if 'timeEnd' in eventData: 

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

547 

548 return eventData 

549 

550def getTomorrowsEvents(): 

551 """Grabs each event that occurs tomorrow""" 

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

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

554 return events 

555 

556def addEventView(viewer,event): 

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

558 if not viewer.isCeltsAdmin: 

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

560 

561def getEventRsvpCountsForTerm(term): 

562 """ 

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

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

565 current RSVPs to that event as the pair. 

566 """ 

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

568 .join(EventRsvp, JOIN.LEFT_OUTER) 

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

570 .group_by(Event.id)) 

571 

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

573 

574 return amountAsDict 

575 

576def getEventRsvpCount(eventId): 

577 """ 

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

579 """ 

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

581 

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

583 """ 

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

585 until the start of the event. 

586 

587 Note about dates: 

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

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

590 tomorrow with no mention of the hour.  

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

592 and hours in actual time. 

593 

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

595 relative to this morning and exclude all hours and minutes 

596 

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

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

599 """ 

600 

601 if currentDatetime is None: 

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

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

604 

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

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

607 

608 if eventEnd < currentDatetime: 

609 return "Already passed" 

610 elif eventStart <= currentDatetime <= eventEnd: 

611 return "Happening now" 

612 

613 timeUntilEvent = relativedelta(eventStart, currentDatetime) 

614 calendarDelta = relativedelta(eventStart, currentMorning) 

615 calendarYearsUntilEvent = calendarDelta.years 

616 calendarMonthsUntilEvent = calendarDelta.months 

617 calendarDaysUntilEvent = calendarDelta.days 

618 

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

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

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

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

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

624 

625 # Years until 

626 if calendarYearsUntilEvent: 

627 if calendarMonthsUntilEvent: 

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

629 return f"{yearString}" 

630 # Months until 

631 if calendarMonthsUntilEvent: 

632 if calendarDaysUntilEvent: 

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

634 return f"{monthString}" 

635 # Days until 

636 if calendarDaysUntilEvent: 

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

638 if calendarDaysUntilEvent == 1: 

639 return "Tomorrow" 

640 return f"{dayString}" 

641 if timeUntilEvent.hours: 

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

643 return f"{dayString}" 

644 # Hours until 

645 if timeUntilEvent.hours: 

646 if timeUntilEvent.minutes: 

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

648 return f"{hourString}" 

649 # Minutes until 

650 elif timeUntilEvent.minutes > 1: 

651 return f"{minuteString}" 

652 # Seconds until 

653 return "<1 minute" 

654 

655def copyRsvpToNewEvent(priorEvent, newEvent): 

656 """ 

657 Copies rvsps from priorEvent to newEvent 

658 """ 

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

660 

661 for student in rsvpInfo: 

662 newRsvp = EventRsvp( 

663 user = student.user, 

664 event = newEvent, 

665 rsvpWaitlist = student.rsvpWaitlist 

666 ) 

667 newRsvp.save() 

668 numRsvps = len(rsvpInfo) 

669 if numRsvps: 

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

671 

672 

673def inviteCohortsToEvent(event, cohortYears): 

674 """ 

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

676 """ 

677 invitedCohorts = [] 

678 try: 

679 for year in cohortYears: 

680 year = int(year) 

681 EventCohort.get_or_create( 

682 event=event, 

683 year=year, 

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

685 ) 

686 

687 addBonnerCohortToRsvpLog(year, event.id) 

688 rsvpForBonnerCohort(year, event.id) 

689 invitedCohorts.append(year) 

690 

691 if invitedCohorts: 

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

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

694 

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

696 

697 except Exception as e: 

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

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

700 

701def updateEventCohorts(event, cohortYears): 

702 """ 

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

704 """ 

705 invitedCohorts = [] 

706 try: 

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

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

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

710 

711 for year in yearsToAdd: 

712 EventCohort.get_or_create( 

713 event=event, 

714 year=year, 

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

716 ) 

717 

718 addBonnerCohortToRsvpLog(year, event.id) 

719 rsvpForBonnerCohort(year, event.id) 

720 invitedCohorts.append(year) 

721 

722 if yearsToAdd: 

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

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

725 

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

727 

728 except Exception as e: 

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

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

731 

732