From 07f5e36fd50e16aaf9d58278ea1a87787ac02f4c Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Mon, 29 Jul 2024 11:06:14 -0400 Subject: [PATCH 01/38] added route to get all event list counts for each event type. Added ajax request to event list JS. Not working --- app/controllers/main/routes.py | 17 ++++++++++++- app/static/js/event_list.js | 46 ++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index f028c99b0..e26b88c53 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -450,7 +450,22 @@ def RemoveRSVP(): if eventData['from'] == 'ajax': return '' return redirect(url_for("admin.eventDisplay", eventId=event.id)) - +# =========================Event List Indicators================================= +@main_bp.route('/admin/getEventListCounts', methods=['GET']) +def getEventListCounts() -> str: + """ + Get the count of events for each category to display in the event list page. + It must be returned as a string to be received by the ajax request. + """ + studentLedEventsCount: int = len(getStudentLedEvents(g.current_term)) + trainingEventsCount: int = len(getTrainingEvents()) + bonnerEventsCount: int = len(getBonnerEvents()) + otherEventsCount: int = len(getOtherEvents()) + return {"studentLedEventsCount": studentLedEventsCount, + "trainingEventsCount": trainingEventsCount, + "bonnerEventsCount": bonnerEventsCount, + "otherEventsCount": otherEventsCount} +# =============================================================================== @main_bp.route('/profile//serviceTranscript', methods = ['GET']) def serviceTranscript(username): user = User.get_or_none(User.username == username) diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index e6333562a..711a8b9a6 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -31,6 +31,50 @@ $(document).ready(function(){ tableRows.hide(); } } +// ================== Event count notifiers=================== + $.ajax({ + url: "/admin/getEventListCounts", + type: "GET", + success: function(EventsCount) { + const studentLedEventsCount = Number(EventsCount.studentLedEventsCount) + const trainingEventsCount = Number(EventsCount.trainingEventsCount) + const bonnerEventsCount = Number(EventsCount.bonnerEventsCount) + const otherEventsCount = Number(EventsCount.otherEventsCount) + + if (studentLedEventsCount > 0) { + $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`) + // $(".courseManagement").popover({ + // trigger: "hover", + // sanitize: false, + // html: true, + // content: function() { + // return "Amount of pending course proposals for the current term." + // } + // }); + } + + if (trainingEventsCount > 0) { + $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`) + + } + + if (bonnerEventsCount > 0) { + $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`) + + } + + if (otherEventsCount > 0) { + $("#otherEvents").html(`Other Events (${otherEventsCount})`) + + } + // if (interestedStudentsCount + unapprovedCoursesCount > 0) { + // $("#admin").html(`Admin (${interestedStudentsCount + unapprovedCoursesCount})`) + // } + }, + error: function(request, status, error) { + console.log(status,error); + } + }); }); function rsvpForEvent(eventID){ @@ -67,5 +111,3 @@ function removeRsvpForEvent(eventID){ }) } - - From 950a1754a9846164dd8fb9f0500487c202c7894a Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Mon, 29 Jul 2024 15:19:19 -0400 Subject: [PATCH 02/38] Idicators now appear in the event list headers. Student led service counter counts the amount of event type categories under the header rather than the amount of events, need to fix that :/ --- app/controllers/main/routes.py | 36 ++++++++++++++++------------------ app/static/js/event_list.js | 13 +++++++----- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index e26b88c53..15bb0c546 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -3,7 +3,7 @@ from peewee import JOIN from http import cookies from playhouse.shortcuts import model_to_dict -from flask import request, render_template, g, abort, flash, redirect, url_for, make_response, session +from flask import request, render_template, jsonify, g, abort, flash, redirect, url_for, make_response, session, request from app.controllers.main import main_bp from app import app @@ -90,7 +90,20 @@ def events(selectedTerm, activeTab, programID): trainingEvents = getTrainingEvents(term, g.current_user) bonnerEvents = getBonnerEvents(term) otherEvents = getOtherEvents(term) - + # Get the count of events for each category to display in the event list page. + studentLedEventsCount: int = len(getStudentLedEvents(g.current_term)) + trainingEventsCount: int = len(getTrainingEvents(term, g.current_user)) + bonnerEventsCount: int = len(getBonnerEvents(term)) + otherEventsCount: int = len(getOtherEvents(term)) + # Handle ajax request for Event type header number notifiers + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({ + "studentLedEventsCount": studentLedEventsCount, + "trainingEventsCount": trainingEventsCount, + "bonnerEventsCount": bonnerEventsCount, + "otherEventsCount": otherEventsCount + }) + managersProgramDict = getManagerProgramDict(g.current_user) # Fetch toggle state from session @@ -111,7 +124,7 @@ def events(selectedTerm, activeTab, programID): programID = int(programID), managersProgramDict = managersProgramDict, countUpcomingStudentLedEvents = countUpcomingStudentLedEvents, - toggle_state = toggle_state + toggle_state = toggle_state, ) @main_bp.route('/updateToggleState', methods=['POST']) @@ -450,22 +463,7 @@ def RemoveRSVP(): if eventData['from'] == 'ajax': return '' return redirect(url_for("admin.eventDisplay", eventId=event.id)) -# =========================Event List Indicators================================= -@main_bp.route('/admin/getEventListCounts', methods=['GET']) -def getEventListCounts() -> str: - """ - Get the count of events for each category to display in the event list page. - It must be returned as a string to be received by the ajax request. - """ - studentLedEventsCount: int = len(getStudentLedEvents(g.current_term)) - trainingEventsCount: int = len(getTrainingEvents()) - bonnerEventsCount: int = len(getBonnerEvents()) - otherEventsCount: int = len(getOtherEvents()) - return {"studentLedEventsCount": studentLedEventsCount, - "trainingEventsCount": trainingEventsCount, - "bonnerEventsCount": bonnerEventsCount, - "otherEventsCount": otherEventsCount} -# =============================================================================== + @main_bp.route('/profile//serviceTranscript', methods = ['GET']) def serviceTranscript(username): user = User.get_or_none(User.username == username) diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index 711a8b9a6..810f210fc 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -33,15 +33,18 @@ $(document).ready(function(){ } // ================== Event count notifiers=================== $.ajax({ - url: "/admin/getEventListCounts", + url: "/eventsList/" + "3", type: "GET", success: function(EventsCount) { const studentLedEventsCount = Number(EventsCount.studentLedEventsCount) const trainingEventsCount = Number(EventsCount.trainingEventsCount) const bonnerEventsCount = Number(EventsCount.bonnerEventsCount) const otherEventsCount = Number(EventsCount.otherEventsCount) + msgFlash("HELLO", "success"); + console.log(EventsCount); + console.log(studentLedEventsCount, trainingEventsCount, bonnerEventsCount, otherEventsCount); - if (studentLedEventsCount > 0) { + if (studentLedEventsCount >= 0) { $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`) // $(".courseManagement").popover({ // trigger: "hover", @@ -53,17 +56,17 @@ $(document).ready(function(){ // }); } - if (trainingEventsCount > 0) { + if (trainingEventsCount >= 0) { $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`) } - if (bonnerEventsCount > 0) { + if (bonnerEventsCount >= 0) { $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`) } - if (otherEventsCount > 0) { + if (otherEventsCount >= 0) { $("#otherEvents").html(`Other Events (${otherEventsCount})`) } From d3d56c885ad15cbeabd8df83c81be5a3ebb591ff Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Mon, 29 Jul 2024 15:58:51 -0400 Subject: [PATCH 03/38] There is now an accurate event count for student led events. The total term event count is still displayed instead of upcoming even if past term is toggled on. This is the next fix --- app/controllers/main/routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 15bb0c546..2480486a7 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -91,7 +91,8 @@ def events(selectedTerm, activeTab, programID): bonnerEvents = getBonnerEvents(term) otherEvents = getOtherEvents(term) # Get the count of events for each category to display in the event list page. - studentLedEventsCount: int = len(getStudentLedEvents(g.current_term)) + studentLedEventsCount: int = sum(list(getUpcomingStudentLedCount(term, currentTime).keys())) + print("RAAAAAH ", countUpcomingStudentLedEvents) trainingEventsCount: int = len(getTrainingEvents(term, g.current_user)) bonnerEventsCount: int = len(getBonnerEvents(term)) otherEventsCount: int = len(getOtherEvents(term)) From 8858765a6eab21d3916772dba150b2df3800a9a2 Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Mon, 29 Jul 2024 16:59:05 -0400 Subject: [PATCH 04/38] Cleaned up unused code and extra space. The ajax request now handles each term rather than just one still need to have total numbers when past term toggle is on and only show upcoming by default --- app/static/js/event_list.js | 38 ++++++++-------------------- app/templates/events/event_list.html | 3 +++ 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index 810f210fc..dbd469932 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -31,48 +31,32 @@ $(document).ready(function(){ tableRows.hide(); } } -// ================== Event count notifiers=================== - $.ajax({ - url: "/eventsList/" + "3", +// ================== Event count Indicators =================== + $.ajax({ //NEED TO REPLACE 3 WITH CURRENT TERM************************************************* + url: "/eventsList/" + $('#termID').val(), type: "GET", success: function(EventsCount) { - const studentLedEventsCount = Number(EventsCount.studentLedEventsCount) - const trainingEventsCount = Number(EventsCount.trainingEventsCount) - const bonnerEventsCount = Number(EventsCount.bonnerEventsCount) - const otherEventsCount = Number(EventsCount.otherEventsCount) + const studentLedEventsCount = Number(EventsCount.studentLedEventsCount); + const trainingEventsCount = Number(EventsCount.trainingEventsCount); + const bonnerEventsCount = Number(EventsCount.bonnerEventsCount); + const otherEventsCount = Number(EventsCount.otherEventsCount); msgFlash("HELLO", "success"); console.log(EventsCount); console.log(studentLedEventsCount, trainingEventsCount, bonnerEventsCount, otherEventsCount); if (studentLedEventsCount >= 0) { - $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`) - // $(".courseManagement").popover({ - // trigger: "hover", - // sanitize: false, - // html: true, - // content: function() { - // return "Amount of pending course proposals for the current term." - // } - // }); + $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); } - if (trainingEventsCount >= 0) { - $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`) - + $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`); } - if (bonnerEventsCount >= 0) { - $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`) - + $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`); } if (otherEventsCount >= 0) { - $("#otherEvents").html(`Other Events (${otherEventsCount})`) - + $("#otherEvents").html(`Other Events (${otherEventsCount})`); } - // if (interestedStudentsCount + unapprovedCoursesCount > 0) { - // $("#admin").html(`Admin (${interestedStudentsCount + unapprovedCoursesCount})`) - // } }, error: function(request, status, error) { console.log(status,error); diff --git a/app/templates/events/event_list.html b/app/templates/events/event_list.html index 296c346b8..f1f7e3793 100644 --- a/app/templates/events/event_list.html +++ b/app/templates/events/event_list.html @@ -15,6 +15,9 @@ {% endblock %} {% block app_content %}

Events List for {{selectedTerm.description}}

