Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added Export Bonner Spreadsheet Features #1388

Open
wants to merge 23 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
611a310
started making us and route changes for the different spreadsheets
WackyWeaver Jan 16, 2025
31b320b
Still fixing buttons and new routes
WackyWeaver Jan 21, 2025
581b707
Attempted a streamline of the saving process, James
ojmakinde Jan 21, 2025
0bdf28a
Attempted to get cohorts iteratively, added the export-button class, …
ojmakinde Jan 22, 2025
05b424a
Removed redundant logic. Established the new spreadsheet ordering sys…
ojmakinde Jan 22, 2025
6a0f6b8
Successfully added Bonner Event columns to the Spreadsheet
ojmakinde Jan 23, 2025
6a30957
Initial attempt at implementing the query logic
ojmakinde Jan 23, 2025
e17ae3b
Fixed the basic query for getting requirement events by name. Need to…
ojmakinde Jan 24, 2025
d3373dd
Implemented query to retrieve Bonner Events and populate spreadsheet …
ojmakinde Jan 27, 2025
8277da8
Naive approach for All Bonner Meeting data retrieval implemented. Tes…
ojmakinde Jan 27, 2025
2399c3c
Naive approach for All Bonner Meeting data retrieval implemented. Tes…
ojmakinde Jan 27, 2025
1e21e56
Merge branch '1378-cohort-export' of https://github.com/BCStudentSoft…
ojmakinde Jan 28, 2025
91ab003
Wrote basics of SQL script. Need to implement
ojmakinde Jan 28, 2025
20d454f
Wrote SQL Script boilerplate. Probably doesn't work???
ojmakinde Jan 30, 2025
7c3a03a
Mostly fixed SQL script
ojmakinde Jan 30, 2025
a5099e9
fixed type in SQL script and program id in test data
WackyWeaver Jan 30, 2025
6d0de8d
Unmerged development, due to failing prod-backup transfer. Improved S…
ojmakinde Jan 30, 2025
666ac65
Removed redundant url call in JavaScript. Modified the file download …
ojmakinde Jan 30, 2025
80f3889
Removed test data changes
ojmakinde Jan 30, 2025
2875a27
Merge branch 'development' of https://github.com/BCStudentSoftwareDev…
WackyWeaver Jan 31, 2025
cbe9b2a
Added requested changes to html UI layout for spreadsheet buttons
WackyWeaver Feb 3, 2025
000022e
Additional changes for added dynamism
ojmakinde Feb 4, 2025
6a34389
Added the dynamic selection
ojmakinde Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions app/controllers/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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/<startingYear>/<noOfYears>")
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/<certid>", methods=["POST"])
def saveRequirements(certid):
if not g.current_user.isCeltsAdmin:
Expand Down
58 changes: 55 additions & 3 deletions app/logic/bonner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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:
Expand All @@ -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()
Expand Down
File renamed without changes.
49 changes: 49 additions & 0 deletions app/static/js/bonnerManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,37 @@ 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');
searchUser(this.id, student => cohortRequest(year, "add", student.username), false, null, "student");
});
}

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');
Expand All @@ -44,6 +64,32 @@ $(document).ready(function(){
}
});

$(".export-spreadsheet").on('click', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments might be useful here.

Copy link
Contributor

@ojmakinde ojmakinde Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they might be. However, I don't think there's any single part of the logic too esoteric to warrant any comments, if you ask me.

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
Expand All @@ -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");
Expand Down Expand Up @@ -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) => (
Expand Down
11 changes: 5 additions & 6 deletions app/templates/admin/bonnerManagement.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ <h3 class="accordion-header" id="headingOne">
{% for year in cohorts.keys()|sort|reverse %}
{% set show = "active" if subTab|int == year else "" %}
{% set aria = "true" if subTab|int == year else "false" %}
<button class="nav-link {{show}}" id="v-pills-{{year}}-tab" data-bs-toggle="pill" data-bs-target="#v-pills-{{year}}" type="button" role="tab" data-year="{{year}}" aria-controls="v-pills-{{year}}" aria-selected="{{aria}}">{{year}} - {{year + 1}}</button>
<button class="nav-link year {{show}}" id="v-pills-{{year}}-tab" data-bs-toggle="pill" data-bs-target="#v-pills-{{year}}" type="button" role="tab" data-year="{{year}}" aria-controls="v-pills-{{year}}" aria-selected="{{aria}}">{{year}} - {{year + 1}}</button>
{% endfor %}
</div>
<div class="tab-content me-2 ms-2" id="v-pills-tabContent">
Expand Down Expand Up @@ -68,11 +68,10 @@ <h3 class="accordion-header" id="headingOne">
</div>
{% endfor %}
</div>
<div class="flex-column ms-2">
<a href="/bonnerxls" target="_blank">
<img src="static/xls-icon-3379.png" alt="Export to Excel" height="32" width="32"/>
<span>Export All</span>
</a>
<div class="d-flex flex-column ms-2">
<button class="btn btn-success py-2 mb-2 export-spreadsheet" data-years="1"> <div><img class="me-2" src="static/xls-icon-3379.png" alt="Export to Excel" height="24" width="24"/> Export Selected Years</div></button>
<button class="btn btn-success py-2 mb-2 export-spreadsheet" data-years="5"> <div class="d-flex"><img class="me-2" src="static/xls-icon-3379.png" alt="Export to Excel" height="24" width="24"/> <a>Export Previous 5 Years</a></div><a id="last5"></a></button>
<button class="btn btn-success py-2 export-spreadsheet" data-years="all"> <div></div><img class="me-2" src="static/xls-icon-3379.png" alt="Export to Excel" height="24" width="24"/> Export All Years</div></button>
</div>
</div>
<!-------------------- End cohort management --------------------->
Expand Down
50 changes: 50 additions & 0 deletions database/oneOff/populate-requirements-match.sql
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @BrianRamsay, what do you think of this, line 35? We're trying to catch Senior Presentation. Do you think just 'presentation' is too vague? (Asking here so I don't forget in person)

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()
2 changes: 1 addition & 1 deletion tests/code/test_spreadsheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down