Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate for Duplicate Event Entries within Multiple Offering Modal #1327

Merged
merged 25 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
49682e9
Added some javascript to check for duplicate events before we let the…
hoerstl Aug 28, 2024
6bc07d7
Modified invalid feedback message
stevensonmichel Aug 30, 2024
83fbd81
Fixed spacing of the add new offerings + button
hoerstl Sep 4, 2024
04963ee
Removed end time as part of duplicate check for event with multiple t…
stevensonmichel Sep 4, 2024
d84dd6f
Merge branch 'development' into 1306-Multiple-Offering-Duplicate-Entries
stevensonmichel Sep 4, 2024
d919e49
Merge branch 'development' into 1306-Multiple-Offering-Duplicate-Entries
bledsoef Sep 6, 2024
b471411
Started refactoring the way we save multiple offerings so we can chec…
hoerstl Sep 6, 2024
886525a
Merge branch '1306-Multiple-Offering-Duplicate-Entries' of github.com…
hoerstl Sep 6, 2024
c891d06
Added docstrings and completed the todos for changing how we handle t…
hoerstl Sep 6, 2024
318feb0
Renamed some variables and changed a data structure for clarity
hoerstl Sep 9, 2024
e03b521
Modified preexisting functions
stevensonmichel Sep 9, 2024
4f981fd
Added preprocessing for multipleOfferingData
hoerstl Sep 9, 2024
66f4d51
Merge branch 'development' into 1306-Multiple-Offering-Duplicate-Entries
bledsoef Sep 10, 2024
fb01536
Renamed a class to be more specific
hoerstl Sep 11, 2024
08853cb
Changed the multiple offerings modal so it loads from the data table …
hoerstl Sep 11, 2024
8ee0224
Fixed syntax for indexing elements in list
stevensonmichel Sep 13, 2024
19c9924
Fixed camel case issue
stevensonmichel Sep 13, 2024
ce05764
Changed comment wording
stevensonmichel Sep 13, 2024
76bbeba
Merge branch 'development' into 1306-Multiple-Offering-Duplicate-Entries
stevensonmichel Sep 13, 2024
716b550
Created function for code reusability in notification for invalidation
stevensonmichel Sep 13, 2024
bca5c8c
Fixed a bug in development where it was possible to save a single off…
hoerstl Sep 16, 2024
93c52d1
Fixed a bug where dismissing an alert would dismiss all alerts
hoerstl Sep 16, 2024
6181c4b
Created tests for attemptSaveMultipleOfferings function
stevensonmichel Sep 16, 2024
fe8972c
Added tests for changes made to preprocessEventData for multipleOffer…
hoerstl Sep 16, 2024
6e299b5
Merge branch 'development' into 1306-Multiple-Offering-Duplicate-Entries
bledsoef Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 20 additions & 35 deletions app/controllers/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
from app.logic.createLogs import createActivityLog
from app.logic.certification import getCertRequirements, updateCertRequirements
from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
from app.logic.events import cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId
from app.logic.events import cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId
from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRecurringEventsData, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId
from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp
from app.logic.minor import getMinorInterest
from app.logic.fileHandler import FileHandler
Expand Down Expand Up @@ -83,7 +82,6 @@ def templateSelect():

@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
def createEvent(templateid, programid):
savedEventsList = []
if not (g.current_user.isAdmin or g.current_user.isProgramManagerFor(programid)):
abort(403)

Expand All @@ -100,7 +98,6 @@ def createEvent(templateid, programid):

# Get the data from the form or from the template
eventData = template.templateData

eventData['program'] = program

if request.method == "GET":
Expand All @@ -115,32 +112,22 @@ def createEvent(templateid, programid):

# Try to save the form
if request.method == "POST":
savedEvents = None
eventData.update(request.form.copy())
eventData = preprocessEventData(eventData)
if eventData.get('isMultipleOffering'):
multipleOfferingId = calculateNewMultipleOfferingId()