+{% if selectedTerm.id %} + +{% endif %}
From 873b927532f2eaf2a392b8807aeac8894cdee352 Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Tue, 30 Jul 2024 17:05:50 -0400 Subject: [PATCH 05/38] Now only upcoming events are displayed as an indicator, ideally we will show indicators for past when the toggle is on. --- app/controllers/main/routes.py | 21 ++++++++++++++++----- app/static/js/event_list.js | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 2480486a7..6aef33fd3 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -90,12 +90,25 @@ def events(selectedTerm, activeTab, programID): trainingEvents = getTrainingEvents(term, g.current_user) bonnerEvents = getBonnerEvents(term) otherEvents = getOtherEvents(term) + + managersProgramDict = getManagerProgramDict(g.current_user) + # Get the count of events for each category to display in the event list page. - studentLedEventsCount: int = sum(list(getUpcomingStudentLedCount(term, currentTime).keys())) - print("RAAAAAH ", countUpcomingStudentLedEvents) + studentLedEventsCount: int = sum(list(getUpcomingStudentLedCount(term, currentTime).keys())) #only upcoming events trainingEventsCount: int = len(getTrainingEvents(term, g.current_user)) + for event in getTrainingEvents(term, g.current_user): + if event.isPastEnd: + trainingEventsCount -= 1 + bonnerEventsCount: int = len(getBonnerEvents(term)) + for event in getBonnerEvents(term): + if event.isPastEnd: + bonnerEventsCount -= 1 + otherEventsCount: int = len(getOtherEvents(term)) + for event in getOtherEvents(term): + if event.isPastEnd: + otherEventsCount -= 1 # Handle ajax request for Event type header number notifiers if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ @@ -104,9 +117,7 @@ def events(selectedTerm, activeTab, programID): "bonnerEventsCount": bonnerEventsCount, "otherEventsCount": otherEventsCount }) - - managersProgramDict = getManagerProgramDict(g.current_user) - + # Fetch toggle state from session toggle_state = session.get('toggleState', 'unchecked') diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index dbd469932..c6a138839 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -32,7 +32,7 @@ $(document).ready(function(){ } } // ================== Event count Indicators =================== - $.ajax({ //NEED TO REPLACE 3 WITH CURRENT TERM************************************************* + $.ajax({ url: "/eventsList/" + $('#termID').val(), type: "GET", success: function(EventsCount) { From 3d13004d912efd71541af8d3c2a4ff2544c17a65 Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Wed, 31 Jul 2024 10:24:41 -0400 Subject: [PATCH 06/38] Toggle state is currently not being reflected on the backend --- app/controllers/main/routes.py | 41 ++++++------- app/static/js/event_list.js | 101 +++++++++++++++++---------------- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 6aef33fd3..e29d2c5ea 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -93,23 +93,27 @@ def events(selectedTerm, activeTab, programID): managersProgramDict = getManagerProgramDict(g.current_user) - # Get the count of events for each category to display in the event list page. - studentLedEventsCount: int = sum(list(getUpcomingStudentLedCount(term, currentTime).keys())) #only upcoming events - trainingEventsCount: int = len(getTrainingEvents(term, g.current_user)) - for event in getTrainingEvents(term, g.current_user): - if event.isPastEnd: - trainingEventsCount -= 1 - - bonnerEventsCount: int = len(getBonnerEvents(term)) - for event in getBonnerEvents(term): - if event.isPastEnd: - bonnerEventsCount -= 1 - - otherEventsCount: int = len(getOtherEvents(term)) - for event in getOtherEvents(term): - if event.isPastEnd: - otherEventsCount -= 1 - # Handle ajax request for Event type header number notifiers + # Fetch toggle state from session + toggle_state = session.get('toggleState', 'unchecked') + print("AYO", toggle_state) #REMOVE THIS LATER********************************************************* + # Get the count of upcoming events for each category to display in the event list page. + studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) + trainingEventsCount: int = len(trainingEvents) + bonnerEventsCount: int = len(bonnerEvents) + otherEventsCount: int = len(otherEvents) + + if (toggle_state == 'unchecked'): + studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) + for event in trainingEvents: + if event.isPastEnd: + trainingEventsCount -= 1 + for event in bonnerEvents: + if event.isPastEnd: + bonnerEventsCount -= 1 + for event in otherEvents: + if event.isPastEnd: + otherEventsCount -= 1 + # Handle ajax request for Event category header number notifiers if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ "studentLedEventsCount": studentLedEventsCount, @@ -118,9 +122,6 @@ def events(selectedTerm, activeTab, programID): "otherEventsCount": otherEventsCount }) - # Fetch toggle state from session - toggle_state = session.get('toggleState', 'unchecked') - return render_template("/events/event_list.html", selectedTerm = term, studentLedEvents = studentLedEvents, diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index c6a138839..32885830b 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -1,29 +1,31 @@ $(document).ready(function(){ - $("#removeRsvpBtn").click(function(){ - removeRsvpForEvent($("#removeRsvpBtn").val()) - }) - $("#rsvpBtn").click(function(){ - rsvpForEvent($("#rsvpBtn").val()) - }) + updateIndicatorCounts() - var viewPastEventsToggle = $("#viewPastEventsToggle"); - var isChecked = viewPastEventsToggle.prop("checked"); - toggleRows(isChecked); + $("#removeRsvpBtn").click(function(){ + removeRsvpForEvent($("#removeRsvpBtn").val()) + }) + $("#rsvpBtn").click(function(){ + rsvpForEvent($("#rsvpBtn").val()) + }) + var viewPastEventsToggle = $("#viewPastEventsToggle"); + var isChecked = viewPastEventsToggle.prop("checked"); + toggleRows(isChecked); + + viewPastEventsToggle.on("change", function(){ if (!g_isPastTerm) { - viewPastEventsToggle.on("change", function(){ - let isChecked = $(this).prop("checked"); - toggleRows(isChecked); - - // Update server state via AJAX POST - $.post("/updateToggleState", { - toggleState: isChecked ? "checked" : "unchecked", - }); - + let isChecked = $(this).prop("checked"); + toggleRows(isChecked); + // Update server state via AJAX POST + $.post("/updateToggleState", { + toggleState: isChecked ? "checked" : "unchecked", }); } + updateIndicatorCounts(); + }); function toggleRows(isChecked) { + console.log(isChecked, " checkstate"); var tableRows = $(".showlist"); if (isChecked) { tableRows.show(); @@ -31,37 +33,6 @@ $(document).ready(function(){ tableRows.hide(); } } -// ================== Event count Indicators =================== - $.ajax({ - url: "/eventsList/" + $('#termID').val(), - type: "GET", - success: function(EventsCount) { - const studentLedEventsCount = Number(EventsCount.studentLedEventsCount); - const trainingEventsCount = Number(EventsCount.trainingEventsCount); - const bonnerEventsCount = Number(EventsCount.bonnerEventsCount); - const otherEventsCount = Number(EventsCount.otherEventsCount); - msgFlash("HELLO", "success"); - console.log(EventsCount); - console.log(studentLedEventsCount, trainingEventsCount, bonnerEventsCount, otherEventsCount); - - if (studentLedEventsCount >= 0) { - $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); - } - if (trainingEventsCount >= 0) { - $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`); - } - if (bonnerEventsCount >= 0) { - $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`); - } - - if (otherEventsCount >= 0) { - $("#otherEvents").html(`Other Events (${otherEventsCount})`); - } - }, - error: function(request, status, error) { - console.log(status,error); - } - }); }); function rsvpForEvent(eventID){ @@ -98,3 +69,35 @@ function removeRsvpForEvent(eventID){ }) } +//gets number of events in each event list category +function updateIndicatorCounts(){ + $.ajax({ + url: "/eventsList/" + $('#termID').val(), + type: "GET", + success: function(EventsCount) { + const studentLedEventsCount = Number(EventsCount.studentLedEventsCount); + const trainingEventsCount = Number(EventsCount.trainingEventsCount); + const bonnerEventsCount = Number(EventsCount.bonnerEventsCount); + const otherEventsCount = Number(EventsCount.otherEventsCount); + msgFlash("HELLO", "success"); //REMOVE THESE LATER*********************** + console.log(EventsCount); + console.log(studentLedEventsCount, trainingEventsCount, bonnerEventsCount, otherEventsCount); + + if (studentLedEventsCount >= 0) { + $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); + } + if (trainingEventsCount >= 0) { + $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`); + } + if (bonnerEventsCount >= 0) { + $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`); + } + if (otherEventsCount >= 0) { + $("#otherEvents").html(`Other Events (${otherEventsCount})`); + } + }, + error: function(request, status, error) { + console.log(status,error); + } + }); +} \ No newline at end of file From 57fa9a3a1553870f4d065f39505d14fe94c1742c Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Wed, 31 Jul 2024 13:35:36 -0400 Subject: [PATCH 07/38] event counts are properly reflected, except for student led and now the toggle is also busted --- app/controllers/main/routes.py | 12 +++++++++--- app/static/js/event_list.js | 32 +++++++++++++++++++------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index e29d2c5ea..921001105 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -94,7 +94,11 @@ def events(selectedTerm, activeTab, programID): managersProgramDict = getManagerProgramDict(g.current_user) # Fetch toggle state from session - toggle_state = session.get('toggleState', 'unchecked') + #toggle_state = session.get('toggleState', 'unchecked') #REMOVE LATER MAYBE************************************ + + toggle_state = request.args.get('toggleState', 'unchecked') + # Update session with toggle state + #session['toggleState'] = toggle_state print("AYO", toggle_state) #REMOVE THIS LATER********************************************************* # Get the count of upcoming events for each category to display in the event list page. studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) @@ -119,7 +123,8 @@ def events(selectedTerm, activeTab, programID): "studentLedEventsCount": studentLedEventsCount, "trainingEventsCount": trainingEventsCount, "bonnerEventsCount": bonnerEventsCount, - "otherEventsCount": otherEventsCount + "otherEventsCount": otherEventsCount, + "toggle_state": toggle_state }) return render_template("/events/event_list.html", @@ -143,9 +148,10 @@ def events(selectedTerm, activeTab, programID): @main_bp.route('/updateToggleState', methods=['POST']) def update_toggle_state(): toggle_state = request.form.get('toggleState') - # Update session with toggle state session['toggleState'] = toggle_state + print("toggle state ", toggle_state) + print("Toggle state updated to:", session.get('toggleState')) return "", 200 diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index 32885830b..1a5ff844c 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -1,5 +1,4 @@ $(document).ready(function(){ - updateIndicatorCounts() $("#removeRsvpBtn").click(function(){ removeRsvpForEvent($("#removeRsvpBtn").val()) @@ -12,16 +11,16 @@ $(document).ready(function(){ var isChecked = viewPastEventsToggle.prop("checked"); toggleRows(isChecked); + //updateIndicatorCounts(isChecked) + viewPastEventsToggle.on("change", function(){ if (!g_isPastTerm) { let isChecked = $(this).prop("checked"); toggleRows(isChecked); - // Update server state via AJAX POST - $.post("/updateToggleState", { - toggleState: isChecked ? "checked" : "unchecked", - }); } - updateIndicatorCounts(); + // $.post("/updateToggleState", { + // toggleState: isChecked ? "checked" : "unchecked" + // }) }); function toggleRows(isChecked) { @@ -32,6 +31,7 @@ $(document).ready(function(){ } else { tableRows.hide(); } + updateIndicatorCounts(isChecked); } }); @@ -70,19 +70,25 @@ function removeRsvpForEvent(eventID){ } //gets number of events in each event list category -function updateIndicatorCounts(){ +function updateIndicatorCounts(isChecked){ $.ajax({ url: "/eventsList/" + $('#termID').val(), type: "GET", - success: function(EventsCount) { - const studentLedEventsCount = Number(EventsCount.studentLedEventsCount); - const trainingEventsCount = Number(EventsCount.trainingEventsCount); - const bonnerEventsCount = Number(EventsCount.bonnerEventsCount); - const otherEventsCount = Number(EventsCount.otherEventsCount); + data: { + toggleState: isChecked ? "checked" : "unchecked", + }, + success: function(eventsCount) { + const studentLedEventsCount = Number(eventsCount.studentLedEventsCount); + const trainingEventsCount = Number(eventsCount.trainingEventsCount); + const bonnerEventsCount = Number(eventsCount.bonnerEventsCount); + const otherEventsCount = Number(eventsCount.otherEventsCount); + const toggle_state = eventsCount.toggle_state; msgFlash("HELLO", "success"); //REMOVE THESE LATER*********************** - console.log(EventsCount); + console.log(eventsCount); console.log(studentLedEventsCount, trainingEventsCount, bonnerEventsCount, otherEventsCount); + $("#viewPastEventsToggle").prop('checked', toggle_state === 'checked'); + if (studentLedEventsCount >= 0) { $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); } From 77c2c8317b3ffeb545b39fb9a6f882fa59506f0f Mon Sep 17 00:00:00 2001 From: Johncox2211 Date: Wed, 31 Jul 2024 15:53:59 -0400 Subject: [PATCH 08/38] Indicator numbers stay as intended now between toggles. Toggle stays consistent accross pages and its state is now properly held in the backend. Cleaned up debug statements and unused comments/ code. --- app/controllers/main/routes.py | 31 ++++++++++--------------------- app/static/js/event_list.js | 28 ++++++++++++---------------- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 921001105..6d76eec7a 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -93,19 +93,18 @@ def events(selectedTerm, activeTab, programID): managersProgramDict = getManagerProgramDict(g.current_user) - # Fetch toggle state from session - #toggle_state = session.get('toggleState', 'unchecked') #REMOVE LATER MAYBE************************************ - + # Fetch toggle state from session toggle_state = request.args.get('toggleState', 'unchecked') - # Update session with toggle state - #session['toggleState'] = toggle_state - print("AYO", toggle_state) #REMOVE THIS LATER********************************************************* - # Get the count of upcoming events for each category to display in the event list page. - studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) + + # Get the count of all term events for each category to display in the event list page. + studentEvents = [event for sublist in studentLedEvents.values() for event in sublist] + + studentLedEventsCount: int = len(studentEvents) trainingEventsCount: int = len(trainingEvents) bonnerEventsCount: int = len(bonnerEvents) otherEventsCount: int = len(otherEvents) - + toggleStatus: str = toggle_state + #gets only upcoming events to display in indicators if (toggle_state == 'unchecked'): studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) for event in trainingEvents: @@ -117,14 +116,14 @@ def events(selectedTerm, activeTab, programID): for event in otherEvents: if event.isPastEnd: otherEventsCount -= 1 - # Handle ajax request for Event category header number notifiers + # Handle ajax request for Event category header number notifiers and toggle if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ "studentLedEventsCount": studentLedEventsCount, "trainingEventsCount": trainingEventsCount, "bonnerEventsCount": bonnerEventsCount, "otherEventsCount": otherEventsCount, - "toggle_state": toggle_state + "toggleStatus": toggleStatus }) return render_template("/events/event_list.html", @@ -145,16 +144,6 @@ def events(selectedTerm, activeTab, programID): toggle_state = toggle_state, ) -@main_bp.route('/updateToggleState', methods=['POST']) -def update_toggle_state(): - toggle_state = request.form.get('toggleState') - # Update session with toggle state - session['toggleState'] = toggle_state - print("toggle state ", toggle_state) - print("Toggle state updated to:", session.get('toggleState')) - - return "", 200 - @main_bp.route('/profile/', methods=['GET']) def viewUsersProfile(username): """ diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index 1a5ff844c..fb51637a2 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -6,32 +6,31 @@ $(document).ready(function(){ $("#rsvpBtn").click(function(){ rsvpForEvent($("#rsvpBtn").val()) }) - + //ensure that toggle state is consistent across terms + var toggleState = sessionStorage.getItem('toggleState') || 'unchecked'; var viewPastEventsToggle = $("#viewPastEventsToggle"); + viewPastEventsToggle.prop('checked', toggleState === 'checked'); var isChecked = viewPastEventsToggle.prop("checked"); toggleRows(isChecked); - - //updateIndicatorCounts(isChecked) - + + updateIndicatorCounts(isChecked) + //update indicator numbers when toggle is changed viewPastEventsToggle.on("change", function(){ if (!g_isPastTerm) { let isChecked = $(this).prop("checked"); toggleRows(isChecked); + updateIndicatorCounts(isChecked); + sessionStorage.setItem('toggleState', isChecked ? "checked" : "unchecked"); } - // $.post("/updateToggleState", { - // toggleState: isChecked ? "checked" : "unchecked" - // }) }); function toggleRows(isChecked) { - console.log(isChecked, " checkstate"); var tableRows = $(".showlist"); if (isChecked) { tableRows.show(); } else { tableRows.hide(); } - updateIndicatorCounts(isChecked); } }); @@ -69,7 +68,7 @@ function removeRsvpForEvent(eventID){ }) } -//gets number of events in each event list category +//gets number indicator of events in each event list category function updateIndicatorCounts(isChecked){ $.ajax({ url: "/eventsList/" + $('#termID').val(), @@ -82,12 +81,9 @@ function updateIndicatorCounts(isChecked){ const trainingEventsCount = Number(eventsCount.trainingEventsCount); const bonnerEventsCount = Number(eventsCount.bonnerEventsCount); const otherEventsCount = Number(eventsCount.otherEventsCount); - const toggle_state = eventsCount.toggle_state; - msgFlash("HELLO", "success"); //REMOVE THESE LATER*********************** - console.log(eventsCount); - console.log(studentLedEventsCount, trainingEventsCount, bonnerEventsCount, otherEventsCount); - - $("#viewPastEventsToggle").prop('checked', toggle_state === 'checked'); + const toggleStatus = eventsCount.toggleStatus; + + $("#viewPastEventsToggle").prop(toggleStatus, true); if (studentLedEventsCount >= 0) { $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); From cb02485d9ba851ed2dce2f1078e7bffb95927384 Mon Sep 17 00:00:00 2001 From: Finn Bledsoe Date: Thu, 22 Aug 2024 14:22:28 -0400 Subject: [PATCH 09/38] fixed javascript error and made (0) not appear when there are no events. --- app/static/js/event_list.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index a99fa4373..39e254346 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -85,16 +85,16 @@ function updateIndicatorCounts(isChecked){ $("#viewPastEventsToggle").prop(toggleStatus, true); - if (studentLedEventsCount >= 0) { + if (studentLedEventsCount > 0) { $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); } - if (trainingEventsCount >= 0) { + if (trainingEventsCount > 0) { $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`); } - if (bonnerEventsCount >= 0) { + if (bonnerEventsCount > 0) { $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`); } - if (otherEventsCount >= 0) { + if (otherEventsCount > 0) { $("#otherEvents").html(`Other Events (${otherEventsCount})`); } }, @@ -102,3 +102,4 @@ function updateIndicatorCounts(isChecked){ console.log(status,error); } }); +} From 3317eede1fbc1e6102f0808f59bf29cae25b2585 Mon Sep 17 00:00:00 2001 From: Finn Bledsoe Date: Thu, 22 Aug 2024 14:23:25 -0400 Subject: [PATCH 10/38] changed variable that used snake instead of camel --- app/controllers/main/routes.py | 8 ++++---- app/templates/events/event_list.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 6d76eec7a..fb7dddcdb 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -94,7 +94,7 @@ def events(selectedTerm, activeTab, programID): managersProgramDict = getManagerProgramDict(g.current_user) # Fetch toggle state from session - toggle_state = request.args.get('toggleState', 'unchecked') + toggleState = request.args.get('toggleState', 'unchecked') # Get the count of all term events for each category to display in the event list page. studentEvents = [event for sublist in studentLedEvents.values() for event in sublist] @@ -103,9 +103,9 @@ def events(selectedTerm, activeTab, programID): trainingEventsCount: int = len(trainingEvents) bonnerEventsCount: int = len(bonnerEvents) otherEventsCount: int = len(otherEvents) - toggleStatus: str = toggle_state + toggleStatus: str = toggleState #gets only upcoming events to display in indicators - if (toggle_state == 'unchecked'): + if (toggleState == 'unchecked'): studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) for event in trainingEvents: if event.isPastEnd: @@ -141,7 +141,7 @@ def events(selectedTerm, activeTab, programID): programID = int(programID), managersProgramDict = managersProgramDict, countUpcomingStudentLedEvents = countUpcomingStudentLedEvents, - toggle_state = toggle_state, + toggleState = toggleState, ) @main_bp.route('/profile/', methods=['GET']) diff --git a/app/templates/events/event_list.html b/app/templates/events/event_list.html index a12d0b44c..92a365a6f 100644 --- a/app/templates/events/event_list.html +++ b/app/templates/events/event_list.html @@ -38,7 +38,7 @@

