diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 12ed866d3..bc5b65f68 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -40,7 +40,7 @@ from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses from app.controllers.admin import admin_bp -from app.logic.spreadsheet import createSpreadsheet +from app.logic.volunteerSpreadsheet import createSpreadsheet @admin_bp.route('/admin/reports') @@ -661,17 +661,16 @@ def updatecohort(year, method, username): else: flash(f"Error: {user.fullName} can't be added.", "danger") abort(500) - return "" -@admin_bp.route("/bonnerxls") -def bonnerxls(): +@admin_bp.route("/bonnerXls//") +def getBonnerXls(startingYear, noOfYears): if not g.current_user.isCeltsAdmin: abort(403) - - newfile = makeBonnerXls() + newfile = makeBonnerXls(startingYear, noOfYears) return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True) + @admin_bp.route("/saveRequirements/", methods=["POST"]) def saveRequirements(certid): if not g.current_user.isCeltsAdmin: diff --git a/app/logic/bonner.py b/app/logic/bonner.py index dfd64780b..a6cfc3717 100644 --- a/app/logic/bonner.py +++ b/app/logic/bonner.py @@ -6,19 +6,29 @@ from app import app from app.models.bonnerCohort import BonnerCohort +from app.models.certificationRequirement import CertificationRequirement +from app.models.event import Event +from app.models.eventParticipant import EventParticipant from app.models.eventRsvp import EventRsvp +from app.models.requirementMatch import RequirementMatch from app.models.user import User from app.models.eventCohort import EventCohort +from app.models.term import Term from app.logic.createLogs import createRsvpLog -def makeBonnerXls(): +def makeBonnerXls(selectedYear, noOfYears=1): """ Create and save a BonnerStudents.xlsx file with all of the current and former bonner students. Working with XLSX files: https://xlsxwriter.readthedocs.io/index.html + Params: + selectedYear: The cohort year of interest. + noOfYears: The number of years to be downloaded. + Returns: The file path and name to the newly created file, relative to the web root. """ + selectedYear = int(selectedYear) filepath = app.config['files']['base_path'] + '/BonnerStudents.xlsx' workbook = xlsxwriter.Workbook(filepath, {'in_memory': True}) worksheet = workbook.add_worksheet('students') @@ -33,8 +43,24 @@ def makeBonnerXls(): worksheet.write('D1', 'Student Email', bold) worksheet.set_column('D:D', 20) - students = BonnerCohort.select(BonnerCohort, User).join(User).order_by(BonnerCohort.year.desc(), User.lastName) - + # bonner event titles + bonnerEventsId = 1 + bonnerEvents = CertificationRequirement.select().where(CertificationRequirement.certification==bonnerEventsId).order_by(CertificationRequirement.order.asc()) + bonnerEventInfo = {bonnerEvent.id:(bonnerEvent.name, index + 4) for index, bonnerEvent in enumerate(bonnerEvents)} + allBonnerSpreadsheetPosition = 7 + currentLetter = "E" # next column + for bonnerEvent in bonnerEvents: + worksheet.write(f"{currentLetter}1", bonnerEvent.name, bold) + worksheet.set_column(f"{currentLetter}:{currentLetter}", 30) + currentLetter = chr(ord(f"{currentLetter}") + 1) + + if noOfYears == "all": + students = BonnerCohort.select(BonnerCohort, User).join(User).order_by(BonnerCohort.year.desc(), User.lastName) + else: + noOfYears = int(noOfYears) + startingYear = selectedYear - noOfYears + 1 + students = BonnerCohort.select(BonnerCohort, User).where(BonnerCohort.year.between(startingYear, selectedYear)).join(User).order_by(BonnerCohort.year.desc(), User.lastName) + prev_year = 0 row = 0 for student in students: @@ -47,6 +73,32 @@ def makeBonnerXls(): worksheet.write(row, 2, student.user.bnumber) worksheet.write(row, 3, student.user.email) + # set event fields to the default "incomplete" status + for eventName, eventSpreadsheetPosition in bonnerEventInfo.values(): + worksheet.write(row, eventSpreadsheetPosition, "Incomplete") + + bonnerEventsAttended = ( + RequirementMatch + .select() + .join(Event, on=(RequirementMatch.event == Event.id)) + .join(EventParticipant, on=(RequirementMatch.event == EventParticipant.event)) + .join(CertificationRequirement, on=(RequirementMatch.requirement == CertificationRequirement.id)) + .join(User, on=(EventParticipant.user == User.username)) + .where((CertificationRequirement.certification_id == bonnerEventsId) & (User.username == student.user.username)) + ) + + allBonnerMeetingDates = [] + for attendedEvent in bonnerEventsAttended: + if bonnerEventInfo.get(attendedEvent.requirement.id): + completedEvent = bonnerEventInfo[attendedEvent.requirement.id] + worksheet.write(row, completedEvent[1], attendedEvent.event.startDate.strftime('%m/%d/%Y')) + if completedEvent[0] == "All Bonner Meeting": + allBonnerMeetingDates.append(attendedEvent.event.startDate.strftime('%m/%d/%Y')) + else: + raise Exception("Untracked requirements found in attended events. Debug required.") + + worksheet.write(row, allBonnerSpreadsheetPosition, ", ".join(sorted(allBonnerMeetingDates))) + row += 1 workbook.close() diff --git a/app/logic/spreadsheet.py b/app/logic/volunteerSpreadsheet.py similarity index 100% rename from app/logic/spreadsheet.py rename to app/logic/volunteerSpreadsheet.py diff --git a/app/static/js/bonnerManagement.js b/app/static/js/bonnerManagement.js index 53d271d57..147f4477f 100644 --- a/app/static/js/bonnerManagement.js +++ b/app/static/js/bonnerManagement.js @@ -14,6 +14,17 @@ function cohortRequest(year, method, username){ }) } +function downloadSpreadsheet(blob, fileName) { + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); +} + function addSearchCapabilities(inputElement){ $(inputElement).on("input", function(){ let year = $(this).data('year'); @@ -21,10 +32,19 @@ function addSearchCapabilities(inputElement){ }); } +function updateExportText(){ + const activeYearElement = document.querySelector(".nav-link.year.active"); + if (!activeYearElement) return; + + const startingYear = Number(activeYearElement.getAttribute("data-year")); + const newText = `(${startingYear - 5} - ${startingYear})`; + document.getElementById("last5").textContent = newText; +} /*** Run After Page Load *************************************/ $(document).ready(function(){ $("#addCohort").on('click', addCohort); + $("input[type=search]").each((i, inputElement) => addSearchCapabilities(inputElement)); $(".removeBonner").on("click", function(){ let year = $(this).data('year'); @@ -44,6 +64,32 @@ $(document).ready(function(){ } }); + $(".export-spreadsheet").on('click', function() { + const startingYear = document.getElementsByClassName("nav-link year active")[0].getAttribute("data-year") + const noOfYears = this.getAttribute("data-years") + const url = `/bonnerXls/${startingYear}/${noOfYears}` + const fileName = noOfYears === "all" + ? "Bonner Spreadsheet, All Cohorts" + : `Bonner Spreadsheet, ${Number(startingYear) - Number(noOfYears)} - ${startingYear}` + $.ajax({ + url: url, + method: "GET", + xhrFields: { responseType: "blob" }, + success: function (blob) { + msgFlash("Download Successful", "success"); + downloadSpreadsheet(blob, fileName); + }, + error: function (error, status) { + msgFlash("Download Failed", "danger"); + console.log("Error response:", error.responseText, status); + } + }) + }) + + $(".year").on('click', function() { + updateExportText(); + }); + addRequirementsRowHandlers() // Add Requirement handler @@ -61,6 +107,8 @@ $(document).ready(function(){ }); /** End onready ****************************/ +document.addEventListener("DOMContentLoaded", updateExportText); + /* Add a new requirements row and focus it */ function addRequirement() { var table = $("#requirements"); @@ -103,6 +151,7 @@ function addCohort(){ // Add functionality to the search box on the newly added tab addSearchCapabilities($(`#search-${newCohortYear}`).get()); } + /* Get the data for the whole requirement set and save them */ function saveRequirements() { var data = $("#requirements tbody tr").map((i,row) => ( diff --git a/app/templates/admin/bonnerManagement.html b/app/templates/admin/bonnerManagement.html index dfdc80e27..d497980a0 100644 --- a/app/templates/admin/bonnerManagement.html +++ b/app/templates/admin/bonnerManagement.html @@ -37,7 +37,7 @@

