Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ba5a40b
Add empty file for common routes
BrianRamsay Jul 2, 2026
0d64daf
Add allocation warning to pending LSF approval modal (#622)
DanielRukwasha Jul 8, 2026
ac0ace0
Highlight positions and break hours independently in allocation warning
DanielRukwasha Jul 8, 2026
3cd1205
Added new model requiremetns
BrianRamsay Jul 8, 2026
a57e055
Add new DDL to prod backup
BrianRamsay Jul 8, 2026
a6e09db
Fix failing admin management tests
BrianRamsay Jul 8, 2026
2de8e2b
Fix failing tests - missing Term, and new tracy setup reqs
BrianRamsay Jul 8, 2026
4203dc5
Fixed remaining failing tests
BrianRamsay Jul 8, 2026
cfd9ee9
Merge pull request #640 from BCStudentSoftwareDevTeam/add-models
BrianRamsay Jul 8, 2026
97f05f5
Add option to run just one file
BrianRamsay Jul 8, 2026
10ae9a2
Merge branch 'department-portal-base' of github.com:BCStudentSoftware…
BrianRamsay Jul 8, 2026
78df419
merging changes from brian branch
conwelld Jul 9, 2026
a08e85e
fixed semantic bug in demo data, changed current_year to hardcoded data
conwelld Jul 9, 2026
9522be3
fixed semantic bug in demo data, changed current_year to hardcoded da…
conwelld Jul 9, 2026
ca34008
Flash a warning after approving forms if the department is now over-a…
DanielRukwasha Jul 9, 2026
653ae75
fixed merge issues
conwelld Jul 9, 2026
8ad22e0
fixed merge issues
conwelld Jul 10, 2026
035cda0
Fix over-allocation check to look at each hour-band, not just the total
DanielRukwasha Jul 10, 2026
64e49b8
fixed test error
conwelld Jul 10, 2026
0369e4a
Merge pull request #643 from BCStudentSoftwareDevTeam/demoDataFix_dpb
MImran2002 Jul 10, 2026
f71b525
Merge remote-tracking branch 'origin/department-portal-base' into 622…
DanielRukwasha Jul 10, 2026
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
65 changes: 8 additions & 57 deletions app/controllers/main_routes/main_routes.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from flask import render_template, request, json, redirect, url_for, send_file, g, flash, jsonify
from peewee import JOIN, DoesNotExist, fn
from peewee import JOIN, DoesNotExist
from functools import reduce
import operator
from app.models.allocation import Allocation
from app.models.department import Department
from app.models.supervisor import Supervisor
from app.models.supervisorDepartment import SupervisorDepartment
from app.models.student import Student
from app.models.laborStatusForm import LaborStatusForm
from app.models.formHistory import FormHistory
from app.models.term import Term
from app.models.allocation import Allocation
from app.controllers.admin_routes.allPendingForms import checkAdjustment
from app.controllers.main_routes import main_bp
from app.logic.download import CSVMaker, saveFormSearchResult, retrieveFormSearchResult
Expand All @@ -19,6 +15,7 @@
from app.logic.getTableData import getDatatableData
from app.logic.banner import Banner
from app.logic.tracy import Tracy
from app.logic.allocation import getAllocationSummary

@main_bp.route('/logout', methods=['GET'])
def triggerLogout():
Expand Down Expand Up @@ -82,64 +79,18 @@ def departmentPortal(org=None,account=None):
if i.ORG == org:
supervisors.append(i.FIRST_NAME + " " + i.LAST_NAME + " (" + i.EMAIL + ")")

allocation = None
allocationBands = None
totalPositionsAllocated = None
totalPositionsUsed = None
breakHoursUsed = None
if dept and g.openTerm:
allocation = Allocation.get_or_none(Allocation.department == dept, Allocation.termCode == g.openTerm)
if allocation:
bandFields = [
('primary_10', 'Primary', 10),
('primary_12', 'Primary', 12),
('primary_15', 'Primary', 15),
('primary_20', 'Primary', 20),
('secondary_5', 'Secondary', 5),
('secondary_10', 'Secondary', 10),
]
allocationBands = {}
for fieldName, jobType, hours in bandFields:
used = (LaborStatusForm
.select()
.join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
.where(LaborStatusForm.department == dept,
LaborStatusForm.termCode == g.openTerm,
LaborStatusForm.jobType == jobType,
LaborStatusForm.weeklyHours == hours,
FormHistory.historyType == "Labor Status Form",
~(FormHistory.status % "Denied%"))
.distinct()
.count())
allocationBands[fieldName] = {'used': used, 'allocated': getattr(allocation, fieldName)}

totalPositionsAllocated = sum(band['allocated'] for band in allocationBands.values())
totalPositionsUsed = sum(band['used'] for band in allocationBands.values())

# Break hours are tracked on separate break-term rows (e.g. Thanksgiving Break)
# that share the same academic year prefix as the open AY term.
yearPrefix = str(g.openTerm.termCode)[:-2]
breakTermCodes = [t.termCode for t in Term.select().where(Term.isBreak == True)
if str(t.termCode).startswith(yearPrefix)]
breakHoursUsed = (LaborStatusForm
.select(fn.SUM(LaborStatusForm.contractHours))
.join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
.where(LaborStatusForm.department == dept,
LaborStatusForm.termCode.in_(breakTermCodes),
FormHistory.historyType == "Labor Status Form",
~(FormHistory.status % "Denied%"))
.scalar()) or 0
allocationSummary = getAllocationSummary(dept, g.openTerm)

return render_template('main/departmentPortal.html',
departments = departments,
department = dept,
positions = positions,
supervisors = supervisors,
allocation = allocation,
allocationBands = allocationBands,
totalPositionsAllocated = totalPositionsAllocated,
totalPositionsUsed = totalPositionsUsed,
breakHoursUsed = breakHoursUsed,
allocation = allocationSummary['allocation'],
allocationBands = allocationSummary['allocationBands'],
totalPositionsAllocated = allocationSummary['totalPositionsAllocated'],
totalPositionsUsed = allocationSummary['totalPositionsUsed'],
breakHoursUsed = allocationSummary['breakHoursUsed'],
currentTerm = g.openTerm)

@main_bp.route('/department/<org>/<account>/managepositions', methods=['GET'])
Expand Down
20 changes: 15 additions & 5 deletions app/logic/allPendingForms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from datetime import date
from flask import jsonify
from flask import jsonify, g
from app.models.formHistory import FormHistory
from app.models.status import Status
from app.logic.banner import Banner
Expand All @@ -14,6 +14,7 @@
from app.models.overloadForm import OverloadForm
from app.models.notes import Notes
from app.login_manager import DoesNotExist, render_template
from app.logic.allocation import getAllocationWarning


def saveStatus(new_status, formHistoryIds, currentUser):
Expand Down Expand Up @@ -196,9 +197,11 @@ def laborAdminOverloadApproval(rsp, historyForm, status, currentUser, currentDat

# extract data from the database to populate pending form approval modal
def modal_approval_and_denial_data(formHistoryIdList):
''' This method grabs the data that populated the on approve modal for lsf'''
''' This method grabs the data that populated the on approve modal for lsf,
plus an over-allocation warning per unique department among the selected forms. '''

details_list = []
allocationWarningsByDept = {}
for fhID in formHistoryIdList:
formHistory = FormHistory.get(FormHistory.formHistoryID == fhID)
lsf = formHistory.formID
Expand All @@ -208,7 +211,8 @@ def modal_approval_and_denial_data(formHistoryIdList):
supervisorName = f"{lsf.supervisor.FIRST_NAME} {lsf.supervisor.LAST_NAME}"
weeklyHours = lsf.weeklyHours
contractHours = lsf.contractHours
deptName = lsf.department.DEPT_NAME
dept = lsf.department
deptName = dept.DEPT_NAME

if formHistory.adjustedForm:
match formHistory.adjustedForm.fieldAdjusted:
Expand All @@ -223,11 +227,17 @@ def modal_approval_and_denial_data(formHistoryIdList):
case "contractHours":
contractHours = formHistory.adjustedForm.newValue
case "department":
deptName = Department.get(Department.ORG==formHistory.adjustedForm.newValue).DEPT_NAME
dept = Department.get(Department.ORG==formHistory.adjustedForm.newValue)
deptName = dept.DEPT_NAME

details_list.append([studentName, deptName, position, str(weeklyHours),str(contractHours), supervisorName])

return details_list
if dept.departmentID not in allocationWarningsByDept:
warning = getAllocationWarning(dept, g.openTerm)
if warning:
allocationWarningsByDept[dept.departmentID] = warning

return {"details": details_list, "allocationWarnings": list(allocationWarningsByDept.values())}


def financialAidSAASOverloadApproval(historyForm, rsp, status, currentUser, currentDate):
Expand Down
110 changes: 110 additions & 0 deletions app/logic/allocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from peewee import fn
from app.models.allocation import Allocation
from app.models.laborStatusForm import LaborStatusForm
from app.models.formHistory import FormHistory
from app.models.term import Term

# Each entry is (Allocation field name, LaborStatusForm.jobType, LaborStatusForm.weeklyHours)
ALLOCATION_BAND_FIELDS = [
('primary_10', 'Primary', 10),
('primary_12', 'Primary', 12),
('primary_15', 'Primary', 15),
('primary_20', 'Primary', 20),
('secondary_5', 'Secondary', 5),
('secondary_10', 'Secondary', 10),
]


def getAllocationSummary(dept, term):
"""
Returns a dict describing a department's allocation vs. actual usage for the given term:
- allocation: the Allocation row for this dept/term, or None if none exists
- allocationBands: {fieldName: {'used': int, 'allocated': int}} per hour-band, or None
- totalPositionsAllocated / totalPositionsUsed: ints, or None
- breakHoursUsed: int, or None
'used' counts are non-denied LaborStatusForms, matching the same filter pattern
used elsewhere in the app (see app/logic/statusFormFunctions.py).
"""
summary = {
'allocation': None,
'allocationBands': None,
'totalPositionsAllocated': None,
'totalPositionsUsed': None,
'breakHoursUsed': None,
}

if not (dept and term):
return summary

allocation = Allocation.get_or_none(Allocation.department == dept, Allocation.termCode == term)
summary['allocation'] = allocation
if not allocation:
return summary

allocationBands = {}
for fieldName, jobType, hours in ALLOCATION_BAND_FIELDS:
used = (LaborStatusForm
.select()
.join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
.where(LaborStatusForm.department == dept,
LaborStatusForm.termCode == term,
LaborStatusForm.jobType == jobType,
LaborStatusForm.weeklyHours == hours,
FormHistory.historyType == "Labor Status Form",
~(FormHistory.status % "Denied%"))
.distinct()
.count())
allocationBands[fieldName] = {'used': used, 'allocated': getattr(allocation, fieldName)}

summary['allocationBands'] = allocationBands
summary['totalPositionsAllocated'] = sum(band['allocated'] for band in allocationBands.values())
summary['totalPositionsUsed'] = sum(band['used'] for band in allocationBands.values())

# Break hours are tracked on separate break-term rows (e.g. Thanksgiving Break)
# that share the same academic year prefix as the given AY term.
yearPrefix = str(term.termCode)[:-2]
breakTermCodes = [t.termCode for t in Term.select().where(Term.isBreak == True)
if str(t.termCode).startswith(yearPrefix)]
summary['breakHoursUsed'] = (LaborStatusForm
.select(fn.SUM(LaborStatusForm.contractHours))
.join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
.where(LaborStatusForm.department == dept,
LaborStatusForm.termCode.in_(breakTermCodes),
FormHistory.historyType == "Labor Status Form",
~(FormHistory.status % "Denied%"))
.scalar()) or 0

return summary


def getAllocationWarning(dept, term):
"""
Returns a summary dict for displaying an over-allocation warning for the given
department/term (e.g. in the pending-LSF approval modal), or None if there's no
allocation on record for that department/term to compare against.

Note: 'used' counts (from getAllocationSummary) include Pending as well as
Approved forms, so a form currently Pending already occupies a slot here -
these numbers already reflect what utilization would be once it's approved.
"""
summary = getAllocationSummary(dept, term)
if not summary['allocation']:
return None

positionsRemaining = summary['totalPositionsAllocated'] - summary['totalPositionsUsed']
breakHoursRemaining = summary['allocation'].breakHours - summary['breakHoursUsed']
isPositionsOverAllocated = positionsRemaining < 0
isBreakHoursOverAllocated = breakHoursRemaining < 0

return {
'departmentName': dept.DEPT_NAME,
'totalPositionsAllocated': summary['totalPositionsAllocated'],
'totalPositionsUsed': summary['totalPositionsUsed'],
'positionsRemaining': positionsRemaining,
'isPositionsOverAllocated': isPositionsOverAllocated,
'breakHoursAllocated': summary['allocation'].breakHours,
'breakHoursUsed': summary['breakHoursUsed'],
'breakHoursRemaining': breakHoursRemaining,
'isBreakHoursOverAllocated': isBreakHoursOverAllocated,
'isOverAllocated': isPositionsOverAllocated or isBreakHoursOverAllocated,
}
30 changes: 28 additions & 2 deletions app/static/js/allPendingForms.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ function insertApprovals(laborHistoryId = null) {
contentType: 'application/json',
success: function(response) {
if (response) {
var returned_details = response;
updateApproveTableData(returned_details);
updateApproveTableData(response.details);
updateAllocationWarnings(response.allocationWarnings);
}
}
});
Expand All @@ -112,6 +112,31 @@ function updateApproveTableData(returned_details) {
}
}