multipleOfferingData = json.loads(eventData.get('multipleOfferingData'))
for event in multipleOfferingData:
multipleOfferingDict = eventData.copy()
multipleOfferingDict.update({
'name': event['eventName'],
'startDate': event['eventDate'],
'timeStart': event['startTime'],
'timeEnd': event['endTime'],
'multipleOfferingId': multipleOfferingId
})
try:
savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, getFilesFromRequest(request))
savedEventsList.append(savedEvents)

except Exception as e:
print("Failed saving multi event", e)

eventData['multipleOfferingData'] = json.loads(eventData['multipleOfferingData'])
succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
if not succeeded:
for index, validationErrorMessage in failedSavedOfferings:
eventData['multipleOfferingData'][index]['isDuplicate'] = True
validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
print(f"Failed to save offerings {failedSavedOfferings}")
else:
try:
savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
except Exception as e:
print("Failed saving regular event", e)
print("Failed saving regular event", e)

if savedEvents:
rsvpcohorts = request.form.getlist("cohorts[]")
Expand All @@ -157,21 +144,19 @@ def createEvent(templateid, programid):
if len(savedEvents) > 1 and eventData.get('isRecurring'):
createActivityLog(f"Created a recurring event, <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a>, for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}. The last event in the series will be on {datetime.strftime(savedEvents[-1].startDate, '%m/%d/%Y')}.")

elif len(savedEventsList) >= 1 and eventData.get('isMultipleOffering'):
modifiedSavedEvents = [item for sublist in savedEventsList for item in sublist]

event_dates = [event_data[0].startDate.strftime('%m/%d/%Y') for event_data in savedEventsList]
elif len(savedEvents) >= 1 and eventData.get('isMultipleOffering'):
eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]

event_list = ', '.join(f"<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>" for event in modifiedSavedEvents)
eventList = ', '.join(f"<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>" for event in savedEvents)

if len(modifiedSavedEvents) > 1:
if len(savedEvents) > 1:
#creates list of events created in a multiple series to display in the logs
event_list = ', '.join(event_list.split(', ')[:-1]) + f', and ' + event_list.split(', ')[-1]
eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
#get last date and stick at the end after 'and' so that it reads like a sentence in admin log
last_event_date = event_dates[-1]
event_dates = ', '.join(event_dates[:-1]) + f', and {last_event_date}'
lastEventDate = eventDates[-1]
eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'

createActivityLog(f"Created events {event_list} for {program.programName}, with start dates of {event_dates}.")
createActivityLog(f"Created events {eventList} for {program.programName}, with start dates of {eventDates}.")

else:
createActivityLog(f"Created events <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a> for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.")
Expand Down Expand Up @@ -452,7 +437,7 @@ def deleteAllRecurringEventsRoute(eventId):

@admin_bp.route('/makeRecurringEvents', methods=['POST'])
def addRecurringEvents():
recurringEvents = calculateRecurringEventFrequency(preprocessEventData(request.form.copy()))
recurringEvents = getRecurringEventsData(preprocessEventData(request.form.copy()))
return json.dumps(recurringEvents, default=str)


Expand Down
119 changes: 94 additions & 25 deletions app/logic/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import timedelta, date, datetime
from dateutil.relativedelta import relativedelta
from werkzeug.datastructures import MultiDict
import json
from app.models import mainDB
from app.models.user import User
from app.models.event import Event
Expand Down Expand Up @@ -95,6 +96,51 @@ def deleteAllRecurringEvents(eventId):
eventId = allRecurringEvents[0].id
return deleteEventAndAllFollowing(eventId)

def attemptSaveMultipleOfferings(eventData, attachmentFiles = None):
hoerstl marked this conversation as resolved.
Show resolved Hide resolved
"""
Tries to save an event with multiple offerings to the database:
Creates separate event data inheriting from the original eventData
with the specifics of each offering.
Calls attemptSaveEvent on each of the newly created datum
If any data is not valid it will return a validation error.

Returns:
allSavesWereSuccessful : bool | Whether or not all offering saves were successful
savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesWereSuccessful is False then this list will be empty.
failedSavedOfferings : List[(int, str), ...] | Tuples containing the indicies of failed saved offerings and the associated validation error message.
"""
savedOfferings = []
failedSavedOfferings = []
allSavesWereSuccessful = True