Events List for {{selectedTerm.description}}

{% if selectedTerm.isFutureTerm or selectedTerm.isCurrentTerm %} - + {% else %} {% endif %} From 13d85eede5d1a74cc39d616c9d68eb74d39e8dba Mon Sep 17 00:00:00 2001 From: bledsoef Date: Sat, 24 Aug 2024 23:43:20 -0400 Subject: [PATCH 11/38] cleaned up code for readibility and removed redundant variables --- app/controllers/main/routes.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index fb7dddcdb..98f032f60 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -82,7 +82,7 @@ def events(selectedTerm, activeTab, programID): participantRSVP = EventRsvp.select(EventRsvp, Event).join(Event).where(EventRsvp.user == g.current_user) rsvpedEventsID = [event.event.id for event in participantRSVP] - term = Term.get_by_id(currentTerm) + term: Term = Term.get_by_id(currentTerm) currentEventRsvpAmount = getEventRsvpCountsForTerm(term) studentLedEvents = getStudentLedEvents(term) @@ -96,15 +96,18 @@ def events(selectedTerm, activeTab, programID): # Fetch toggle state from session toggleState = request.args.get('toggleState', 'unchecked') - # Get the count of all term events for each category to display in the event list page. - studentEvents = [event for sublist in studentLedEvents.values() for event in sublist] + # compile all student led events into one list + studentEvents = [] + for studentEvent in studentLedEvents.values(): + studentEvents += studentEvent # add all contents of studentEvent to the studentEvents list + # Get the count of all term events for each category to display in the event list page. studentLedEventsCount: int = len(studentEvents) trainingEventsCount: int = len(trainingEvents) bonnerEventsCount: int = len(bonnerEvents) otherEventsCount: int = len(otherEvents) - toggleStatus: str = toggleState - #gets only upcoming events to display in indicators + + # gets only upcoming events to display in indicators if (toggleState == 'unchecked'): studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) for event in trainingEvents: @@ -116,6 +119,7 @@ def events(selectedTerm, activeTab, programID): for event in otherEvents: if event.isPastEnd: otherEventsCount -= 1 + # Handle ajax request for Event category header number notifiers and toggle if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ @@ -123,7 +127,7 @@ def events(selectedTerm, activeTab, programID): "trainingEventsCount": trainingEventsCount, "bonnerEventsCount": bonnerEventsCount, "otherEventsCount": otherEventsCount, - "toggleStatus": toggleStatus + "toggleStatus": toggleState }) return render_template("/events/event_list.html", From 49682e9e1f4d9395cc15dc7096631a3de7723948 Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Wed, 28 Aug 2024 17:00:58 -0400 Subject: [PATCH 12/38] Added some javascript to check for duplicate events before we let them save the modal and cleaned up code --- app/static/js/createEvents.js | 80 +++++++++++++++++++--------- app/templates/admin/createEvent.html | 6 +-- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index ab1deed2e..477a43353 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -49,7 +49,7 @@ function calculateRecurringEventFrequency(){ var recurringEvents = JSON.parse(jsonData) var recurringTable = $("#recurringEventsTable") $("#recurringEventsTable tbody tr").remove(); - for (var event of recurringEvents){ + for(var event of recurringEvents){ var eventdate = new Date(event.date).toLocaleDateString() recurringTable.append(""+event.name+""+eventdate+""); } @@ -59,25 +59,29 @@ function calculateRecurringEventFrequency(){ } }); } - $('#submitParticipant').on('click', function() { + $('#multipleOfferingSave').on('click', function() { //Requires that modal info updated before it can be saved, gives notifier if there are empty fields - let eventNameInputs = document.querySelectorAll('.multipleOfferingNameField'); - let datePickerInputs = document.querySelectorAll('.multipleOfferingDatePicker'); - let startTimeInputs = document.querySelectorAll('.multipleOfferingStartTime'); - let endTimeInputs = document.querySelectorAll('.multipleOfferingEndTime'); + let eventOfferings = $('.eventOffering'); + let eventNameInputs = $('.multipleOfferingNameField'); + let datePickerInputs = $('.multipleOfferingDatePicker'); + let startTimeInputs = $('.multipleOfferingStartTime'); + let endTimeInputs = $('.multipleOfferingEndTime'); let isEmpty = false; - let timeCheck = false; - eventNameInputs.forEach(eventNameInput => { - // Check if the input field is empty + let hasValidTimes = true; + let hasDuplicateListings = false; + + // Check if the input field is empty + eventNameInputs.each((index, eventNameInput) => { if (eventNameInput.value.trim() === '') { - isEmpty = true; - $(eventNameInput).addClass('border-red'); + isEmpty = true; + $(eventNameInput).addClass('border-red'); } else{ $(eventNameInput).removeClass('border-red'); } - }); - datePickerInputs.forEach(datePickerInput => { - // Check if the input field is empty + }); + + // Check if the date input field is empty + datePickerInputs.each((index, datePickerInput) => { if (datePickerInput.value.trim() === '') { isEmpty = true; $(datePickerInput).addClass('border-red'); @@ -86,19 +90,37 @@ function calculateRecurringEventFrequency(){ } }); + // Check if the start time is after the end time for(let i = 0; i < startTimeInputs.length; i++){ - if(startTimeInputs[i].value >= endTimeInputs[i].value){ - console.log(startTimeInputs[i]); - console.log(endTimeInputs[i]); - $(startTimeInputs[i]).addClass('border-red'); - $(endTimeInputs[i]).addClass('border-red'); - timeCheck = true; - }else{ + if(startTimeInputs[i].value < endTimeInputs[i].value){ $(startTimeInputs[i]).removeClass('border-red'); $(endTimeInputs[i]).removeClass('border-red'); + } else { + $(startTimeInputs[i]).addClass('border-red'); + $(endTimeInputs[i]).addClass('border-red'); + hasValidTimes = false; } - console.log(timeCheck); } + + // Check if there are duplicate event offerings + let eventListings = {}; + for(let i = 0; i < eventOfferings.length; i++){ + let eventName = eventNameInputs[i].value + let date = datePickerInputs[i].value.trim() + let startTime = startTimeInputs[i].value + let endTime = endTimeInputs[i].value + let eventListing = JSON.stringify([eventName, date, startTime, endTime]) + + if (eventListing in eventListings){ // If we've seen this event before mark this event and the previous as duplicates + hasDuplicateListings = true + $(eventOfferings[i]).addClass('border-red'); + $(eventOfferings[eventListings[eventListing]]).addClass('border-red') + } else { // If we haven't seen this event before + $(eventOfferings[i]).removeClass('border-red'); + eventListings[eventListing] = i + } + } + if (isEmpty){ $('#textNotifierPadding').addClass('pt-5'); $('.invalidFeedback').text("Event name or date field is empty"); @@ -107,9 +129,8 @@ function calculateRecurringEventFrequency(){ $('.invalidFeedback').css('display', 'none'); $('#textNotifierPadding').removeClass('pt-5') }); - isEmpty = false; } - else if(timeCheck){ + else if(!hasValidTimes){ $('#textNotifierPadding').addClass('pt-5'); $('.invalidFeedback').text("Event end time must be after start time"); $('.invalidFeedback').css('display', 'block'); @@ -117,8 +138,15 @@ function calculateRecurringEventFrequency(){ $('.invalidFeedback').css('display', 'none'); $('#textNotifierPadding').removeClass('pt-5') }); - timeCheck= false; - + } + else if (hasDuplicateListings){ + $('#textNotifierPadding').addClass('pt-5'); + $('.invalidFeedback').text("Two event listings cannot have the same event name, date, and time"); + $('.invalidFeedback').css('display', 'block'); + $('.invalidFeedback').on('animationend', function() { + $('.invalidFeedback').css('display', 'none'); + $('#textNotifierPadding').removeClass('pt-5') + }); } else { storeMultipleOfferingEventAttributes(); diff --git a/app/templates/admin/createEvent.html b/app/templates/admin/createEvent.html index 7def298b0..318be873f 100644 --- a/app/templates/admin/createEvent.html +++ b/app/templates/admin/createEvent.html @@ -467,7 +467,7 @@
-
+

