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
« 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
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
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()
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')}.")
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)
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
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
57 if recurringEvent == event:
58 newEventName = recurringEvent.name
59 eventDeleted = True
61 program = event.program
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')}.")
68 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where(Event.id == event.id).execute()
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)
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
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
91 if repeatingEvent == event:
92 newEventName = repeatingEvent.name
93 eventDeleted = True
95 program = event.program
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')}.")
102 Event.update({Event.deletionDate: datetime.now(), Event.deletedBy: g.current_user}).where(Event.id == event.id).execute()
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
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
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)
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)
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.
165 Returns:
166 Created events and an error message.
167 """
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
174 newEventData = preprocessEventData(eventData)
176 isValid, validationErrorMessage = validateNewEventData(newEventData)
178 if not isValid:
179 return False, validationErrorMessage
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
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.
200 Returns:
201 Created events and an error message.
202 """
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
209 newEventData = NEWpreprocessEventData(eventData)
211 isValid, validationErrorMessage = NEWvalidateNewEventData(newEventData)
213 if not isValid:
214 return False, validationErrorMessage
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
227def saveEventToDb(newEventData, renewedEvent = False):
229 if not newEventData.get('valid', False) and not renewedEvent:
230 raise Exception("Unvalidated data passed to saveEventToDb")
233 isNewEvent = ('id' not in newEventData)
236 eventsToCreate = []
237 recurringSeriesId = None
238 multipleSeriesId = None
239 if (isNewEvent and newEventData['isRecurring']) and not renewedEvent:
240 eventsToCreate = calculateRecurringEventFrequency(newEventData)
241 recurringSeriesId = calculateNewrecurringId()
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']
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():
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 }
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()
290 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
291 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
293 eventRecords.append(eventRecord)
294 return eventRecords
296# RepeatingImplementation: Remove function above; remove "NEW" from the function name
297def NEWsaveEventToDb(newEventData, renewedEvent = False):
299 if not newEventData.get('valid', False) and not renewedEvent:
300 raise Exception("Unvalidated data passed to saveEventToDb")
302 isNewEvent = ('id' not in newEventData)
304 eventsToCreate = []
305 seriesId = None
307 # RepeatingImplementation: How can I merge this logic?
308 if (isNewEvent and newEventData['isRepeating']) and not renewedEvent:
309 eventsToCreate = calculateRepeatingEventFrequency(newEventData)
310 seriesId = calculateNewSeriesId()
312 elif(isNewEvent and newEventData['isSeries']) and not renewedEvent:
313 eventsToCreate.append({'name': f"{newEventData['name']}",
314 'date':newEventData['startDate']
315 })
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():
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 }
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()
359 if 'certRequirement' in newEventData and newEventData['certRequirement'] != "":
360 updateCertRequirementForEvent(eventRecord, newEventData['certRequirement'])
362 eventRecords.append(eventRecord)
363 return eventRecords
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())
374 programs = {}
376 for event in studentLedEvents:
377 programs.setdefault(event.program, []).append(event)
379 return programs
381def getUpcomingStudentLedCount(term, currentTime):
382 """
383 Return a count of all upcoming events for each student led program.
384 """
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))
394 programCountDict = {}
396 for programCount in upcomingCount:
397 programCountDict[programCount.id] = programCount.eventCount
398 return programCountDict
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))
416 hideBonner = (not user.isAdmin) and not (user.isStudent and user.isBonnerScholar)
417 if hideBonner:
418 trainingQuery = trainingQuery.where(Program.isBonnerScholars == False)
420 return list(trainingQuery.execute())
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
431def getOtherEvents(term):
432 """
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
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())
452 return otherEvents
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 """
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)))
472 if program:
473 events = events.where(Event.program == program)
475 events = events.order_by(Event.startDate, Event.timeStart)
477 events_list = []
478 shown_recurring_event_list = []
479 shown_multiple_offering_event_list = []
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)
495 return events_list
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 """
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)))
516 if program:
517 events = events.where(Event.program == program)
519 events = events.order_by(Event.startDate, Event.timeStart)
521 eventsList = []
522 seriesEventsList = []
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)
535 return eventsList
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 """
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))
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())
560 return unionParticipationWithVolunteer
562def validateNewEventData(data):
563 """
564 Confirm that the provided data is valid for an event.
566 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
568 Returns 3 values: (boolean success, the validation error message, the data object)
569 """
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.")
574 if data['isRecurring'] and data['endDate'] < data['startDate']:
575 return (False, "Event start date is after event end date.")
577 if data['timeEnd'] <= data['timeStart']:
578 return (False, "Event end time must be after start time.")
580 # Validation if we are inserting a new event
581 if 'id' not in data:
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()))
588 sameEventListCopy = sameEventList.copy()
590 for event in sameEventListCopy:
591 if event.isCanceled or event.recurringId:
592 sameEventList.remove(event)
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")
601 data['valid'] = True
602 return (True, "All inputs are valid.")
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.
609 Assumes the event data has been processed with `preprocessEventData`. NOT raw form data
611 Returns 3 values: (boolean success, the validation error message, the data object)
612 """
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.")
617 if data['isRepeating'] and data['endDate'] < data['startDate']:
618 return (False, "Event start date is after event end date.")
620 if data['timeEnd'] <= data['timeStart']:
621 return (False, "Event end time must be after start time.")
623 # Validation if we are inserting a new event
624 if 'id' not in data:
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()))
631 sameEventListCopy = sameEventList.copy()
633 for event in sameEventListCopy:
634 if event.isCanceled or event.seriesId:
635 sameEventList.remove(event)
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")
644 data['valid'] = True
645 return (True, "All inputs are valid.")
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
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
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
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
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.
702 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
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.")
709 if event['endDate'] == event['startDate']:
710 raise Exception("This event is not a recurring event")
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)]
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.
723 Assumes that the data has been processed with `preprocessEventData`. NOT raw form data.
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.")
730 if event['endDate'] == event['startDate']:
731 raise Exception("This event is not a recurring event")
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)]
738def preprocessEventData(eventData):
739 """
740 Ensures that the event data dictionary is consistent before it reaches the template or event logic.
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']
751 for checkBox in eventCheckBoxes:
752 if checkBox not in eventData:
753 eventData[checkBox] = False
754 else:
755 eventData[checkBox] = bool(eventData[checkBox])
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] = ''
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']
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'] = ''
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'])
792 if 'timeEnd' in eventData:
793 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
795 return eventData
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.
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']
811 for checkBox in eventCheckBoxes:
812 if checkBox not in eventData:
813 eventData[checkBox] = False
814 else:
815 eventData[checkBox] = bool(eventData[checkBox])
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] = ''
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']
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'] = ''
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'])
852 if 'timeEnd' in eventData:
853 eventData['timeEnd'] = format24HourTime(eventData['timeEnd'])
855 return eventData
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
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)
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))
879 amountAsDict = {event.id: event.count for event in amount}
881 return amountAsDict
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))
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.
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.
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
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 """
908 if currentDatetime is None:
909 currentDatetime = datetime.now().replace(second=0, microsecond=0)
910 currentMorning = currentDatetime.replace(hour=0, minute=0)
912 eventStart = datetime.combine(event.startDate, event.timeStart)
913 eventEnd = datetime.combine(event.endDate, event.timeEnd)
915 if eventEnd < currentDatetime:
916 return "Already passed"
917 elif eventStart <= currentDatetime <= eventEnd:
918 return "Happening now"
920 timeUntilEvent = relativedelta(eventStart, currentDatetime)
921 calendarDelta = relativedelta(eventStart, currentMorning)
922 calendarYearsUntilEvent = calendarDelta.years
923 calendarMonthsUntilEvent = calendarDelta.months
924 calendarDaysUntilEvent = calendarDelta.days
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 ''}"
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"
962def copyRsvpToNewEvent(priorEvent, newEvent):
963 """
964 Copies rvsps from priorEvent to newEvent
965 """
966 rsvpInfo = list(EventRsvp.select().where(EventRsvp.event == priorEvent['id']).execute())
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}")