# Creates a shared multipleOfferingId for all offerings to have
multipleOfferingId = calculateNewMultipleOfferingId()

# Create separate event data inheriting from the original eventData
multipleOfferingData = eventData.get('multipleOfferingData')
with mainDB.atomic() as transaction:
for index, event in enumerate(multipleOfferingData):
multipleOfferingDict = eventData.copy()
multipleOfferingDict.update({
'name': event['eventName'],
'startDate': event['eventDate'],
'timeStart': event['startTime'],
'timeEnd': event['endTime'],
'multipleOfferingId': multipleOfferingId
})
# Try to save each offering
savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles)
if validationErrorMessage:
failedSavedOfferings.append((index, validationErrorMessage))
allSavesWereSuccessful = False
else:
savedEvent = savedEvents[0]
savedOfferings.append(savedEvent)
if not allSavesWereSuccessful:
savedOfferings = []
transaction.rollback()

return allSavesWereSuccessful, savedOfferings, failedSavedOfferings


def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
Expand All @@ -105,7 +151,7 @@ def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
If it is not valid it will return a validation error.

Returns:
Created events and an error message.
The saved event, created events and an error message if an error occurred.
"""

# Manually set the value of RSVP Limit if it is and empty string since it is
Expand All @@ -116,9 +162,8 @@ def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
newEventData = preprocessEventData(eventData)

isValid, validationErrorMessage = validateNewEventData(newEventData)

if not isValid:
return False, validationErrorMessage
return [], validationErrorMessage

try:
events = saveEventToDb(newEventData, renewedEvent)
Expand All @@ -129,7 +174,7 @@ def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False):
return events, ""
except Exception as e:
print(f'Failed attemptSaveEvent() with Exception: {e}')
return False, e
return [], e

def saveEventToDb(newEventData, renewedEvent = False):

Expand All @@ -144,7 +189,7 @@ def saveEventToDb(newEventData, renewedEvent = False):
recurringSeriesId = None
multipleSeriesId = None
if (isNewEvent and newEventData['isRecurring']) and not renewedEvent:
eventsToCreate = calculateRecurringEventFrequency(newEventData)
eventsToCreate = getRecurringEventsData(newEventData)
recurringSeriesId = calculateNewrecurringId()

#temporarily applying the append for single events for now to tests
Expand Down Expand Up @@ -311,25 +356,25 @@ def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None):

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

events_list = []
shown_recurring_event_list = []
shown_multiple_offering_event_list = []
eventsList = []
shownRecurringEventList = []
shownMultipleOfferingEventList = []

# removes all recurring events except for the next upcoming one
for event in events:
if event.recurringId or event.multipleOfferingId:
if not event.isCanceled:
if event.recurringId not in shown_recurring_event_list:
events_list.append(event)
shown_recurring_event_list.append(event.recurringId)
if event.multipleOfferingId not in shown_multiple_offering_event_list:
events_list.append(event)
shown_multiple_offering_event_list.append(event.multipleOfferingId)
if event.recurringId not in shownRecurringEventList:
eventsList.append(event)
shownRecurringEventList.append(event.recurringId)
if event.multipleOfferingId not in shownMultipleOfferingEventList:
eventsList.append(event)
shownMultipleOfferingEventList.append(event.multipleOfferingId)
else:
if not event.isCanceled:
events_list.append(event)
eventsList.append(event)

return events_list
return eventsList

def getParticipatedEventsForUser(user):
"""
Expand Down Expand Up @@ -437,7 +482,7 @@ def getPreviousMultipleOfferingEventData(multipleOfferingId):
.where(Event.multipleOfferingId == multipleOfferingId))
return previousEventVolunteers

def calculateRecurringEventFrequency(event):
def getRecurringEventsData(eventData):
"""
Calculate the events to create based on a recurring event start and end date. Takes a
dictionary of event data.
Expand All @@ -446,16 +491,16 @@ def calculateRecurringEventFrequency(event):

Return a list of events to create from the event data.
"""
if not isinstance(event['endDate'], date) or not isinstance(event['startDate'], date):
if not isinstance(eventData['endDate'], date) or not isinstance(eventData['startDate'], date):
raise Exception("startDate and endDate must be datetime.date objects.")