@@ -535,8 +535,8 @@ {% set disableSave = "disabled" if cpPreviewErrors|count else "" %} - +
From 6bc07d7dddb31be58060d861da9da83321dc63e0 Mon Sep 17 00:00:00 2001 From: Stevenson Date: Fri, 30 Aug 2024 14:04:01 +0000 Subject: [PATCH 13/38] Modified invalid feedback message --- app/static/js/createEvents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index 477a43353..735aa28cf 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -141,7 +141,7 @@ function calculateRecurringEventFrequency(){ } else if (hasDuplicateListings){ $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Two event listings cannot have the same event name, date, and time"); + $('.invalidFeedback').text("Event listings cannot have the same event name, date, and time"); $('.invalidFeedback').css('display', 'block'); $('.invalidFeedback').on('animationend', function() { $('.invalidFeedback').css('display', 'none'); From 83fbd81824d4390aa7a349c8cdd6c1e07c220304 Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Wed, 4 Sep 2024 14:05:06 -0400 Subject: [PATCH 14/38] Fixed spacing of the add new offerings + button --- app/templates/admin/createEvent.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/templates/admin/createEvent.html b/app/templates/admin/createEvent.html index 318be873f..08e45fa6d 100644 --- a/app/templates/admin/createEvent.html +++ b/app/templates/admin/createEvent.html @@ -526,11 +526,10 @@
-
+
-
-{% include 'emailModal.html' %} +{% include 'events/emailModal.html' %} diff --git a/app/templates/eventNav.html b/app/templates/events/eventNav.html similarity index 100% rename from app/templates/eventNav.html rename to app/templates/events/eventNav.html diff --git a/app/templates/eventView.html b/app/templates/events/eventView.html similarity index 99% rename from app/templates/eventView.html rename to app/templates/events/eventView.html index 8168a0835..59a8c83e1 100644 --- a/app/templates/eventView.html +++ b/app/templates/events/eventView.html @@ -2,7 +2,7 @@ {% set page_title = eventData.name %} {% set isPastEnd = event.isPastEnd %} {% set tabName = 'view' %} -{% extends "eventNav.html"%} +{% extends "events/eventNav.html"%} {% set eventPast = event.isPastStart if not isNewEvent else False %} diff --git a/app/templates/events/manageVolunteers.html b/app/templates/events/manageVolunteers.html index 418dae3d4..37fb025c4 100644 --- a/app/templates/events/manageVolunteers.html +++ b/app/templates/events/manageVolunteers.html @@ -2,7 +2,7 @@ {% set page_title = event.name %} {% set eventPast = event.isPastStart %} {% set tabName = 'manageVolunteers' %} -{% extends "eventNav.html" %} +{% extends "events/eventNav.html" %} diff --git a/app/templates/events/rsvpLog.html b/app/templates/events/rsvpLog.html index 05ad3390f..74b41887e 100644 --- a/app/templates/events/rsvpLog.html +++ b/app/templates/events/rsvpLog.html @@ -2,7 +2,7 @@ {% set page_title = event.name %} {% set eventPast = event.isPastStart %} {% set tabName = 'rsvpLog' %} -{% extends "eventNav.html" %} +{% extends "events/eventNav.html" %} {% block scripts %} {{super()}} diff --git a/app/templates/events/volunteerDetails.html b/app/templates/events/volunteerDetails.html index 483ff0396..fa2c74aef 100644 --- a/app/templates/events/volunteerDetails.html +++ b/app/templates/events/volunteerDetails.html @@ -2,7 +2,7 @@ {% set page_title = event.name %} {% set eventPast = event.isPastStart %} {% set tabName = 'details' %} -{% extends "eventNav.html" %} +{% extends "events/eventNav.html" %} {% block styles %} {{super()}} From 09f72cda03a6a5a621bc7c6eca687ab0d18db9ea Mon Sep 17 00:00:00 2001 From: vungc Date: Thu, 5 Sep 2024 17:10:41 -0400 Subject: [PATCH 18/38] deleted empty file --- app/templates/minor/communityEngagement.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/templates/minor/communityEngagement.html diff --git a/app/templates/minor/communityEngagement.html b/app/templates/minor/communityEngagement.html deleted file mode 100644 index e69de29bb..000000000 From c3f86c4836d877aa5755bdedd4b58ccceea19bac Mon Sep 17 00:00:00 2001 From: vungc Date: Fri, 6 Sep 2024 09:01:29 -0400 Subject: [PATCH 19/38] created a macros folder and moved the two macro files into the folder --- app/templates/events/createEvent.html | 2 +- app/templates/events/manageVolunteers.html | 2 +- app/templates/{ => macros}/displayFilesMacro.html | 0 app/templates/{ => macros}/trainingsHoverMacro.html | 0 app/templates/main/userProfile.html | 2 +- app/templates/minor/requestOtherEngagement.html | 2 +- app/templates/serviceLearning/slcProposal.html | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename app/templates/{ => macros}/displayFilesMacro.html (100%) rename app/templates/{ => macros}/trainingsHoverMacro.html (100%) diff --git a/app/templates/events/createEvent.html b/app/templates/events/createEvent.html index 50edeabe8..7aa9724be 100644 --- a/app/templates/events/createEvent.html +++ b/app/templates/events/createEvent.html @@ -331,7 +331,7 @@

{{page_title}}

{% if filepaths %}
- {% from 'displayFilesMacro.html' import displayFiles %} + {% from 'macros/displayFilesMacro.html' import displayFiles %} {{ displayFiles(filepaths, 'Event Attachments', '/deleteEventFile', eventData.id)}}
diff --git a/app/templates/events/manageVolunteers.html b/app/templates/events/manageVolunteers.html index 37fb025c4..e6325701c 100644 --- a/app/templates/events/manageVolunteers.html +++ b/app/templates/events/manageVolunteers.html @@ -22,7 +22,7 @@ {% block app_content %} {{super()}} -{% from 'trainingsHoverMacro.html' import trainingsHover %} +{% from 'macros/trainingsHoverMacro.html' import trainingsHover %} {% set rsvpNeeded = (event.isRsvpRequired) and (not event.isPastStart) %} {% set rsvpFull = rsvpNeeded and (event.rsvpLimit is not none) and (currentRsvpAmount >= event.rsvpLimit) %} diff --git a/app/templates/displayFilesMacro.html b/app/templates/macros/displayFilesMacro.html similarity index 100% rename from app/templates/displayFilesMacro.html rename to app/templates/macros/displayFilesMacro.html diff --git a/app/templates/trainingsHoverMacro.html b/app/templates/macros/trainingsHoverMacro.html similarity index 100% rename from app/templates/trainingsHoverMacro.html rename to app/templates/macros/trainingsHoverMacro.html diff --git a/app/templates/main/userProfile.html b/app/templates/main/userProfile.html index 0f9b4804e..9ae362695 100644 --- a/app/templates/main/userProfile.html +++ b/app/templates/main/userProfile.html @@ -204,7 +204,7 @@

{% if row.program in programsInterested %} {% set checked = "checked" %} {% endif %} - {% from 'trainingsHoverMacro.html' import trainingsHover %} + {% from 'macros/trainingsHoverMacro.html' import trainingsHover %} {{row.program.programName}} diff --git a/app/templates/minor/requestOtherEngagement.html b/app/templates/minor/requestOtherEngagement.html index 0464b1df3..5d056b5a1 100644 --- a/app/templates/minor/requestOtherEngagement.html +++ b/app/templates/minor/requestOtherEngagement.html @@ -128,7 +128,7 @@

Experience Information

{% if filepaths %}
- {% from 'displayFilesMacro.html' import displayFiles %} + {% from 'macros/displayFilesMacro.html' import displayFiles %} {# this call to deleteRequestFiles won't work. Fix when this becomes relevant #} {{ displayFiles(filepaths,'RequestOtherEngagement', '/deleteRequestFile', term.id) }}
diff --git a/app/templates/serviceLearning/slcProposal.html b/app/templates/serviceLearning/slcProposal.html index a864d3136..5fb99b65f 100644 --- a/app/templates/serviceLearning/slcProposal.html +++ b/app/templates/serviceLearning/slcProposal.html @@ -107,7 +107,7 @@

Status: {% if filePaths %}
- {% from 'displayFilesMacro.html' import displayFiles %} + {% from 'macros/displayFilesMacro.html' import displayFiles %} {{ displayFiles(filePaths, 'Course Attachments', '/deleteCourseFile', course.id) }}
{% endif %} From eef92264e727f0bba80c1a77cdf7c1101cd6e82a Mon Sep 17 00:00:00 2001 From: makindeo Date: Fri, 6 Sep 2024 12:43:09 -0400 Subject: [PATCH 20/38] Removed the outdated 'addParticipants' and 'outsideParticipant' models from our application --- .vscode/settings.json | 2 + app/controllers/admin/routes.py | 7 --- app/models/outsideParticipant.py | 9 ---- app/static/js/addParticipants.js | 54 ---------------------- app/templates/addParticipants.html | 73 ------------------------------ database/migrate_db.sh | 1 - database/prod-backup.sql | 29 ------------ 7 files changed, 2 insertions(+), 173 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 app/models/outsideParticipant.py delete mode 100644 app/static/js/addParticipants.js delete mode 100644 app/templates/addParticipants.html diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 88a1a5468..4a25e955a 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -473,13 +473,6 @@ def studentSearchPage(): return render_template("/admin/searchStudentPage.html") abort(403) -@admin_bp.route('/addParticipants', methods = ['GET']) -def addParticipants(): - '''Renders the page, will be removed once merged with full page''' - - return render_template('addParticipants.html', - title="Add Participants") - @admin_bp.route('/activityLogs', methods = ['GET', 'POST']) def activityLogs(): if g.current_user.isCeltsAdmin: diff --git a/app/models/outsideParticipant.py b/app/models/outsideParticipant.py deleted file mode 100644 index 402ffbd08..000000000 --- a/app/models/outsideParticipant.py +++ /dev/null @@ -1,9 +0,0 @@ -from app.models import* -from app.models.event import Event - -class OutsideParticipant(baseModel): - event = ForeignKeyField(Event) - firstName = CharField() - lastName = CharField() - email = CharField() - phoneNumber = CharField() diff --git a/app/static/js/addParticipants.js b/app/static/js/addParticipants.js deleted file mode 100644 index 7659e0175..000000000 --- a/app/static/js/addParticipants.js +++ /dev/null @@ -1,54 +0,0 @@ -function searchVolunteer(){ -var query = $("#volunteerInput").val() -$("#volunteerInput").autocomplete({ - minLength: 2, - source: function(request, response){ - $.ajax({ - url: "/searchStudents/" + query, - type: "GET", - contentType: "application/json", - data: query, - dataType: "json", - success: function(dictToJSON) { - response($.map( dictToJSON, function( item ) { - return { - label: item, - value: dictToJSON[item] - } - })) - }, - error: function(request, status, error) { - console.log(status,error); - } - }) - }, - select: function( event, ui ) { - var volunteerName = ui.item.value - $("#Volunteertable").append('' + volunteerName + '') - - } - }); -}; -function removeRow(e) { - $(e).parent().parent().remove(); -} - -count = 0; -function addOutsideParticipant() { - firstName = $("#firstNameTextarea").val(); - lastName = $("#lastNameTextarea").val(); - emailEntry = $("#emailTextarea").val(); - phoneNumber = $("#phoneNumberTextarea").val(); - $("#OutsideTable").append('' + firstName + " " + lastName + " " + '' + emailEntry + " " +'' + phoneNumber + " " + ''); - opList = ["email", "firstName", "lastName", "phoneNumber"]; - opList.forEach(item => { - $("") - .attr("value", $('#'+item+'Textarea').val()) - .attr("id", item+count) - .attr("name", item+count) - .appendTo("#"+count) - $('#'+item+'Textarea').val('').blur(); - }) - count++; - $('#particpantsModal').modal('hide') -} diff --git a/app/templates/addParticipants.html b/app/templates/addParticipants.html deleted file mode 100644 index cdf7ee481..000000000 --- a/app/templates/addParticipants.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "base.html" %} - -{% block scripts %} -{{super()}} - - -{% endblock %} - -{% block app_content %} - - - - - -
-

Add Volunteers*

-
- -
- - - - - -
NameRemove
-
-
-

Add Outside Participants

- - - - - - - - -
NameEmailPhone NumberRemove
-
- -{% endblock %} diff --git a/database/migrate_db.sh b/database/migrate_db.sh index a81f759ac..67d1473df 100755 --- a/database/migrate_db.sh +++ b/database/migrate_db.sh @@ -42,7 +42,6 @@ pem add app.models.eventTemplate.EventTemplate pem add app.models.eventParticipant.EventParticipant pem add app.models.interest.Interest pem add app.models.note.Note -pem add app.models.outsideParticipant.OutsideParticipant pem add app.models.partner.Partner pem add app.models.program.Program pem add app.models.user.User diff --git a/database/prod-backup.sql b/database/prod-backup.sql index 2b2981997..fb59af80f 100644 --- a/database/prod-backup.sql +++ b/database/prod-backup.sql @@ -920,35 +920,6 @@ INSERT INTO `note` VALUES (1,'lyonss','2023-03-30 13:23:33','Notes are not visib /*!40000 ALTER TABLE `note` ENABLE KEYS */; UNLOCK TABLES; --- --- Table structure for table `outsideparticipant` --- - -DROP TABLE IF EXISTS `outsideparticipant`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `outsideparticipant` ( - `id` int NOT NULL AUTO_INCREMENT, - `event_id` int NOT NULL, - `firstName` varchar(255) NOT NULL, - `lastName` varchar(255) NOT NULL, - `email` varchar(255) NOT NULL, - `phoneNumber` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `outsideparticipant_event_id` (`event_id`), - CONSTRAINT `outsideparticipant_ibfk_1` FOREIGN KEY (`event_id`) REFERENCES `event` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `outsideparticipant` --- - -LOCK TABLES `outsideparticipant` WRITE; -/*!40000 ALTER TABLE `outsideparticipant` DISABLE KEYS */; -/*!40000 ALTER TABLE `outsideparticipant` ENABLE KEYS */; -UNLOCK TABLES; - -- -- Table structure for table `partner` -- From f978394757e35e96dc1ee80b64fdd75cf1316605 Mon Sep 17 00:00:00 2001 From: bledsoef Date: Fri, 6 Sep 2024 15:34:28 -0400 Subject: [PATCH 21/38] added logic to make sure that when there are no events, a number does not populate --- app/static/js/event_list.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/app/static/js/event_list.js b/app/static/js/event_list.js index 39e254346..305a96bc0 100644 --- a/app/static/js/event_list.js +++ b/app/static/js/event_list.js @@ -85,18 +85,11 @@ function updateIndicatorCounts(isChecked){ $("#viewPastEventsToggle").prop(toggleStatus, true); - if (studentLedEventsCount > 0) { - $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`); - } - if (trainingEventsCount > 0) { - $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`); - } - if (bonnerEventsCount > 0) { - $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`); - } - if (otherEventsCount > 0) { - $("#otherEvents").html(`Other Events (${otherEventsCount})`); - } + // use ternary operators to populate the tab with a number if there are events, and clear the count if there are none + studentLedEventsCount > 0 ? $("#studentLedEvents").html(`Student Led Service (${studentLedEventsCount})`) : $("#studentLedEvents").html(`Student Led Service`) + trainingEventsCount > 0 ? $("#trainingEvents").html(`Training and Education (${trainingEventsCount})`) : $("#trainingEvents").html(`Training and Education`) + bonnerEventsCount > 0 ? $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`) : $("#bonnerScholarsEvents").html(`Bonner Scholars`) + otherEventsCount > 0 ? $("#otherEvents").html(`Other Events (${otherEventsCount})`) : $("#otherEvents").html(`Other Events`) }, error: function(request, status, error) { console.log(status,error); From b471411fe5450bdc3b31fabee5ba4a9f93a23dfa Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Fri, 6 Sep 2024 17:03:23 -0400 Subject: [PATCH 22/38] Started refactoring the way we save multiple offerings so we can check which offerings failed to save and report that info to the user --- app/controllers/admin/routes.py | 29 +++----------- app/logic/events.py | 67 ++++++++++++++++++++++++++------- tests/code/test_events.py | 12 +++--- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 85daa7216..39d354e05 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -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 @@ -83,7 +82,6 @@ def templateSelect(): @admin_bp.route('/eventTemplates///create', methods=['GET','POST']) def createEvent(templateid, programid): - savedEventsList = [] if not (g.current_user.isAdmin or g.current_user.isProgramManagerFor(programid)): abort(403) @@ -117,25 +115,9 @@ def createEvent(templateid, programid): if request.method == "POST": eventData.update(request.form.copy()) 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) - + succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) + if not succeeded: + print(f"Failed to save offerings {failedSavedOfferings}") else: try: savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request)) @@ -157,6 +139,7 @@ def createEvent(templateid, programid): if len(savedEvents) > 1 and eventData.get('isRecurring'): createActivityLog(f"Created a recurring event, {savedEvents[0].name}, 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')}.") + # TODO: Change the way that this logic works since we're no longer using a nested savedEventsList and are using savedEvents instead like the lines above elif len(savedEventsList) >= 1 and eventData.get('isMultipleOffering'): modifiedSavedEvents = [item for sublist in savedEventsList for item in sublist] @@ -452,7 +435,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) diff --git a/app/logic/events.py b/app/logic/events.py index b85724b95..5f24f8fc2 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -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 @@ -95,6 +96,45 @@ def deleteAllRecurringEvents(eventId): eventId = allRecurringEvents[0].id return deleteEventAndAllFollowing(eventId) +def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): + """ + 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: + (Whether all saves were successful, the list of saved offering objects, a list of the indecies of all failed offerings) + """ + savedOfferings = [] + failedSavedOfferingIndicies = [] + + # Creates a shared multipleOfferingId for all offerings to have + multipleOfferingId = calculateNewMultipleOfferingId() + + # Create separate event data inheriting from the original eventData + multipleOfferingData = json.loads(eventData.get('multipleOfferingData')) + for i, 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 + savedEvent, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles) + if validationErrorMessage: + failedSavedOfferingIndicies.append(i) + print(f"Failed saving multi event {i}:", validationErrorMessage) + else: + savedOfferings.append(savedEvent) + + succeeded = len(failedSavedOfferingIndicies) == 0 + # TODO: We need to rollback right here from all saved offerings if any failed so we can refresh the page and highlight the ones that had been created. + return succeeded, savedOfferings, failedSavedOfferingIndicies def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): @@ -105,7 +145,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 @@ -116,9 +156,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) @@ -129,7 +168,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): @@ -144,7 +183,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 @@ -437,7 +476,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. @@ -446,16 +485,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): """ @@ -479,11 +518,11 @@ 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 diff --git a/tests/code/test_events.py b/tests/code/test_events.py index ab8f4e2bb..e342976af 100644 --- a/tests/code/test_events.py +++ b/tests/code/test_events.py @@ -22,7 +22,7 @@ from app.models.eventRsvp import EventRsvp from app.models.note import Note -from app.logic.events import preprocessEventData, validateNewEventData, calculateRecurringEventFrequency +from app.logic.events import preprocessEventData, validateNewEventData, getRecurringEventsData from app.logic.events import attemptSaveEvent, saveEventToDb, cancelEvent, deleteEvent, getParticipatedEventsForUser from app.logic.events import calculateNewrecurringId, getPreviousRecurringEventData, getUpcomingEventsForUser, calculateNewMultipleOfferingId, getPreviousMultipleOfferingEventData from app.logic.events import deleteEventAndAllFollowing, deleteAllRecurringEvents, getEventRsvpCountsForTerm, getEventRsvpCount, getCountdownToEvent, copyRsvpToNewEvent @@ -326,7 +326,7 @@ def test_calculateRecurringEventFrequency(): 'endDate': parser.parse("03/9/2023")} # test correct response - returnedEvents = calculateRecurringEventFrequency(eventInfo) + returnedEvents = getRecurringEventsData(eventInfo) assert returnedEvents[0] == {'name': 'testEvent Week 1', 'date': parser.parse('02/22/2023'), 'week': 1} assert returnedEvents[1] == {'name': 'testEvent Week 2', 'date': parser.parse('03/01/2023'), 'week': 2} assert returnedEvents[2] == {'name': 'testEvent Week 3', 'date': parser.parse('03/08/2023'), 'week': 3} @@ -334,13 +334,13 @@ def test_calculateRecurringEventFrequency(): # test non-datetime eventInfo["startDate"] = '2021/06/07' with pytest.raises(Exception): - returnedEvents = calculateRecurringEventFrequency(eventInfo) + returnedEvents = getRecurringEventsData(eventInfo) # test non-recurring eventInfo["startDate"] = '2021/06/07' eventInfo["endDate"] = '2021/06/07' with pytest.raises(Exception): - returnedEvents = calculateRecurringEventFrequency(eventInfo) + returnedEvents = getRecurringEventsData(eventInfo) @pytest.mark.integration def test_attemptSaveEvent(): @@ -358,8 +358,8 @@ def test_attemptSaveEvent(): with mainDB.atomic() as transaction: with app.app_context(): g.current_user = User.get_by_id("ramsayb2") - success, errorMessage = attemptSaveEvent(eventInfo) - if not success: + savedEvents, errorMessage = attemptSaveEvent(eventInfo) + if not savedEvents: pytest.fail(f"Save failed: {errorMessage}") try: From c891d060f2eace1b10808a88fce6b06555457b7f Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Fri, 6 Sep 2024 18:31:24 -0400 Subject: [PATCH 23/38] Added docstrings and completed the todos for changing how we handle transactions when one multiple offering fails and how we use the restructured outputs from the attemptSaveMultipleOfferings function --- app/controllers/admin/routes.py | 12 ++++---- app/logic/events.py | 50 ++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 39d354e05..fc067407e 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -117,6 +117,7 @@ def createEvent(templateid, programid): if eventData.get('isMultipleOffering'): succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) if not succeeded: + validationErrorMessage = sorted(failedSavedOfferings.items(), key=lambda e: e[0])[-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: @@ -139,15 +140,12 @@ def createEvent(templateid, programid): if len(savedEvents) > 1 and eventData.get('isRecurring'): createActivityLog(f"Created a recurring event, {savedEvents[0].name}, 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')}.") - # TODO: Change the way that this logic works since we're no longer using a nested savedEventsList and are using savedEvents instead like the lines above - 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'): + event_dates = [event_data.startDate.strftime('%m/%d/%Y') for event_data in savedEvents] - event_list = ', '.join(f"{event.name}" for event in modifiedSavedEvents) + event_list = ', '.join(f"{event.name}" 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] #get last date and stick at the end after 'and' so that it reads like a sentence in admin log diff --git a/app/logic/events.py b/app/logic/events.py index 5f24f8fc2..a90a67169 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -105,36 +105,42 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): If any data is not valid it will return a validation error. Returns: - (Whether all saves were successful, the list of saved offering objects, a list of the indecies of all failed offerings) + allSavesSucceeded : bool | Whether or not all offering saves were successful + savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesSucceeded is False then this list will be empty. + failedSavedOfferings : dict | Maps the indicies of failed saved offerings to the associated validation error message. """ savedOfferings = [] - failedSavedOfferingIndicies = [] + failedSavedOfferings = {} + allSavesSucceeded = True # Creates a shared multipleOfferingId for all offerings to have multipleOfferingId = calculateNewMultipleOfferingId() # Create separate event data inheriting from the original eventData multipleOfferingData = json.loads(eventData.get('multipleOfferingData')) - for i, 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 - savedEvent, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles) - if validationErrorMessage: - failedSavedOfferingIndicies.append(i) - print(f"Failed saving multi event {i}:", validationErrorMessage) - else: - savedOfferings.append(savedEvent) - - succeeded = len(failedSavedOfferingIndicies) == 0 - # TODO: We need to rollback right here from all saved offerings if any failed so we can refresh the page and highlight the ones that had been created. - return succeeded, savedOfferings, failedSavedOfferingIndicies + with mainDB.atomic() as transaction: + for i, 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[i] = validationErrorMessage + allSavesSucceeded = False + print(f"Failed saving multi event {i}:", validationErrorMessage) + else: + savedEvent = savedEvents[0] + savedOfferings.append(savedEvent) + if not allSavesSucceeded: + savedOfferings = [] + transaction.rollback() + return allSavesSucceeded, savedOfferings, failedSavedOfferings def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): From 318feb00509e1c5d272b56332fec1d162f015b8b Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Mon, 9 Sep 2024 13:45:27 -0400 Subject: [PATCH 24/38] Renamed some variables and changed a data structure for clarity --- app/controllers/admin/routes.py | 5 +++-- app/logic/events.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index fc067407e..77e0e594f 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -113,17 +113,18 @@ def createEvent(templateid, programid): # Try to save the form if request.method == "POST": + savedEvents = None eventData.update(request.form.copy()) if eventData.get('isMultipleOffering'): succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) if not succeeded: - validationErrorMessage = sorted(failedSavedOfferings.items(), key=lambda e: e[0])[-1][1] # The last validation error message from the list of offerings if there are multiple + 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[]") diff --git a/app/logic/events.py b/app/logic/events.py index a90a67169..0f5549460 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -105,13 +105,13 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): If any data is not valid it will return a validation error. Returns: - allSavesSucceeded : bool | Whether or not all offering saves were successful - savedOfferings : List[event] | A list of event objects holding all offerings that were saved. If allSavesSucceeded is False then this list will be empty. - failedSavedOfferings : dict | Maps the indicies of failed saved offerings to the associated validation error message. + 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 = {} - allSavesSucceeded = True + failedSavedOfferings = [] + allSavesWereSuccessful = True # Creates a shared multipleOfferingId for all offerings to have multipleOfferingId = calculateNewMultipleOfferingId() @@ -131,16 +131,16 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): # Try to save each offering savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles) if validationErrorMessage: - failedSavedOfferings[i] = validationErrorMessage - allSavesSucceeded = False + failedSavedOfferings.append((i, validationErrorMessage)) + allSavesWereSuccessful = False print(f"Failed saving multi event {i}:", validationErrorMessage) else: savedEvent = savedEvents[0] savedOfferings.append(savedEvent) - if not allSavesSucceeded: + if not allSavesWereSuccessful: savedOfferings = [] transaction.rollback() - return allSavesSucceeded, savedOfferings, failedSavedOfferings + return allSavesWereSuccessful, savedOfferings, failedSavedOfferings def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): From e03b5219e0b3d89635a45fa04fef498277937ae9 Mon Sep 17 00:00:00 2001 From: Stevenson Date: Mon, 9 Sep 2024 15:55:51 -0400 Subject: [PATCH 25/38] Modified preexisting functions --- app/controllers/admin/routes.py | 3 ++ app/logic/events.py | 3 +- app/static/js/createEvents.js | 63 ++++++++++++++++++---------- app/templates/admin/createEvent.html | 2 +- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 77e0e594f..520751746 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -116,8 +116,11 @@ def createEvent(templateid, programid): savedEvents = None eventData.update(request.form.copy()) if eventData.get('isMultipleOffering'): + eventData['multipleOfferingData'] = json.loads(eventData['multipleOfferingData']) succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) if not succeeded: + for i, validationErrorMessage in failedSavedOfferings: + eventData['multipleOfferingData'][i]['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: diff --git a/app/logic/events.py b/app/logic/events.py index 0f5549460..b8bf014df 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -117,7 +117,7 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): multipleOfferingId = calculateNewMultipleOfferingId() # Create separate event data inheriting from the original eventData - multipleOfferingData = json.loads(eventData.get('multipleOfferingData')) + multipleOfferingData = eventData.get('multipleOfferingData') with mainDB.atomic() as transaction: for i, event in enumerate(multipleOfferingData): multipleOfferingDict = eventData.copy() @@ -140,6 +140,7 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): if not allSavesWereSuccessful: savedOfferings = [] transaction.rollback() + return allSavesWereSuccessful, savedOfferings, failedSavedOfferings diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index eb76c21d8..4463f1eec 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -148,7 +148,8 @@ function calculateRecurringEventFrequency(){ }); } else { - storeMultipleOfferingEventAttributes(); + saveOfferingsFromModal(); + updateOfferingsTable(); pendingmultipleEvents = []; $("#checkIsMultipleOffering").prop('checked', true); // Remove the modal and overlay from the DOM @@ -158,33 +159,49 @@ function calculateRecurringEventFrequency(){ msgFlash("Multiple time offering events saved!", "success"); } }); -//build multi-event table -function storeMultipleOfferingEventAttributes() { - let entries = []; - $(".extraSlots").children().each(function(index, element) { - let rowData = $.map($(element).find("input"), (el) => $(el).val()); - - entries.push({ - eventName: rowData[0], - eventDate: rowData[1], - startTime: rowData[2], - endTime: rowData[3] - }); - }); - let entriesJson = JSON.stringify(entries); - $("#multipleOfferingDataId").val(entriesJson); + +// Save the offerings from the modal to the hidden input field +function saveOfferingsFromModal() { + let offerings = []; + $(".extraSlots").children().each(function(index, element) { + let rowData = $.map($(element).find("input"), (el) => $(el).val()); + + offerings.push({ + eventName: rowData[0], + eventDate: rowData[1], + startTime: rowData[2], + endTime: rowData[3] + }); + }); + + let offeringsJson = JSON.stringify(offerings); + $("#multipleOfferingDataId").val(offeringsJson); +} + +// Update the table of offerings with the offerings from the hidden input field +function updateOfferingsTable() { + console.log("multiple offering data id is", $("#multipleOfferingDataId").val()) + let offerings = JSON.parse($("#multipleOfferingDataId").val()) var multipleOfferingTable = $("#multipleOfferingEventsTable"); multipleOfferingTable.find("tbody tr").remove(); // Clear existing rows - entries.forEach(function(entry){ + offerings.forEach(function(offering){ //fromat to 12hr time for display - var formattedEventDate = formatDate(entry.eventDate); - var startTime = format24to12HourTime(entry.startTime); - var endTime = format24to12HourTime(entry.endTime); - multipleOfferingTable.append("" + entry.eventName + "" + formattedEventDate +"" + startTime + "" + endTime + ""); + var formattedEventDate = formatDate(offering.eventDate); + var startTime = format24to12HourTime(offering.startTime); + var endTime = format24to12HourTime(offering.endTime); + multipleOfferingTable.append(`` + + "" + offering.eventName + "" + + "" + formattedEventDate + "" + + "" + startTime + "" + + "" + endTime + "" + + "" + ); }); -} +} + + //visual date formatting for multi-event table function formatDate(originalDate) { var dateObj = new Date(originalDate); @@ -261,6 +278,8 @@ $(".startDatePicker, .endDatePicker").change(function () { event.preventDefault(); } }); + + updateOfferingsTable(); let modalOpenedByEditButton = false; diff --git a/app/templates/admin/createEvent.html b/app/templates/admin/createEvent.html index 823c08f2c..14d2f4ec7 100644 --- a/app/templates/admin/createEvent.html +++ b/app/templates/admin/createEvent.html @@ -222,7 +222,7 @@

{{page_title}}

- + {% if 'program' in eventData and eventData['program'].isBonnerScholars %}
From 4f981fd86a432fbc4b687f41292aba6f081498ad Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Mon, 9 Sep 2024 17:32:12 -0400 Subject: [PATCH 26/38] Added preprocessing for multipleOfferingData --- app/logic/events.py | 23 +++++++++++++++++++++++ app/templates/admin/createEvent.html | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/logic/events.py b/app/logic/events.py index b8bf014df..5b7f0839a 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -511,6 +511,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 @@ -536,6 +537,28 @@ def preprocessEventData(eventData): 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']) + 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[attribute] = False + + eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData']) + # Process terms if 'term' in eventData: try: diff --git a/app/templates/admin/createEvent.html b/app/templates/admin/createEvent.html index 14d2f4ec7..31af77d69 100644 --- a/app/templates/admin/createEvent.html +++ b/app/templates/admin/createEvent.html @@ -173,7 +173,7 @@