{% for year in cohorts.keys()|sort|reverse %} {% set show = "active" if subTab|int == year else "" %} {% set aria = "true" if subTab|int == year else "false" %} - + {% endfor %}
@@ -68,11 +68,10 @@

{% endfor %} -
- - Export to Excel - Export All - +
+ + +
diff --git a/database/oneOff/populate-requirements-match.sql b/database/oneOff/populate-requirements-match.sql new file mode 100644 index 000000000..3fa313113 --- /dev/null +++ b/database/oneOff/populate-requirements-match.sql @@ -0,0 +1,50 @@ +DROP PROCEDURE IF EXISTS populateRequirementMatch; +DELIMITER // + +create procedure populateRequirementMatch() + begin + declare event_id int; + declare event_name varchar(100); + declare done boolean default false; + + -- bonner variables + declare bonner_orient, all_bonner, service_trip, soph_exchange, junior_recommitment int; + declare legacy_training, learning_pres, bonner_congress, leadership_institute int; + + declare event_info cursor for select event.id, LOWER(event.name) from celts.event join celts.program on event.program_id=program.id where program.isBonnerScholars = 1; + declare continue handler for not found set done = TRUE; + + open event_info; + + events_loop: LOOP + fetch event_info into event_id, event_name; + if done then leave events_loop; + end if; + if event_name like "%orientatio%" then + insert into celts.requirementmatch (requirement_id, event_id) values (1, event_id); + elseif event_name like '%ll bonner meet%' then + insert into celts.requirementmatch (requirement_id, event_id) values (2, event_id); + elseif event_name like '%service trip%' then + insert into celts.requirementmatch (requirement_id, event_id) values (3, event_id); + elseif event_name like '%xchange%' then + insert into celts.requirementmatch (requirement_id, event_id) values (4, event_id); + elseif event_name like '%recommitment%' then + insert into celts.requirementmatch (requirement_id, event_id) values (5, event_id); + elseif event_name like '%legacy%' then + insert into celts.requirementmatch (requirement_id, event_id) values (6, event_id); + elseif event_name like '%presentation%' then + insert into celts.requirementmatch (requirement_id, event_id) values (7, event_id); + elseif event_name like '%congress%' then + insert into celts.requirementmatch (requirement_id, event_id) values (8, event_id); + elseif event_name like '%institute%' then + insert into celts.requirementmatch (requirement_id, event_id) values (9, event_id); + else select event_id, event_name; + end if; + /* selecting it so we can see the failing event on the console */ + end loop; + close event_info; + end // + +DELIMITER ; + +call populateRequirementMatch() \ No newline at end of file diff --git a/tests/code/test_spreadsheet.py b/tests/code/test_spreadsheet.py index 7b929928a..96a94eeaa 100644 --- a/tests/code/test_spreadsheet.py +++ b/tests/code/test_spreadsheet.py @@ -5,7 +5,7 @@ from app.models.term import Term from app.models.eventParticipant import EventParticipant -from app.logic.spreadsheet import * +from app.logic.volunteerSpreadsheet import * @pytest.fixture def fixture_info():