if event['endDate'] == event['startDate']:
if eventData['endDate'] == eventData['startDate']:
raise Exception("This event is not a recurring event")

return [ {'name': f"{event['name']} Week {counter+1}",
'date': event['startDate'] + timedelta(days=7*counter),
return [ {'name': f"{eventData['name']} Week {counter+1}",
'date': eventData['startDate'] + timedelta(days=7*counter),
"week": counter+1}
for counter in range(0, ((event['endDate']-event['startDate']).days//7)+1)]
for counter in range(0, ((eventData['endDate']-eventData['startDate']).days//7)+1)]

def preprocessEventData(eventData):
"""
Expand All @@ -465,6 +510,7 @@ def preprocessEventData(eventData):
- checkboxes should be True or False
- if term is given, convert it to a model object
- times should exist be strings in 24 hour format example: 14:40
- multipleOfferingData should be a JSON string
- Look up matching certification requirement if necessary
"""
## Process checkboxes
Expand All @@ -479,17 +525,40 @@ def preprocessEventData(eventData):
## Process dates
eventDates = ['startDate', 'endDate']
for eventDate in eventDates:
if eventDate not in eventData:
if eventDate not in eventData: # There is no date given
eventData[eventDate] = ''
elif type(eventData[eventDate]) is str and eventData[eventDate]:
elif type(eventData[eventDate]) is str and eventData[eventDate]: # The date is a nonempty string
eventData[eventDate] = parser.parse(eventData[eventDate])
elif not isinstance(eventData[eventDate], date):
elif not isinstance(eventData[eventDate], date): # The date is not a date object
eventData[eventDate] = ''

# If we aren't recurring, all of our events are single-day or mutliple offerings, which also have the same start and end date
if not eventData['isRecurring']:
eventData['endDate'] = eventData['startDate']

# Process multipleOfferingData
if 'multipleOfferingData' not in eventData:
eventData['multipleOfferingData'] = json.dumps([])
elif type(eventData['multipleOfferingData']) is str:
try:
multipleOfferingData = json.loads(eventData['multipleOfferingData'])
eventData['multipleOfferingData'] = multipleOfferingData
if type(multipleOfferingData) != list:
eventData['multipleOfferingData'] = json.dumps([])
except json.decoder.JSONDecodeError as e:
eventData['multipleOfferingData'] = json.dumps([])
if type(eventData['multipleOfferingData']) is list:
# validate the list data. Make sure there is 'eventName', 'startDate', 'timeStart', 'timeEnd', and 'isDuplicate' data
multipleOfferingData = eventData['multipleOfferingData']
for offeringDatum in multipleOfferingData:
for attribute in ['eventName', 'startDate', 'timeStart', 'timeEnd']:
if type(offeringDatum.get(attribute)) != str:
offeringDatum[attribute] = ''
if type(offeringDatum.get('isDuplicate')) != bool:
offeringDatum['isDuplicate'] = False

eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData'])

# Process terms
if 'term' in eventData:
try:
Expand Down
2 changes: 1 addition & 1 deletion app/static/css/createEvent.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
margin-right: auto;
}

.deleteRow{
.deleteOfferingBtn{
border-radius: 5px;
position:relative;
top: 12px;
Expand Down
6 changes: 3 additions & 3 deletions app/static/js/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ function msgFlash(flash_message, status){
$("#flash_container").prepend(`
<div class="alert alert-${status} alert-dismissible alert-success" role="alert">${flash_message}
${flashMessageResponse(flash_message)}
<button type="button" class="btn-close kiosk-hide" id="flashResponded" aria-label="Close"></button>
<button type="button" class="btn-close kiosk-hide close-alert" aria-label="Close"></button>
</div>`);
$("#flashResponded").click(function(){
$(".alert").delay(1000).fadeOut();
$(".close-alert").click(function(){
$(this).closest(".alert").delay(1000).fadeOut();
})
}

Expand Down
Loading
Loading