{{page_title}}

+ {{"checked" if eventData.isMultipleOffering}} />
{% endif %} @@ -222,7 +222,7 @@

{{page_title}}

- + {% if 'program' in eventData and eventData['program'].isBonnerScholars %}
From fb015367da23cc21483f8e43e51ad82057208157 Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Wed, 11 Sep 2024 17:07:59 -0400 Subject: [PATCH 27/38] Renamed a class to be more specific --- app/static/css/createEvent.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/css/createEvent.css b/app/static/css/createEvent.css index 638498de2..76cf85c95 100644 --- a/app/static/css/createEvent.css +++ b/app/static/css/createEvent.css @@ -17,7 +17,7 @@ margin-right: auto; } -.deleteRow{ +.deleteOfferingBtn{ border-radius: 5px; position:relative; top: 12px; From 08853cb953c06b2ae5850a8c3b139fb1e9a2609f Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Wed, 11 Sep 2024 17:12:03 -0400 Subject: [PATCH 28/38] Changed the multiple offerings modal so it loads from the data table correctly and refactored. --- app/static/js/createEvents.js | 323 ++++++++++++++------------- app/templates/admin/createEvent.html | 122 +++++----- 2 files changed, 233 insertions(+), 212 deletions(-) diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index 4463f1eec..dfc00a070 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -36,135 +36,173 @@ function format24to12HourTime(timeStr) { } function calculateRecurringEventFrequency(){ - var eventDatesAndName = {name:$("#inputEventName").val(), - isRecurring: true, - startDate:$(".startDatePicker")[0].value, - endDate:$(".endDatePicker")[0].value} - $.ajax({ - type:"POST", - url: "/makeRecurringEvents", - //get the startDate, endDate and name as a dictionary - data: eventDatesAndName, - success: function(jsonData){ - var recurringEvents = JSON.parse(jsonData) - var recurringTable = $("#recurringEventsTable") - $("#recurringEventsTable tbody tr").remove(); - for(var event of recurringEvents){ - var eventdate = new Date(event.date).toLocaleDateString() - recurringTable.append(""+event.name+""+eventdate+""); - } - }, - error: function(error){ - console.log(error) + var eventDatesAndName = {name:$("#inputEventName").val(), + isRecurring: true, + startDate:$(".startDatePicker")[0].value, + endDate:$(".endDatePicker")[0].value} + $.ajax({ + type:"POST", + url: "/makeRecurringEvents", + //get the startDate, endDate and name as a dictionary + data: eventDatesAndName, + success: function(jsonData){ + var recurringEvents = JSON.parse(jsonData) + var recurringTable = $("#recurringEventsTable") + $("#recurringEventsTable tbody tr").remove(); + for(var event of recurringEvents){ + var eventdate = new Date(event.date).toLocaleDateString() + recurringTable.append(""+event.name+""+eventdate+""); } - }); - } - $('#multipleOfferingSave').on('click', function() { - //Requires that modal info updated before it can be saved, gives notifier if there are empty fields - let eventOfferings = $('.eventOffering'); - let eventNameInputs = $('.multipleOfferingNameField'); - let datePickerInputs = $('.multipleOfferingDatePicker'); - let startTimeInputs = $('.multipleOfferingStartTime'); - let endTimeInputs = $('.multipleOfferingEndTime'); - let isEmpty = false; - let hasValidTimes = true; - let hasDuplicateListings = false; - - // Check if the input field is empty - eventNameInputs.each((index, eventNameInput) => { - if (eventNameInput.value.trim() === '') { - isEmpty = true; - $(eventNameInput).addClass('border-red'); - } else{ - $(eventNameInput).removeClass('border-red'); - } + }, + error: function(error){ + console.log(error) + } + }); +} + +function setViewForMultipleOffering(){ + $(".startDatePicker").prop('required', false); + $("#multipleOfferingTableDiv").removeClass('d-none'); + $("#checkIsRecurring").prop('checked', false); + $(".endDateStyle, #recurringTableDiv").addClass('d-none'); + //hides the non multiple offering time and dates and replace + $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').addClass('d-none'); +} + +function createOfferingModalRow({eventName=null, eventDate=null, startTime=null, endTime=null, isDuplicate=false}={}){ + // Parameters are in the format: {'test name', '2024-09-11', '12:00', '13:00'} + + let clonedMultipleOffering = $("#multipleOfferingEvent").clone().removeClass('d-none').removeAttr("id"); + // insert values for the newly created row + if (eventName) {clonedMultipleOffering.find('.multipleOfferingNameField').val(eventName)} + if (eventDate) {clonedMultipleOffering.find('.multipleOfferingDatePicker').val(eventDate)} + if (startTime) {clonedMultipleOffering.find('.multipleOfferingStartTime').val(startTime)} + if (endTime) {clonedMultipleOffering.find('.multipleOfferingEndTime').val(endTime)} + if (isDuplicate) {clonedMultipleOffering.addClass('border-red')} + + $("#multipleOfferingSlots").append(clonedMultipleOffering); + pendingmultipleEvents.push(clonedMultipleOffering); + + //this is so that the trash icon can be used to delete the event + clonedMultipleOffering.find(".deleteMultipleOffering").on("click", function() { + let attachedRow = $(this).closest(".eventOffering") + attachedRow.animate({ + opacity: 0, + height: '0px' + }, 500, function() { + // After the animation completes, remove the row + attachedRow.remove(); }); + }); + return clonedMultipleOffering +} - // Check if the date input field is empty - datePickerInputs.each((index, datePickerInput) => { - if (datePickerInput.value.trim() === '') { - isEmpty = true; - $(datePickerInput).addClass('border-red'); - } else { - $(datePickerInput).removeClass('border-red'); - } - }); - - // Check if the start time is after the end time - for(let i = 0; i < startTimeInputs.length; i++){ - if(startTimeInputs[i].value < endTimeInputs[i].value){ - $(startTimeInputs[i]).removeClass('border-red'); - $(endTimeInputs[i]).removeClass('border-red'); - } else { - $(startTimeInputs[i]).addClass('border-red'); - $(endTimeInputs[i]).addClass('border-red'); - hasValidTimes = false; - } +$('#multipleOfferingSave').on('click', function() { + //Requires that modal info updated before it can be saved, gives notifier if there are empty fields + let eventOfferings = $('#multipleOfferingSlots .eventOffering'); + let eventNameInputs = $('#multipleOfferingSlots .multipleOfferingNameField'); + let datePickerInputs = $('#multipleOfferingSlots .multipleOfferingDatePicker'); + let startTimeInputs = $('#multipleOfferingSlots .multipleOfferingStartTime'); + let endTimeInputs = $('#multipleOfferingSlots .multipleOfferingEndTime'); + let isEmpty = false; + let hasValidTimes = true; + let hasDuplicateListings = false; + + // Check if the input field is empty + eventNameInputs.each((index, eventNameInput) => { + if (eventNameInput.value.trim() === '') { + isEmpty = true; + $(eventNameInput).addClass('border-red'); + } else{ + $(eventNameInput).removeClass('border-red'); } + }); - // Check if there are duplicate event offerings - let eventListings = {}; - for(let i = 0; i < eventOfferings.length; i++){ - let eventName = eventNameInputs[i].value - let date = datePickerInputs[i].value.trim() - let startTime = startTimeInputs[i].value - let eventListing = JSON.stringify([eventName, date, startTime]) - - if (eventListing in eventListings){ // If we've seen this event before mark this event and the previous as duplicates - hasDuplicateListings = true - $(eventOfferings[i]).addClass('border-red'); - $(eventOfferings[eventListings[eventListing]]).addClass('border-red') - } else { // If we haven't seen this event before - $(eventOfferings[i]).removeClass('border-red'); - eventListings[eventListing] = i - } + // Check if the date input field is empty + datePickerInputs.each((index, datePickerInput) => { + if (datePickerInput.value.trim() === '') { + isEmpty = true; + $(datePickerInput).addClass('border-red'); + } else { + $(datePickerInput).removeClass('border-red'); } + }); - if (isEmpty){ - $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Event name or date field is empty"); - $('.invalidFeedback').css('display', 'block'); - $('.invalidFeedback').on('animationend', function() { - $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5') - }); - } - else if(!hasValidTimes){ - $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Event end time must be after start time"); - $('.invalidFeedback').css('display', 'block'); - $('.invalidFeedback').on('animationend', function() { - $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5') - }); + // Check if the start time is after the end time + for(let i = 0; i < startTimeInputs.length; i++){ + if(startTimeInputs[i].value < endTimeInputs[i].value){ + $(startTimeInputs[i]).removeClass('border-red'); + $(endTimeInputs[i]).removeClass('border-red'); + } else { + $(startTimeInputs[i]).addClass('border-red'); + $(endTimeInputs[i]).addClass('border-red'); + hasValidTimes = false; } - else if (hasDuplicateListings){ - $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Event listings cannot have the same event name, date, and start time"); - $('.invalidFeedback').css('display', 'block'); - $('.invalidFeedback').on('animationend', function() { - $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5') - }); + } + + // Check if there are duplicate event offerings + let eventListings = {}; + for(let i = 0; i < eventOfferings.length; i++){ + let eventName = eventNameInputs[i].value + let date = datePickerInputs[i].value.trim() + let startTime = startTimeInputs[i].value + let eventListing = JSON.stringify([eventName, date, startTime]) + + if (eventListing in eventListings){ // If we've seen this event before mark this event and the previous as duplicates + hasDuplicateListings = true + $(eventOfferings[i]).addClass('border-red'); + $(eventOfferings[eventListings[eventListing]]).addClass('border-red') + } else { // If we haven't seen this event before + $(eventOfferings[i]).removeClass('border-red'); + eventListings[eventListing] = i } - else { - saveOfferingsFromModal(); - updateOfferingsTable(); - pendingmultipleEvents = []; - $("#checkIsMultipleOffering").prop('checked', true); - // Remove the modal and overlay from the DOM - $('#modalMultipleOffering').modal('hide'); + } + + if (isEmpty){ + $('#textNotifierPadding').addClass('pt-5'); + $('.invalidFeedback').text("Event name or date field is empty"); + $('.invalidFeedback').css('display', 'block'); + $('.invalidFeedback').on('animationend', function() { $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5'); - msgFlash("Multiple time offering events saved!", "success"); - } - }); + $('#textNotifierPadding').removeClass('pt-5') + }); + } + else if(!hasValidTimes){ + $('#textNotifierPadding').addClass('pt-5'); + $('.invalidFeedback').text("Event end time must be after start time"); + $('.invalidFeedback').css('display', 'block'); + $('.invalidFeedback').on('animationend', function() { + $('.invalidFeedback').css('display', 'none'); + $('#textNotifierPadding').removeClass('pt-5') + }); + } + else if (hasDuplicateListings){ + $('#textNotifierPadding').addClass('pt-5'); + $('.invalidFeedback').text("Event listings cannot have the same event name, date, and start time"); + $('.invalidFeedback').css('display', 'block'); + $('.invalidFeedback').on('animationend', function() { + $('.invalidFeedback').css('display', 'none'); + $('#textNotifierPadding').removeClass('pt-5') + }); + } + else { + saveOfferingsFromModal(); + updateOfferingsTable(); + pendingmultipleEvents = []; + $("#checkIsMultipleOffering").prop('checked', true); + // Remove the modal and overlay from the DOM + $('#modalMultipleOffering').modal('hide'); + $('.invalidFeedback').css('display', 'none'); + $('#textNotifierPadding').removeClass('pt-5'); + msgFlash("Multiple time offering events saved!", "success"); + } +}); // Save the offerings from the modal to the hidden input field function saveOfferingsFromModal() { let offerings = []; - $(".extraSlots").children().each(function(index, element) { + $("#multipleOfferingSlots").children().each(function(index, element) { let rowData = $.map($(element).find("input"), (el) => $(el).val()); offerings.push({ @@ -174,15 +212,23 @@ function saveOfferingsFromModal() { endTime: rowData[3] }); }); - + $('#multipleOfferingSlots').children().remove(); let offeringsJson = JSON.stringify(offerings); - $("#multipleOfferingDataId").val(offeringsJson); + $("#multipleOfferingData").val(offeringsJson); +} + +function loadOfferingsToModal(){ + let offerings = JSON.parse($("#multipleOfferingData").val()) + offerings.forEach((offering, i) =>{ + let newOfferingModalRow = createOfferingModalRow(offering) + //stripes odd event sections in event modal + newOfferingModalRow.css('background-color', i % 2 ?'#f2f2f2':'#fff'); + }) } // Update the table of offerings with the offerings from the hidden input field function updateOfferingsTable() { - console.log("multiple offering data id is", $("#multipleOfferingDataId").val()) - let offerings = JSON.parse($("#multipleOfferingDataId").val()) + let offerings = JSON.parse($("#multipleOfferingData").val()) var multipleOfferingTable = $("#multipleOfferingEventsTable"); multipleOfferingTable.find("tbody tr").remove(); // Clear existing rows @@ -281,8 +327,11 @@ $(".startDatePicker, .endDatePicker").change(function () { updateOfferingsTable(); - let modalOpenedByEditButton = false; + if ($("#checkIsMultipleOffering").is(":checked")){ + setViewForMultipleOffering(); + } + let modalOpenedByEditButton = false; //#checkIsRecurring, #checkIsMultipleOffering are attributes for the toggle buttons on create event page $("#checkIsRecurring, #checkIsMultipleOffering, #edit_modal").click(function(event) { if(!($('#inputEventName').val().trim() == '')){ @@ -306,15 +355,10 @@ $(".startDatePicker, .endDatePicker").change(function () { $('#multipleOfferingTableDiv').addClass('d-none'); $(".endDatePicker").prop('required', true); } - else if (multipleOfferingStatus == true) { - $(".startDatePicker").prop('required', false); - $("#multipleOfferingTableDiv").removeClass('d-none'); - $("#checkIsRecurring").prop('checked', false); - $(".endDateStyle, #recurringTableDiv").addClass('d-none'); + setViewForMultipleOffering(); + loadOfferingsToModal(); $('#modalMultipleOffering').modal('show'); - //hides the non multiple offering time and dates and replace - $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').addClass('d-none'); } else { //adds the display none button of bootstrap so that the end-date div disappears for recurring even @@ -335,7 +379,7 @@ $(".startDatePicker, .endDatePicker").change(function () { $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').removeClass('d-none'); $("#multipleOfferingTableDiv").addClass('d-none'); $('#modalMultipleOffering').modal('hide'); - $('.extraSlots').children().not(':first').remove(); + $('#multipleOfferingSlots').children().remove(); } pendingmultipleEvents.forEach(function(element){ element.remove(); @@ -344,29 +388,8 @@ $(".startDatePicker, .endDatePicker").change(function () { }); /*cloning the div with ID multipleOfferingEvent and cloning, changing the ID of each clone going up by 1. This also changes - the ID of the deleteMultipleOfferingEvent so that when the trash icon is clicked, that specific row will be deleted*/ - let counterAdd = 0 // counter to add customized ids into the newly created slots - $(".addMultipleOfferingEvent").click(function(){ - let clonedMultipleOffering = $("#multipleOfferingEvent").clone(); - let newMultipleObject = clonedMultipleOffering.attr("id", "multipleOfferingEvent" + counterAdd) - clonedMultipleOffering.find("#deleteMultipleOfferingEvent").attr("id", "deleteMultipleOfferingEvent" + counterAdd).removeClass('d-none'); - $(".extraSlots").append(clonedMultipleOffering); - pendingmultipleEvents.push(newMultipleObject); - //stripes event sections in event modal - if(counterAdd % 2 == 0){ - newMultipleObject.css('background-color', '#f2f2f2'); - } - else{ - newMultipleObject.css('background-color', '#fff'); - } - counterAdd += 1 - //this is so that the trash icon can be used to delete the event - clonedMultipleOffering.on("click", "[id^=deleteMultipleOfferingEvent]", function() { - // Extract the numeric part from the id - var id = $(this).attr('id').match(/\d+/)[0]; - $("#multipleOfferingEvent" + id).remove(); - }); - }); + the ID of the deleteMultipleOffering so that when the trash icon is clicked, that specific row will be deleted*/ + $(".addMultipleOfferingEvent").click(createOfferingModalRow) $("#allowPastStart").click(function() { var minDate = $("#allowPastStart:checked").val() ? new Date('10/25/1999') : new Date() diff --git a/app/templates/admin/createEvent.html b/app/templates/admin/createEvent.html index 31af77d69..8af80c4be 100644 --- a/app/templates/admin/createEvent.html +++ b/app/templates/admin/createEvent.html @@ -182,15 +182,11 @@

{{page_title}}

{% endif %} {% if eventData.isRecurring == True and isNewEvent %} - {% set hideDate = "" %} - {% set hideMultipleOffering = "d-none" %} - {% elif eventData.isMultipleOffering == True and isNewEvent %} - {% set hideDate = "d-none" %} - {% set hideMultipleOffering = "" %} - + {% set hideDate = "" %} + {% set hideMultipleOffering = "d-none" %} {% else %} - {% set hideDate = "d-none" %} - {% set hideMultipleOffering = "d-none" %} + {% set hideDate = "d-none" %} + {% set hideMultipleOffering = "d-none" %} {% endif %} {{locationTimeMacro(eventData, hideDate, 'main')}} @@ -222,7 +218,7 @@

{{page_title}}

- + {% if 'program' in eventData and eventData['program'].isBonnerScholars %}
@@ -466,64 +462,66 @@

-
-
-
-
- - -
- -
- -
- -
-
-
- -
- {% if eventData.timeStart %} - {% set startTime = eventData.timeStart %} - {% else %} - {% set startTime = "12:00" %} - {% endif %} - - -
+ + +
+
+
+ + +
+ +
+ +
+
-
- -
- {% if eventData.timeEnd %} - {% set endTime = eventData.timeEnd %} - {%else%} - {% set endTime = "13:00" %} - {% endif %} - - -
+
+
+ +
+ {% if eventData.timeStart %} + {% set startTime = eventData.timeStart %} + {% else %} + {% set startTime = "12:00" %} + {% endif %} + +
-
-
- +
+
+ +
+ {% if eventData.timeEnd %} + {% set endTime = eventData.timeEnd %} + {%else%} + {% set endTime = "13:00" %} + {% endif %} + +
+
+
+
+
+ +
From 1cfd8035c5e9d5df2b6a6fd231eaa2f33b619401 Mon Sep 17 00:00:00 2001 From: Oluwagbayi James Makinde <122561188+ojmakinde@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:49:11 -0400 Subject: [PATCH 29/38] Merged development --- .vscode/settings.json | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41bf..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file From 8ee02247b97192d6fe1e0e580b8dd098a7d8d300 Mon Sep 17 00:00:00 2001 From: Stevenson Date: Fri, 13 Sep 2024 13:51:33 -0400 Subject: [PATCH 30/38] Fixed syntax for indexing elements in list --- app/controllers/admin/routes.py | 4 ++-- app/logic/events.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 520751746..8d3f82385 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -119,8 +119,8 @@ def createEvent(templateid, programid): eventData['multipleOfferingData'] = json.loads(eventData['multipleOfferingData']) succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) if not succeeded: - for i, validationErrorMessage in failedSavedOfferings: - eventData['multipleOfferingData'][i]['isDuplicate'] = True + 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: diff --git a/app/logic/events.py b/app/logic/events.py index 5b7f0839a..ecc338515 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -119,7 +119,7 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): # Create separate event data inheriting from the original eventData multipleOfferingData = eventData.get('multipleOfferingData') with mainDB.atomic() as transaction: - for i, event in enumerate(multipleOfferingData): + for index, event in enumerate(multipleOfferingData): multipleOfferingDict = eventData.copy() multipleOfferingDict.update({ 'name': event['eventName'], @@ -131,9 +131,9 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): # Try to save each offering savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, attachmentFiles) if validationErrorMessage: - failedSavedOfferings.append((i, validationErrorMessage)) + failedSavedOfferings.append((index, validationErrorMessage)) allSavesWereSuccessful = False - print(f"Failed saving multi event {i}:", validationErrorMessage) + print(f"Failed saving multi event {index}:", validationErrorMessage) else: savedEvent = savedEvents[0] savedOfferings.append(savedEvent) From 19c992478ed43e6e770c02074ea37ab43ad38b29 Mon Sep 17 00:00:00 2001 From: Stevenson Date: Fri, 13 Sep 2024 14:01:27 -0400 Subject: [PATCH 31/38] Fixed camel case issue --- app/controllers/admin/routes.py | 12 ++++++------ app/logic/events.py | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 8d3f82385..23c5fe43c 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -145,18 +145,18 @@ def createEvent(templateid, programid): createActivityLog(f"Created a recurring event, {savedEvents[0].name}, 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(savedEvents) >= 1 and eventData.get('isMultipleOffering'): - event_dates = [event_data.startDate.strftime('%m/%d/%Y') for event_data in savedEvents] + eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents] - event_list = ', '.join(f"{event.name}" for event in savedEvents) + eventList = ', '.join(f"{event.name}" for event in savedEvents) 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 {savedEvents[0].name} for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.") diff --git a/app/logic/events.py b/app/logic/events.py index ecc338515..9ee4503f4 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -357,25 +357,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): """ From ce057646378d2ee9adc58a989918654ea85d5e15 Mon Sep 17 00:00:00 2001 From: Stevenson Date: Fri, 13 Sep 2024 14:03:11 -0400 Subject: [PATCH 32/38] Changed comment wording --- app/logic/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/logic/events.py b/app/logic/events.py index 9ee4503f4..ab85fd630 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -152,7 +152,7 @@ def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): If it is not valid it will return a validation error. Returns: - The saved event Created events and an error message if an error occurred. + 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 From 716b5506af21ec4b7ed2267bb093f4fb1c81155b Mon Sep 17 00:00:00 2001 From: Stevenson Date: Fri, 13 Sep 2024 15:19:00 -0400 Subject: [PATCH 33/38] Created function for code reusability in notification for invalidation --- app/static/js/createEvents.js | 37 +++++++++++++++-------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index dfc00a070..9e6e42296 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -69,6 +69,16 @@ function setViewForMultipleOffering(){ $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').addClass('d-none'); } +function displayNotification(message) { + $('#textNotifierPadding').addClass('pt-5'); + $('.invalidFeedback').text(message); + $('.invalidFeedback').css('display', 'block'); + $('.invalidFeedback').on('animationend', function() { + $('.invalidFeedback').css('display', 'none'); + $('#textNotifierPadding').removeClass('pt-5') + }); +} + function createOfferingModalRow({eventName=null, eventDate=null, startTime=null, endTime=null, isDuplicate=false}={}){ // Parameters are in the format: {'test name', '2024-09-11', '12:00', '13:00'} @@ -159,31 +169,16 @@ $('#multipleOfferingSave').on('click', function() { } if (isEmpty){ - $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Event name or date field is empty"); - $('.invalidFeedback').css('display', 'block'); - $('.invalidFeedback').on('animationend', function() { - $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5') - }); + let emptyFieldMessage = "Event name or date field is empty"; + displayNotification(emptyFieldMessage); } else if(!hasValidTimes){ - $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Event end time must be after start time"); - $('.invalidFeedback').css('display', 'block'); - $('.invalidFeedback').on('animationend', function() { - $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5') - }); + let invalidTimeMessage = "Event end time must be after start time"; + displayNotification(invalidTimeMessage); } else if (hasDuplicateListings){ - $('#textNotifierPadding').addClass('pt-5'); - $('.invalidFeedback').text("Event listings cannot have the same event name, date, and start time"); - $('.invalidFeedback').css('display', 'block'); - $('.invalidFeedback').on('animationend', function() { - $('.invalidFeedback').css('display', 'none'); - $('#textNotifierPadding').removeClass('pt-5') - }); + let eventConflictMessage = "Event listings cannot have the same event name, date, and start time"; + displayNotification(eventConflictMessage); } else { saveOfferingsFromModal(); From bca5c8c54d478dfbee3b8fb82c1697fdfd8e6ab8 Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Mon, 16 Sep 2024 11:46:51 -0400 Subject: [PATCH 34/38] Fixed a bug in development where it was possible to save a single offering event without selecting a date --- app/static/js/createEvents.js | 17 ++++++++++------- app/templates/events/createEvent.html | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index 9e6e42296..1a9012293 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -60,13 +60,18 @@ function calculateRecurringEventFrequency(){ }); } +function setViewForSingleOffering(){ + $(".startDatePicker").prop('required', true); + $("#multipleOfferingTableDiv").addClass('d-none'); + $(".endDateStyle, #recurringTableDiv").addClass('d-none'); + $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').removeClass('d-none'); +} + function setViewForMultipleOffering(){ $(".startDatePicker").prop('required', false); $("#multipleOfferingTableDiv").removeClass('d-none'); - $("#checkIsRecurring").prop('checked', false); $(".endDateStyle, #recurringTableDiv").addClass('d-none'); - //hides the non multiple offering time and dates and replace - $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').addClass('d-none'); + $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').addClass('d-none'); } function displayNotification(message) { @@ -370,11 +375,9 @@ $(".startDatePicker, .endDatePicker").change(function () { //untoggles the button when the modal cancel or close button is clicked $("#cancelModalPreview, #multipleOfferingXbutton").click(function(){ if (modalOpenedByEditButton == false) { - $("#checkIsMultipleOffering").prop('checked', false); - $('#nonMultipleOfferingTime, #nonMultipleOfferingDate').removeClass('d-none'); - $("#multipleOfferingTableDiv").addClass('d-none'); $('#modalMultipleOffering').modal('hide'); - $('#multipleOfferingSlots').children().remove(); + $("#checkIsMultipleOffering").prop('checked', false); + setViewForSingleOffering() } pendingmultipleEvents.forEach(function(element){ element.remove(); diff --git a/app/templates/events/createEvent.html b/app/templates/events/createEvent.html index caea91b6c..606701688 100644 --- a/app/templates/events/createEvent.html +++ b/app/templates/events/createEvent.html @@ -71,7 +71,7 @@

{{page_title}}

+ data-page-location="{{pageLocation}}" required>
From 93c52d1f258e3c2ed4c90c3151549fb0256a221f Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Mon, 16 Sep 2024 11:48:39 -0400 Subject: [PATCH 35/38] Fixed a bug where dismissing an alert would dismiss all alerts --- app/static/js/base.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/static/js/base.js b/app/static/js/base.js index 51ba133bf..5aa7c39af 100644 --- a/app/static/js/base.js +++ b/app/static/js/base.js @@ -11,10 +11,10 @@ function msgFlash(flash_message, status){ $("#flash_container").prepend(` `); - $("#flashResponded").click(function(){ - $(".alert").delay(1000).fadeOut(); + $(".close-alert").click(function(){ + $(this).closest(".alert").delay(1000).fadeOut(); }) } From 6181c4bee5dd09aa235bba6398c57af6fe4a1171 Mon Sep 17 00:00:00 2001 From: Stevenson Date: Mon, 16 Sep 2024 15:40:49 -0400 Subject: [PATCH 36/38] Created tests for attemptSaveMultipleOfferings function --- app/controllers/admin/routes.py | 2 +- app/logic/events.py | 2 +- tests/code/test_events.py | 78 ++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 634c291c5..df8b4f059 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -98,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": @@ -115,6 +114,7 @@ def createEvent(templateid, programid): if request.method == "POST": savedEvents = None eventData.update(request.form.copy()) + eventData = preprocessEventData(eventData) if eventData.get('isMultipleOffering'): eventData['multipleOfferingData'] = json.loads(eventData['multipleOfferingData']) succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) diff --git a/app/logic/events.py b/app/logic/events.py index ab85fd630..26686ed9c 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -133,7 +133,6 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): if validationErrorMessage: failedSavedOfferings.append((index, validationErrorMessage)) allSavesWereSuccessful = False - print(f"Failed saving multi event {index}:", validationErrorMessage) else: savedEvent = savedEvents[0] savedOfferings.append(savedEvent) @@ -543,6 +542,7 @@ def preprocessEventData(eventData): 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: diff --git a/tests/code/test_events.py b/tests/code/test_events.py index e342976af..16a24e812 100644 --- a/tests/code/test_events.py +++ b/tests/code/test_events.py @@ -23,7 +23,7 @@ from app.models.note import Note from app.logic.events import preprocessEventData, validateNewEventData, getRecurringEventsData -from app.logic.events import attemptSaveEvent, saveEventToDb, cancelEvent, deleteEvent, getParticipatedEventsForUser +from app.logic.events import attemptSaveEvent, attemptSaveMultipleOfferings, saveEventToDb, cancelEvent, deleteEvent, getParticipatedEventsForUser from app.logic.events import calculateNewrecurringId, getPreviousRecurringEventData, getUpcomingEventsForUser, calculateNewMultipleOfferingId, getPreviousMultipleOfferingEventData from app.logic.events import deleteEventAndAllFollowing, deleteAllRecurringEvents, getEventRsvpCountsForTerm, getEventRsvpCount, getCountdownToEvent, copyRsvpToNewEvent from app.logic.volunteers import updateEventParticipants @@ -371,6 +371,82 @@ def test_attemptSaveEvent(): finally: transaction.rollback() # undo our database changes + +@pytest.mark.integration +def test_attemptSaveMultipleOfferings(): + baseEventData = { + 'isTraining':'on', 'isRecurring':False, 'recurringId':None, 'isMultipleOffering':False, 'multipleOffeirngId':None, + 'startDate': '2021-12-12', + 'rsvpLimit': None, + 'endDate':'2022-06-12', 'location':"a big room", + 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', + 'description':"Empty Bowls Spring 2021", + 'name':'Attempt Save Test','term':1,'contactName':"Garrett D. Clark", + 'contactEmail': 'boorclark@gmail.com' + } + + baseEventData['program'] = Program.get_by_id(1) + + validMultipleOfferingData = baseEventData.copy() + validMultipleOfferingData['multipleOfferingData'] = [{ + 'eventName': 'Offering 1', + 'eventDate': '2022-06-12', + 'startTime': '09:00 PM', + 'endTime': '10:00 PM', + }, + { + 'eventName': 'Offering 2', + 'eventDate': '2022-06-13', + 'startTime': '09:00 PM', + 'endTime': '10:00 PM', + }, + { + 'eventName': 'Offering 3', + 'eventDate': '2022-06-16', + 'startTime': '09:00 PM', + 'endTime': '10:00 PM', + }] + + duplicatedMultipleOfferingData = baseEventData.copy() + duplicatedMultipleOfferingData['multipleOfferingData'] = [{ + 'eventName': 'Offering 1', + 'eventDate': '2022-06-12', + 'startTime': '09:00 PM', + 'endTime': '10:00 PM', + }, + { + 'eventName': 'Offering 1', + 'eventDate': '2022-06-12', + 'startTime': '09:00 PM', + 'endTime': '10:00 PM', + }, + { + 'eventName': 'Offering 3', + 'eventDate': '2022-06-16', + 'startTime': '09:00 PM', + 'endTime': '10:00 PM', + }] + + + + with mainDB.atomic() as transaction: + # test valid data + succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(validMultipleOfferingData, None) + assert succeeded == True + assert len(savedEvents) == 3 + assert len(failedSavedOfferings) == 0 + + transaction.rollback() + + # test duplicated data + succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(duplicatedMultipleOfferingData, None) + assert succeeded == False + assert len(savedEvents) == 0 + assert len(failedSavedOfferings) == 1 + + transaction.rollback() + + @pytest.mark.integration def test_saveEventToDb_create(): From fe8972cdc37a5c382d6908cc0af54d614f62b6d4 Mon Sep 17 00:00:00 2001 From: Lawrence Hoerst Date: Mon, 16 Sep 2024 17:01:52 -0400 Subject: [PATCH 37/38] Added tests for changes made to preprocessEventData for multipleOfferingData --- app/logic/events.py | 2 +- tests/code/test_events.py | 62 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/logic/events.py b/app/logic/events.py index 26686ed9c..1d318a019 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -555,7 +555,7 @@ def preprocessEventData(eventData): if type(offeringDatum.get(attribute)) != str: offeringDatum[attribute] = '' if type(offeringDatum.get('isDuplicate')) != bool: - offeringDatum[attribute] = False + offeringDatum['isDuplicate'] = False eventData['multipleOfferingData'] = json.dumps(eventData['multipleOfferingData']) diff --git a/tests/code/test_events.py b/tests/code/test_events.py index 16a24e812..8bff7287d 100644 --- a/tests/code/test_events.py +++ b/tests/code/test_events.py @@ -1,4 +1,5 @@ import pytest +import json from flask import g, session from app import app from peewee import DoesNotExist, OperationalError, IntegrityError, fn @@ -250,6 +251,67 @@ def test_preprocessEventData_requirement(): transaction.rollback() +@pytest.mark.integration +def test_preprocessEventData_multipleOfferingData(): + # When there is no multipleOfferingData we should get a jsonified empty list + eventData = preprocessEventData({}) + assert eventData['multipleOfferingData'] == json.dumps([]) + + # Test parsing from list + offeringData = [ + { + 'eventName': 'Offering 1', + 'startDate': 'Today', + 'timeStart': '01:00 PM', + 'timeEnd': '02:00 PM', + 'isDuplicate': False + } + ] + eventData = preprocessEventData({'multipleOfferingData': offeringData}) + assert eventData['multipleOfferingData'] == json.dumps(offeringData) + + # Test when data is missing/invalid + offeringData = [ + {}, # Empty event offering should be filled with defaults + { + 'eventName': 'Offering 1', + 'startDate': 'Today', + 'timeStart': 1, # Wrong format and type + #'timeEnd': '02:00 PM' (No end time provided) + 'isDuplicate': True + } + ] + eventData = preprocessEventData({'multipleOfferingData': offeringData}) + offering = json.loads(eventData['multipleOfferingData'])[1] + assert offering['eventName'] == 'Offering 1' + assert offering['startDate'] == 'Today' + assert offering['timeStart'] == '' + assert offering['timeEnd'] == '' + assert offering['isDuplicate'] == True + defaultOffering = json.loads(eventData['multipleOfferingData'])[0] + assert defaultOffering['eventName'] == '' + assert defaultOffering['startDate'] == '' + assert defaultOffering['timeStart'] == '' + assert defaultOffering['timeEnd'] == '' + assert defaultOffering['isDuplicate'] == False + + + + # Test when data is already valid and stringified + offeringData = json.dumps([ + { + 'eventName': 'Offering 1', + 'startDate': 'Today', + 'timeStart': '01:00 PM', + 'timeEnd': '02:00 PM', + 'isDuplicate': False + } + ]) + eventData = preprocessEventData({'multipleOfferingData': offeringData}) + offering = json.loads(eventData['multipleOfferingData'])[0] + assert offering == json.loads(offeringData)[0] + + @pytest.mark.integration def test_correctValidateNewEventData(): From fb03136c997059d94a3d5ef061f834771ec8cb8e Mon Sep 17 00:00:00 2001 From: Oluwagbayi James Makinde <122561188+ojmakinde@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:06:05 -0400 Subject: [PATCH 38/38] Pushed hotfix --- app/controllers/main/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 98f032f60..51e1b77b3 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -130,7 +130,7 @@ def events(selectedTerm, activeTab, programID): "toggleStatus": toggleState }) - return render_template("/events/event_list.html", + return render_template("/events/eventList.html", selectedTerm = term, studentLedEvents = studentLedEvents, trainingEvents = trainingEvents,