// Shows a non-blocking allocation warning per department represented among the
// selected forms, so admins can see the impact of approval before confirming.
// Each category (positions / break hours) is highlighted independently, since
// a department can be over on one and fine on the other.
function updateAllocationWarnings(allocationWarnings) {
if (!allocationWarnings) { return; }
for (var i = 0; i < allocationWarnings.length; i++) {
var w = allocationWarnings[i];
var boxClass = w.isOverAllocated ? 'alert-warning' : 'alert-info';
var overStyle = 'color:#a94442; font-weight:bold;';
var positionsStyle = w.isPositionsOverAllocated ? overStyle : '';
var breakHoursStyle = w.isBreakHoursOverAllocated ? overStyle : '';
var positionsFlag = w.isPositionsOverAllocated ? ' &#9888; Over allocation' : '';
var breakHoursFlag = w.isBreakHoursOverAllocated ? ' &#9888; Over allocation' : '';
var html = '<div class="alert ' + boxClass + '" role="alert">' +
'<strong>' + w.departmentName + ' Allocation</strong><br>' +
'<span style="' + positionsStyle + '">Positions: ' + w.totalPositionsUsed + ' / ' + w.totalPositionsAllocated +
' allocated (' + w.positionsRemaining + ' remaining)' + positionsFlag + '</span><br>' +
'<span style="' + breakHoursStyle + '">Break Hours: ' + w.breakHoursUsed + ' / ' + w.breakHoursAllocated +
' allocated (' + w.breakHoursRemaining + ' remaining)' + breakHoursFlag + '</span>' +
'</div>';
$('#allocationWarnings').append(html);
}
}


$('#approvalModal').on('hidden.bs.modal', function () {// Makes the close functionality work when clicking outside of the modal
approvalModalClose();
Expand All @@ -120,6 +145,7 @@ $('#approvalModal').on('hidden.bs.modal', function () {// Makes the close functi

function approvalModalClose(){// on close of approval modal we are clearing the table to prevent duplicate data.
$('#classTableBody').empty();
$('#allocationWarnings').empty();
labor_details_ids = [] // emptying the list, becuase otherwise will cause duplicate data.
}

Expand Down
1 change: 1 addition & 0 deletions app/templates/snips/pendingApprovalModal.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ <h5 class="modal-title" id="approvalModalLabel" align="center"><strong>{{modalTi
</button>
</div>
<div class="modal-body" style="overflow: auto;height: 350px" id="approveModalBody">
<div id="allocationWarnings"></div>
<!-- putting info into modal using table for better viewing -->
<table id="classTable" class="table table-bordered">
<thead>
Expand Down
2 changes: 2 additions & 0 deletions database/demo_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
student['ID'] = student['ID'].strip()
student['legal_name'] = student['FIRST_NAME'].strip()
del student['FIRST_NAME']
student['isActive'] = True

students.append(student)
Student.insert_many(students).on_conflict_replace().execute()
Expand Down Expand Up @@ -365,6 +366,7 @@

staff['legal_name'] = staff['FIRST_NAME'].strip()
del staff['FIRST_NAME']
staff['isActive'] = True
Supervisor.get_or_create(**staff)

# Add non Supervisor staffs to Tracy db
Expand Down