From 9bd90f637cb2e72d2ebd41550ee604a467e3b468 Mon Sep 17 00:00:00 2001 From: conwelld Date: Tue, 21 Jul 2026 10:54:46 -0400 Subject: [PATCH 01/15] added changes from previous branch to avoid all files being commited --- app/controllers/main_routes/__init__.py | 1 + app/controllers/main_routes/main_routes.py | 113 ++++- app/models/positionHistory.py | 1 + app/static/css/base.css | 1 + app/static/css/departmentPortal.css | 75 +++ app/static/js/departmentPortal.js | 4 + app/templates/main/departmentPortal.html | 47 +- database/demo_data.py | 515 ++++++++++++++++++++- 8 files changed, 747 insertions(+), 10 deletions(-) create mode 100644 app/static/css/departmentPortal.css diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 54b32d3c5..e8c4c6bb0 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -25,3 +25,4 @@ def injectGlobalData(): from app.controllers.main_routes import studentLaborEvaluation from app.controllers.main_routes import search from app.controllers.main_routes import studentResponse +from app.controllers.main_routes import departmentPortal diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 0a2f21e4b..de9a189c3 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -1,5 +1,6 @@ from flask import render_template, request, json, redirect, url_for, send_file, g, flash, jsonify -from peewee import JOIN, DoesNotExist +from peewee import JOIN, DoesNotExist, fn +from flask_bootstrap import forms from functools import reduce import operator from app.models.department import Department @@ -16,6 +17,9 @@ from app.login_manager import require_login, logout from app.logic.getTableData import getDatatableData from app.logic.banner import Banner +from app.models.allocation import Allocation +from app.logic.tracy import Tracy +from app.models.positionHistory import PositionHistory @main_bp.route('/logout', methods=['GET']) def triggerLogout(): @@ -51,9 +55,12 @@ def supervisorPortal(): @main_bp.route('/department/', methods=['GET']) @main_bp.route('/department//', methods=['GET']) def departmentPortal(org=None,account=None): - try: - dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) - except (NameError, DoesNotExist): + if org and account: + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + dept = None + else: dept = None @@ -62,11 +69,105 @@ def departmentPortal(org=None,account=None): departments = list(Department.select().order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) else: departments = list(getDepartmentsForSupervisor(g.currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) + try: + allocation = Allocation.select(Allocation, Term).join(Term).where(Allocation.department == dept, Allocation.termCode == 202500).get() + except DoesNotExist: + allocation = None + + supervisorDepartments = (SupervisorDepartment.select().join(Supervisor).where(SupervisorDepartment.department == dept) + .order_by(fn.COALESCE(Supervisor.preferred_name, Supervisor.legal_name, Supervisor.LAST_NAME).asc())) + + laborCoordinators = [] + supervisors = [] + + for supervisorDepartment in supervisorDepartments: + supervisor = supervisorDepartment.supervisor + + if supervisor is None: + continue + + firstName = supervisor.preferred_name or supervisor.legal_name or "" + lastName = supervisor.LAST_NAME or "" + + supervisorName = f"{firstName} {lastName}".strip() + + supervisorDisplay = { + "name": supervisorName, + "email": supervisor.EMAIL + } + + if supervisorDepartment.isCoordinator: + laborCoordinators.append(supervisorDisplay) + else: + supervisors.append(supervisorDisplay) + + totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() + usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) + studentHours = {} + for form in LaborStatusForm.select().where(LaborStatusForm.department == dept,LaborStatusForm.termCode_id == 202500,LaborStatusForm.contractHours.is_null(True) + ): + studentSuperviseeId = form.studentSupervisee_id + if studentSuperviseeId not in studentHours: + studentHours[studentSuperviseeId] = [] + studentHours[studentSuperviseeId].append({ + "jobType": form.jobType, + "weeklyHours": form.weeklyHours + }) + + + def count_workers(job_type, hours_bucket): + return LaborStatusForm.select().where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.jobType == job_type, LaborStatusForm.weeklyHours == hours_bucket, LaborStatusForm.contractHours.is_null(True)).count() + + usedPositions = { + "used_10": count_workers("Primary", "10"), + "used_12": count_workers("Primary", "12"), + "used_15": count_workers("Primary", "15"), + "used_20": count_workers("Primary", "20"), + "used_5_sec": count_workers("Secondary", "5"), + "used_10_sec": count_workers("Secondary", "10"), +} + break_allocation = LaborStatusForm.select(LaborStatusForm.contractHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(False)) + sumBreak = sum(form.contractHours or 0 for form in break_allocation) + positions = list(PositionHistory.select().where(PositionHistory.department == dept, PositionHistory.status == "Active").order_by(PositionHistory.positionTitle.asc())) if dept else [] + positionsList = [] + posUrl = [] + if not positions: + positionsList = ["No active positions in this department"] + else: + for i in positions: + positionsList.append(i.positionTitle + ": " + "(WLS " + str(i.wls) + ")") + posUrl.append(str(i.positionCode)) return render_template('main/departmentPortal.html', departments = departments, - department = dept) - + department = dept, + allocation = allocation, + total_allocation = totalPositions, + used_allocation = usedAllocation, + term = g.openTerm.termName, + studentHours = studentHours, + usedPositions = usedPositions, + break_hours = sumBreak, + positions = positionsList, + posUrl = posUrl, + supervisors = supervisors, + laborCoordinators=laborCoordinators, + currentUser=g.currentUser + ) +@main_bp.route('/department///managepositions', methods=['GET']) +def managePositions(org, account): + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except DoesNotExist: + return render_template('errors/404.html'), 404 + + positions = Tracy().getPositionsFromDepartment(org, account) + print(positions) + return render_template('main/managepositions.html', + department = dept, + department_name = dept.DEPT_NAME, + positions = positions + ) @main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form diff --git a/app/models/positionHistory.py b/app/models/positionHistory.py index 514670759..9a248aacb 100644 --- a/app/models/positionHistory.py +++ b/app/models/positionHistory.py @@ -2,6 +2,7 @@ from app.models.department import Department class PositionHistory(baseModel): + positionTitle = CharField() positionCode = CharField() department = ForeignKeyField(Department) status = CharField() diff --git a/app/static/css/base.css b/app/static/css/base.css index 940f8d6e5..6b87b2286 100755 --- a/app/static/css/base.css +++ b/app/static/css/base.css @@ -207,6 +207,7 @@ a { background-clip: border-box; border: 1px solid rgba(0, 0, 0, 0.125); border-radius: 0.25rem; + overflow: hidden; } .card-body { diff --git a/app/static/css/departmentPortal.css b/app/static/css/departmentPortal.css new file mode 100644 index 000000000..6cccf556a --- /dev/null +++ b/app/static/css/departmentPortal.css @@ -0,0 +1,75 @@ +.card { + border-radius: 1rem; + overflow: hidden; + width: 100%; + height: 100%; + min-width: 100%; + box-shadow: 2px 2px 4px rgba(100, 100, 100, 0.26); +} +.bi-suitcase-lg-fill { /* Bootstrap Icon */ + border: 1px solid #c0c0c0; + border-radius: 8px; + padding: 3px 3.5px 1.5px 3.5px; + font-size: 3rem; + color:#6e6e6e; +} +.bi-clock { + border: 1px solid #c0c0c0; + border-radius: 8px; + padding: 3px 3.5px 1.5px 3.5px; + font-size: 3rem; + color:#6e6e6e; +} +.bi-info-circle { + padding: 3px 3.5px 1.5px 3.5px; + font-size: 1.5rem; + vertical-align: middle; + color:#6e6e6e; +} +.header-container { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 2%; + padding-right: 10px; +} +.allocation-list { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 10px; + padding-right: 10px; +} +.primary-chart { + font-size: 1.2em; + padding-left: 15%; +} +.secondary-chart { + font-size: 1.2em; + padding-right: 15%; +} + +.bi-people-fill { /* Bootstrap Icon for Members Card */ + border: 1px solid #c0c0c0; + border-radius: 8px; + padding: 3px 3.5px 1.5px 3.5px; + font-size: 3rem; + color:#6e6e6e; +} + +.card-group { + gap: 1rem; +} +.form-group { + width: 50%; + margin-left: auto ; + margin-right:auto; +} +.card-body { + padding: 1rem 1rem; + min-width: 100% +} +.row { + display: flex; + flex-wrap: wrap; +} \ No newline at end of file diff --git a/app/static/js/departmentPortal.js b/app/static/js/departmentPortal.js index cb2023a3a..06ae72e76 100644 --- a/app/static/js/departmentPortal.js +++ b/app/static/js/departmentPortal.js @@ -4,3 +4,7 @@ $(document).ready(function() { window.location = `/department/${deptData.org}/${deptData.account}`; }); }); + +$(function () { + $('[data-toggle="tooltip"]').tooltip() +}) diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index 8ee01838b..b51354f0e 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -3,11 +3,12 @@ {% block scripts %} {{super()}} + {% endblock %} - {% block app_content %}

{% if department %} {{department.DEPT_NAME}} Portal {% else %} Choose a Department: {% endif %}

+
+
+{% if department %} +
+
+
+
+
+
+ +
+
+

Allocations

+
+
+

AY 2025-2026

{{used_allocation}}/{{total_allocation or 0}} Positions

+
+
+
    +
  • 10 Hour - {{usedPositions.used_10}}/{{allocation.primary_10}}
  • +
  • 12 Hour - {{usedPositions.used_12}}/{{allocation.primary_12}}
  • +
  • 15 Hour - {{usedPositions.used_15}}/{{allocation.primary_15}}
  • +
  • 20 Hour - {{usedPositions.used_20}}/{{allocation.primary_20}}
  • +
+
    +
  • 5 Hour - {{usedPositions.used_5_sec}}/{{allocation.secondary_5}}
  • +
  • 10 Hour - {{usedPositions.used_10_sec}}/{{allocation.secondary_10}}
  • +
+
+
+

Break Hours

{{break_hours}}/{{allocation.breakHours or 0}} Hours

+
+
+
+ +
+
-{% endblock %} +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/database/demo_data.py b/database/demo_data.py index de6047624..3bce122a8 100644 --- a/database/demo_data.py +++ b/database/demo_data.py @@ -19,6 +19,10 @@ from app.models.supervisorDepartment import SupervisorDepartment from app.models.allocation import Allocation from app.models.positionHistory import PositionHistory +from app.models.laborReleaseForm import LaborReleaseForm +from app.models.supervisorDepartment import SupervisorDepartment +from app.models.allocation import Allocation +from app.models.positionHistory import PositionHistory print("Inserting data for demo and testing purposes") @@ -41,7 +45,36 @@ "LAST_POSN":"Media Technician", "LAST_SUP_PIDM":"7" }, - + { + "ID":"B00741361", + "PIDM":"99", + "FIRST_NAME":"Antonia", + "LAST_NAME":"Schmith", + "CLASS_LEVEL":"Freshman", + "ACADEMIC_FOCUS":"Computer Science", + "MAJOR":"Computer Science", + "PROBATION":"0", + "ADVISOR":"Scott Heggen", + "STU_EMAIL":"schmitha@berea.edu", + "STU_CPO":"777", + "LAST_POSN":"TA", + "LAST_SUP_PIDM":"7" + }, + { + "ID":"B00732363", + "PIDM":"58", + "FIRST_NAME":"Barbara", + "LAST_NAME":"Williams", + "CLASS_LEVEL":"Junior", + "ACADEMIC_FOCUS":"Computer Science", + "MAJOR":"Computer Science", + "PROBATION":"0", + "ADVISOR":"Jasmine Jones", + "STU_EMAIL":"williamsb@berea.edu", + "STU_CPO":"118", + "LAST_POSN":"TA", + "LAST_SUP_PIDM":"7" + }, { "ID":"B00730361", "PIDM":"1", @@ -104,6 +137,12 @@ "LAST_POSN":"Student Manager", "LAST_SUP_PIDM":"7" }, + {"ID": "B00811617", "legal_name": "Chris Georgiev", "isActive": True, "PIDM": "8", "FIRST_NAME": "Chris", "LAST_NAME": "Georgiev"}, + {"ID": "B00815474", "legal_name": "Julius Fritz", "isActive": True, "PIDM": "9", "FIRST_NAME": "Julius", "LAST_NAME": "Fritz"}, + {"ID": "B12345223", "legal_name": "Subaru Natsuki", "isActive": True, "PIDM": "10", "FIRST_NAME": "Subaru", "LAST_NAME": "Natsuki"}, + {"ID": "B12345003", "legal_name": "Hatsune Miku", "isActive": True, "PIDM": "11", "FIRST_NAME": "Hatsune", "LAST_NAME": "Miku"}, + {"ID": "B12345772", "legal_name": "Michael Jackson", "isActive": True, "PIDM": "12", "FIRST_NAME": "Michael", "LAST_NAME": "Jackson"}, + {"ID": "B12345756", "legal_name": "Genji Overwatch", "isActive": True, "PIDM": "13", "FIRST_NAME": "Genji", "LAST_NAME": "Overwatch"} ] tracyStudents = [ { @@ -411,6 +450,22 @@ "isSaasAdmin": None }, { + "student": "B00741361", + "supervisor": None, + "username": "schmitha", + "isLaborAdmin": None, + "isFinancialAidAdmin": None, + "isSaasAdmin": None + }, + { + "student": "B00732363", + "supervisor": None, + "username": "williamsb", + "isLaborAdmin": None, + "isFinancialAidAdmin": None, + "isSaasAdmin": None + }, + { "student": "B00730361", "supervisor": None, "username": "jamalie", @@ -567,6 +622,124 @@ "createdDate": f"2025-04-14", "status_id": "Pending" }]).on_conflict_replace().execute() +LaborStatusForm.insert([{ + "laborStatusFormID": 11, + "termCode_id": f"202500", + "studentName": "Antonia Schmith", + "studentSupervisee_id": "B00741361", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Student Programmer", + "POSN_CODE": "S61407", + "weeklyHours": 10, + "startDate": f"2026-04-01", + "endDate": f"2026-09-01", + "studentConfirmation": True + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 11, + "formID_id": "11", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 12, + "termCode_id": f"202500", + "studentName": "Barbara Williams", + "studentSupervisee_id": "B00732363", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Student Programmer", + "POSN_CODE": "S61407", + "weeklyHours": 10, + "startDate": f"2027-04-01", + "endDate": f"2029-09-01", + "studentConfirmation": True + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 12, + "formID_id": "12", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status": "Approved" + }]).on_conflict_replace().execute() + +LaborReleaseForm.insert([{ + "laborReleaseFormID": 10, + "conditionAtRelease": "unsatisfactory", + "releaseDate": f"2025-04-14", + "reasonForRelease": "Smoking Cigarettes in the Programmers' space." + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 13, + "formID_id": "12", + "historyType_id": "Labor Release Form", + "releaseForm": 10, + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 4, + "termCode_id": f"202500", + "studentName": "Elaleh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Labor Workers", + "POSN_CODE": "S61419", + "weeklyHours": 10, + "startDate": f"2027-04-01", + "endDate": "2027-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 4, + "formID_id": "4", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 5, + "termCode_id": f"202500", + "studentName": "Oluwagbayi Makinde", + "studentSupervisee_id": "B00791326", + "supervisor_id": "B12365892", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Labor Workers", + "POSN_CODE": "S61429", + "weeklyHours": 10, + "startDate": f"2025-04-01", + "endDate": "2029-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 5, + "formID_id": "5", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status": "Approved" + }]).on_conflict_replace().execute() LaborStatusForm.insert([{ "laborStatusFormID": 3, @@ -592,6 +765,23 @@ "createdDate": f"2025-04-14", "status_id": "Approved" }]).on_conflict_replace().execute() +LaborStatusForm.insert([{ + + "laborStatusFormID": 9, + "termCode_id": f"202500", + "studentName": "Genji Overwatch", + "studentSupervisee_id": "B12345756", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "overwtahc guy", + "POSN_CODE": "S61410", + "contractHours": 15, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + + }]).on_conflict_replace().execute() @@ -796,4 +986,325 @@ ] PositionHistory.insert_many(positionHistory).on_conflict_replace().execute() -print(" * position history added") \ No newline at end of file +print(" * position history added") + +############################ +# Allocation Dummy Data: +########################### +allocations = [ + { + "termCode": 202500, + "department": 3, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "Downscaling due to decrease in student enrollment caused by current economic conditions", + "primary_10": 2, + "primary_12": 2, + "primary_15": 1, + "primary_20": 0, + "secondary_5": 1, + "secondary_10": 0, + "breakHours": 260, + }, + { + "termCode": 202500, + "department": 2, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "Increase in student enrollment due to exodous from CS department", + "primary_10": 4, + "primary_12": 2, + "primary_15": 7, + "primary_20": 4, + "secondary_5": 2, + "secondary_10": 0, + "breakHours": 750, + }, + { + "termCode": 202500, + "department": 1, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "We are hiring more students to help with the increased workload in the department", + "primary_10": 5, + "primary_12": 6, + "primary_15": 4, + "primary_20": 1, + "secondary_5": 7, + "secondary_10": 0, + "breakHours": 550, + }, + { + "termCode": 202500, + "department": 4, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "Downscaling the number of students in the department due to budget cuts", + "primary_10": 4, + "primary_12": 5, + "primary_15": 0, + "primary_20": 0, + "secondary_5": 1, + "secondary_10": 0, + "breakHours": 300, + }, + { + "termCode": 202500, + "department": 5, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "Due to rapid department growth, we need to hire more students to help with the increased workload", + "primary_10": 8, + "primary_12": 10, + "primary_15": 7, + "primary_20": 4, + "secondary_5": 5, + "secondary_10": 1, + "breakHours": 900, + }, + + ] +Allocation.insert_many(allocations).on_conflict_replace().execute() + +print("Data insertion complete :)") +allocation =[ + { + "termCode":f"{2025}00", + "department": 3, + "isFinal": True, + "approvedOn": f"{2025}-06-30", + "approvedBy": "B12365892", + "justification": "We just want it for fun", + "primary_10": 2, + "primary_12": 3, + "primary_15": 1, + "primary_20": 6, + "secondary_5": 2, + "secondary_10": 0, + "breakHours": 500 + }, + { + "termCode":f"{2025}00", + "department": 2, + "isFinal": False, + "approvedOn": f"{2025}-06-20", + "approvedBy": "B00763721", + "justification": "We need it to lower the amount of allocations we have", + "primary_10": 1, + "primary_12": 2, + "primary_15": 5, + "primary_20": 2, + "secondary_5": 10, + "secondary_10": 0, + "breakHours": 1500 + } + ] +Allocation.insert_many(allocation).on_conflict_replace().execute() +print(" * allocation added") + + +############################# +# Position History +############################# + +positionHistory = [ + { + "positionTitle": "Student Programmer", + "positionCode": "S61407", + "status": "Active", + "wls": 1, + "revisionDate": f"2026-07-01", + "description": "", + "department": 1 + }, + { + "positionTitle": "Research Associate", + "positionCode": "S61408", + "status": "Active", + "wls": 2, + "revisionDate": f"2026-09-01", + "description": "", + "department": 1 + }, + { + "positionTitle": "Labor Workers", + "positionCode": "S61409", + "status": "Active", + "wls": 3, + "revisionDate": f"2026-07-01", + "description": "", + "department": 1 + }, + { + "positionTitle": "Teaching Associate", + "positionCode": "S61411", + "status": "Active", + "wls":3, + "revisionDate" : f"2026-01-01", + "description": "", + "department" : 1 + + }, + { + "positionTitle": "Teaching Associate", + "positionCode": "S61410", + "status": "Inactive", + "wls":2, + "revisionDate" : f"2026-01-01", + "description": "", + "department" : 3 + }, + { + "positionTitle": "Teaching Associate", + "positionCode": "S61410", + "status": "Active", + "wls":2, + "revisionDate" : f"2026-03-29", + "description": "", + "department" : 3 + }, + { + "positionTitle": "DUMMY POSITION", + "positionCode": "S12345", + "status": "Active", + "wls":3, + "revisionDate" : f"2026-01-23", + "description": "", + "department" : 1 + }, + { + "positionTitle": "Junior Data Analyst", + "positionCode": "S39568", + "status": "Active", + "wls":4, + "revisionDate" : f"2026-01-31", + "description": "", + "department" : 1 + }, + { + "positionTitle": "Student Manager", + "positionCode": "S74933", + "status": "Active", + "wls":5, + "revisionDate" : f"2026-04-01", + "description": "", + "department" : 1 + }, + { + "positionTitle": "IT Technician", + "positionCode": "S94932", + "status": "Active", + "wls":6, + "revisionDate" : f"2026-05-03", + "description": "", + "department" : 1 + }, + { + "positionTitle": "Human code generator", + "positionCode": "S22222", + "status": "Active", + "wls":1, + "revisionDate" : f"2026-05-03", + "description": "", + "department" : 1 + }, + { + "positionTitle": "Senior Software Engineer", + "positionCode": "S00000", + "status": "Active", + "wls":6, + "revisionDate" : f"2026-05-03", + "description": "", + "department" : 1 + } + + + +] +PositionHistory.insert_many(positionHistory).on_conflict_replace().execute() + +dummy_lsf = [ + { + "laborStatusFormID": 4, + "termCode_id": f"202500", + "studentName": "Chris Georgiev", + "studentSupervisee_id": "B00811617", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 4, + "POSN_TITLE": "guy who does stuff", + "POSN_CODE": "S61415", + "weeklyHours": 12, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }, + { + + "laborStatusFormID": 5, + "termCode_id": f"202500", + "studentName": "Julius Fritz", + "studentSupervisee_id": "B00815474", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 2, + "POSN_TITLE": "guy who sits in chair", + "POSN_CODE": "S61416", + "weeklyHours": 15, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }, + { + "laborStatusFormID": 6, + "termCode_id": f"202500", + "studentName": "Subaru Natsuki", + "studentSupervisee_id": "B12345223", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Aura Monster", + "POSN_CODE": "S61417", + "weeklyHours": 20, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }, + { + "laborStatusFormID": 7, + "termCode_id": f"202500", + "studentName": "Hatsune Miku", + "studentSupervisee_id": "B12345003", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 6, + "POSN_TITLE": "Singer", + "POSN_CODE": "S61409", + "weeklyHours": 20, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + + }, + { + "laborStatusFormID": 8, + "termCode_id": f"202500", + "studentName": "Michael Jackson", + "studentSupervisee_id": "B12345772", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Secondary", + "WLS": 6, + "POSN_TITLE": "Famous singer", + "POSN_CODE": "S61410", + "weeklyHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + } +] +LaborStatusForm.insert_many(dummy_lsf).on_conflict_replace().execute() \ No newline at end of file From 56d5a2971ea6a3d93be4341c5e1096ecfac94091 Mon Sep 17 00:00:00 2001 From: conwelld Date: Tue, 21 Jul 2026 11:28:11 -0400 Subject: [PATCH 02/15] fixed some pr comments --- app/controllers/main_routes/main_routes.py | 30 +++------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index de9a189c3..45a791318 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -101,9 +101,9 @@ def departmentPortal(org=None,account=None): else: supervisors.append(supervisorDisplay) - totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() - usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) - studentHours = {} + totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() #grabs the total positions that can be fufilled by contracts + usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) #grabs the total amount of contracts fufilled from totalPositions + studentHours = {} #Group each active LSF (job type + weekly hours) by student, so we can show all of a student's jobs together for form in LaborStatusForm.select().where(LaborStatusForm.department == dept,LaborStatusForm.termCode_id == 202500,LaborStatusForm.contractHours.is_null(True) ): studentSuperviseeId = form.studentSupervisee_id @@ -128,16 +128,7 @@ def count_workers(job_type, hours_bucket): } break_allocation = LaborStatusForm.select(LaborStatusForm.contractHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(False)) sumBreak = sum(form.contractHours or 0 for form in break_allocation) - positions = list(PositionHistory.select().where(PositionHistory.department == dept, PositionHistory.status == "Active").order_by(PositionHistory.positionTitle.asc())) if dept else [] - positionsList = [] - posUrl = [] - if not positions: - positionsList = ["No active positions in this department"] - else: - for i in positions: - positionsList.append(i.positionTitle + ": " + "(WLS " + str(i.wls) + ")") - posUrl.append(str(i.positionCode)) - + return render_template('main/departmentPortal.html', departments = departments, department = dept, @@ -154,20 +145,7 @@ def count_workers(job_type, hours_bucket): laborCoordinators=laborCoordinators, currentUser=g.currentUser ) -@main_bp.route('/department///managepositions', methods=['GET']) -def managePositions(org, account): - try: - dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) - except DoesNotExist: - return render_template('errors/404.html'), 404 - positions = Tracy().getPositionsFromDepartment(org, account) - print(positions) - return render_template('main/managepositions.html', - department = dept, - department_name = dept.DEPT_NAME, - positions = positions - ) @main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form From faf19578132b3b969d583813ac8929a1ced636cf Mon Sep 17 00:00:00 2001 From: conwelld Date: Tue, 21 Jul 2026 11:34:27 -0400 Subject: [PATCH 03/15] fixed some pr comments for css styling --- app/controllers/main_routes/main_routes.py | 24 ++++++++++++++-------- app/static/css/departmentPortal.css | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 45a791318..15ef9faf7 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -101,9 +101,9 @@ def departmentPortal(org=None,account=None): else: supervisors.append(supervisorDisplay) - totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() #grabs the total positions that can be fufilled by contracts - usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) #grabs the total amount of contracts fufilled from totalPositions - studentHours = {} #Group each active LSF (job type + weekly hours) by student, so we can show all of a student's jobs together + totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() # Total allocated positions for this department/term, summed across all hour buckets + usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) # Count how many of those positions are currently filled (excludes contract/break-hour forms) + studentHours = {} # Group each active LSF (job type + weekly hours) by student, so we can show all of a student's jobs together for form in LaborStatusForm.select().where(LaborStatusForm.department == dept,LaborStatusForm.termCode_id == 202500,LaborStatusForm.contractHours.is_null(True) ): studentSuperviseeId = form.studentSupervisee_id @@ -139,13 +139,21 @@ def count_workers(job_type, hours_bucket): studentHours = studentHours, usedPositions = usedPositions, break_hours = sumBreak, - positions = positionsList, - posUrl = posUrl, - supervisors = supervisors, - laborCoordinators=laborCoordinators, - currentUser=g.currentUser ) +@main_bp.route('/department///managepositions', methods=['GET']) +def managePositions(org, account): + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except DoesNotExist: + return render_template('errors/404.html'), 404 + positions = Tracy().getPositionsFromDepartment(org, account) + print(positions) + return render_template('main/managepositions.html', + department = dept, + department_name = dept.DEPT_NAME, + positions = positions + ) @main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form diff --git a/app/static/css/departmentPortal.css b/app/static/css/departmentPortal.css index 6cccf556a..9937e0a99 100644 --- a/app/static/css/departmentPortal.css +++ b/app/static/css/departmentPortal.css @@ -42,11 +42,11 @@ } .primary-chart { font-size: 1.2em; - padding-left: 15%; + padding-left: 7%; } .secondary-chart { font-size: 1.2em; - padding-right: 15%; + padding-right: 7%; } .bi-people-fill { /* Bootstrap Icon for Members Card */ From 120426913fcf4e951b3b545c3fcd05c86da32522 Mon Sep 17 00:00:00 2001 From: conwelld Date: Tue, 21 Jul 2026 11:43:35 -0400 Subject: [PATCH 04/15] fixed useless code --- app/controllers/main_routes/main_routes.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 15ef9faf7..5d8d60b52 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -103,18 +103,7 @@ def departmentPortal(org=None,account=None): totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() # Total allocated positions for this department/term, summed across all hour buckets usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) # Count how many of those positions are currently filled (excludes contract/break-hour forms) - studentHours = {} # Group each active LSF (job type + weekly hours) by student, so we can show all of a student's jobs together - for form in LaborStatusForm.select().where(LaborStatusForm.department == dept,LaborStatusForm.termCode_id == 202500,LaborStatusForm.contractHours.is_null(True) - ): - studentSuperviseeId = form.studentSupervisee_id - if studentSuperviseeId not in studentHours: - studentHours[studentSuperviseeId] = [] - studentHours[studentSuperviseeId].append({ - "jobType": form.jobType, - "weeklyHours": form.weeklyHours - }) - - + def count_workers(job_type, hours_bucket): return LaborStatusForm.select().where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.jobType == job_type, LaborStatusForm.weeklyHours == hours_bucket, LaborStatusForm.contractHours.is_null(True)).count() @@ -136,7 +125,6 @@ def count_workers(job_type, hours_bucket): total_allocation = totalPositions, used_allocation = usedAllocation, term = g.openTerm.termName, - studentHours = studentHours, usedPositions = usedPositions, break_hours = sumBreak, ) From 0f166c268ab2d36f1bf369a5b11ecda37458d8e1 Mon Sep 17 00:00:00 2001 From: conwelld Date: Tue, 21 Jul 2026 12:01:04 -0400 Subject: [PATCH 05/15] fixed department code --- app/controllers/main_routes/main_routes.py | 27 ---------------------- 1 file changed, 27 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 5d8d60b52..340be409f 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -74,33 +74,6 @@ def departmentPortal(org=None,account=None): except DoesNotExist: allocation = None - supervisorDepartments = (SupervisorDepartment.select().join(Supervisor).where(SupervisorDepartment.department == dept) - .order_by(fn.COALESCE(Supervisor.preferred_name, Supervisor.legal_name, Supervisor.LAST_NAME).asc())) - - laborCoordinators = [] - supervisors = [] - - for supervisorDepartment in supervisorDepartments: - supervisor = supervisorDepartment.supervisor - - if supervisor is None: - continue - - firstName = supervisor.preferred_name or supervisor.legal_name or "" - lastName = supervisor.LAST_NAME or "" - - supervisorName = f"{firstName} {lastName}".strip() - - supervisorDisplay = { - "name": supervisorName, - "email": supervisor.EMAIL - } - - if supervisorDepartment.isCoordinator: - laborCoordinators.append(supervisorDisplay) - else: - supervisors.append(supervisorDisplay) - totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() # Total allocated positions for this department/term, summed across all hour buckets usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) # Count how many of those positions are currently filled (excludes contract/break-hour forms) From 6f5850a121c0c2bbea42505c12abd5e8630aadbb Mon Sep 17 00:00:00 2001 From: rukwashai <{rukwashai}@berea.edu> Date: Wed, 22 Jul 2026 10:14:03 -0400 Subject: [PATCH 06/15] We have change the hard coded part with variable able to be flexible everything touching the 202500 --- app/controllers/main_routes/main_routes.py | 15 +++++++++------ app/templates/main/departmentPortal.html | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 340be409f..e457970e2 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -55,6 +55,9 @@ def supervisorPortal(): @main_bp.route('/department/', methods=['GET']) @main_bp.route('/department//', methods=['GET']) def departmentPortal(org=None,account=None): + open_term = g.openTerm + term_code = open_term.termCode + if org and account: try: dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) @@ -70,15 +73,15 @@ def departmentPortal(org=None,account=None): else: departments = list(getDepartmentsForSupervisor(g.currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) try: - allocation = Allocation.select(Allocation, Term).join(Term).where(Allocation.department == dept, Allocation.termCode == 202500).get() + allocation = Allocation.select(Allocation, Term).join(Term).where(Allocation.department == dept, Allocation.termCode == term_code).get() except DoesNotExist: allocation = None - totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == 202500).scalar() # Total allocated positions for this department/term, summed across all hour buckets - usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(True))]) # Count how many of those positions are currently filled (excludes contract/break-hour forms) + totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == term_code).scalar() # Total allocated positions for this department/term, summed across all hour buckets + usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == term_code, LaborStatusForm.contractHours.is_null(True))]) # Count how many of those positions are currently filled (excludes contract/break-hour forms) def count_workers(job_type, hours_bucket): - return LaborStatusForm.select().where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.jobType == job_type, LaborStatusForm.weeklyHours == hours_bucket, LaborStatusForm.contractHours.is_null(True)).count() + return LaborStatusForm.select().where(LaborStatusForm.department == dept, LaborStatusForm.termCode == term_code, LaborStatusForm.jobType == job_type, LaborStatusForm.weeklyHours == hours_bucket, LaborStatusForm.contractHours.is_null(True)).count() usedPositions = { "used_10": count_workers("Primary", "10"), @@ -88,7 +91,7 @@ def count_workers(job_type, hours_bucket): "used_5_sec": count_workers("Secondary", "5"), "used_10_sec": count_workers("Secondary", "10"), } - break_allocation = LaborStatusForm.select(LaborStatusForm.contractHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == 202500, LaborStatusForm.contractHours.is_null(False)) + break_allocation = LaborStatusForm.select(LaborStatusForm.contractHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == term_code, LaborStatusForm.contractHours.is_null(False)) sumBreak = sum(form.contractHours or 0 for form in break_allocation) return render_template('main/departmentPortal.html', @@ -97,7 +100,7 @@ def count_workers(job_type, hours_bucket): allocation = allocation, total_allocation = totalPositions, used_allocation = usedAllocation, - term = g.openTerm.termName, + term = open_term, usedPositions = usedPositions, break_hours = sumBreak, ) diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index b51354f0e..6d17feae5 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -40,7 +40,7 @@

{% if department %} {{department.DEPT_NAME}} Portal {% e

Allocations

-

AY 2025-2026

{{used_allocation}}/{{total_allocation or 0}} Positions

+

{{ term.termName }}

{{used_allocation}}/{{total_allocation or 0}} Positions

    @@ -67,4 +67,4 @@

    Break Hours {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} From 813f398c440e34acf15c96f79cc3918cc2e671f2 Mon Sep 17 00:00:00 2001 From: rukwashai <{rukwashai}@berea.edu> Date: Wed, 22 Jul 2026 13:52:37 -0400 Subject: [PATCH 07/15] Temporary allocation logic will be replaced by the official shared service. See #657 but beyond that we created allo files --- app/controllers/main_routes/main_routes.py | 28 +++------ app/logic/allocation_utilization.py | 71 ++++++++++++++++++++++ database/reset_database.sh | 2 - 3 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 app/logic/allocation_utilization.py diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index e457970e2..ca45fa7c7 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -1,5 +1,5 @@ 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 flask_bootstrap import forms from functools import reduce import operator @@ -17,6 +17,7 @@ from app.login_manager import require_login, logout from app.logic.getTableData import getDatatableData from app.logic.banner import Banner +from app.logic.allocation_utilization import get_department_allocation_summary from app.models.allocation import Allocation from app.logic.tracy import Tracy from app.models.positionHistory import PositionHistory @@ -77,32 +78,17 @@ def departmentPortal(org=None,account=None): except DoesNotExist: allocation = None - totalPositions = Allocation.select(fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) + fn.SUM(Allocation.primary_15) + fn.SUM(Allocation.primary_20) + fn.sum(Allocation.secondary_5) + fn.SUM(Allocation.secondary_10)).where(Allocation.department == dept, Allocation.termCode == term_code).scalar() # Total allocated positions for this department/term, summed across all hour buckets - usedAllocation = len([hours for hours in LaborStatusForm.select(LaborStatusForm.weeklyHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == term_code, LaborStatusForm.contractHours.is_null(True))]) # Count how many of those positions are currently filled (excludes contract/break-hour forms) - - def count_workers(job_type, hours_bucket): - return LaborStatusForm.select().where(LaborStatusForm.department == dept, LaborStatusForm.termCode == term_code, LaborStatusForm.jobType == job_type, LaborStatusForm.weeklyHours == hours_bucket, LaborStatusForm.contractHours.is_null(True)).count() - - usedPositions = { - "used_10": count_workers("Primary", "10"), - "used_12": count_workers("Primary", "12"), - "used_15": count_workers("Primary", "15"), - "used_20": count_workers("Primary", "20"), - "used_5_sec": count_workers("Secondary", "5"), - "used_10_sec": count_workers("Secondary", "10"), -} - break_allocation = LaborStatusForm.select(LaborStatusForm.contractHours).where(LaborStatusForm.department == dept, LaborStatusForm.termCode == term_code, LaborStatusForm.contractHours.is_null(False)) - sumBreak = sum(form.contractHours or 0 for form in break_allocation) + allocation_summary = get_department_allocation_summary(dept, term_code) return render_template('main/departmentPortal.html', departments = departments, department = dept, allocation = allocation, - total_allocation = totalPositions, - used_allocation = usedAllocation, + total_allocation = allocation_summary["total_positions"], + used_allocation = allocation_summary["used_allocation"], term = open_term, - usedPositions = usedPositions, - break_hours = sumBreak, + usedPositions = allocation_summary["used_positions"], + break_hours = allocation_summary["break_hours"], ) @main_bp.route('/department///managepositions', methods=['GET']) def managePositions(org, account): diff --git a/app/logic/allocation_utilization.py b/app/logic/allocation_utilization.py new file mode 100644 index 000000000..77df38e73 --- /dev/null +++ b/app/logic/allocation_utilization.py @@ -0,0 +1,71 @@ +from peewee import fn + +from app.models.allocation import Allocation +from app.models.laborStatusForm import LaborStatusForm + + +def get_department_allocation_summary(department, term_code): + """Return allocation-utilization values for one department and term.""" + total_positions = ( + Allocation.select( + fn.SUM(Allocation.primary_10) + + fn.SUM(Allocation.primary_12) + + fn.SUM(Allocation.primary_15) + + fn.SUM(Allocation.primary_20) + + fn.SUM(Allocation.secondary_5) + + fn.SUM(Allocation.secondary_10) + ) + .where( + Allocation.department == department, + Allocation.termCode == term_code, + ) + .scalar() + ) + + used_allocation = ( + LaborStatusForm.select() + .where( + LaborStatusForm.department == department, + LaborStatusForm.termCode == term_code, + LaborStatusForm.contractHours.is_null(True), + ) + .count() + ) + + def count_workers(job_type, hours_bucket): + return ( + LaborStatusForm.select() + .where( + LaborStatusForm.department == department, + LaborStatusForm.termCode == term_code, + LaborStatusForm.jobType == job_type, + LaborStatusForm.weeklyHours == hours_bucket, + LaborStatusForm.contractHours.is_null(True), + ) + .count() + ) + + used_positions = { + "used_10": count_workers("Primary", 10), + "used_12": count_workers("Primary", 12), + "used_15": count_workers("Primary", 15), + "used_20": count_workers("Primary", 20), + "used_5_sec": count_workers("Secondary", 5), + "used_10_sec": count_workers("Secondary", 10), + } + + break_hours = sum( + form.contractHours or 0 + for form in LaborStatusForm.select(LaborStatusForm.contractHours).where( + LaborStatusForm.department == department, + LaborStatusForm.termCode == term_code, + LaborStatusForm.contractHours.is_null(False), + ) + ) + + return { + "total_positions": total_positions or 0, + "used_allocation": used_allocation, + "used_positions": used_positions, + "break_hours": break_hours, + } diff --git a/database/reset_database.sh b/database/reset_database.sh index 82f6cff53..ba87204db 100755 --- a/database/reset_database.sh +++ b/database/reset_database.sh @@ -29,8 +29,6 @@ echo "Recreating databases and users" mysql -u root -proot --execute="CREATE DATABASE IF NOT EXISTS \`lsf\`; CREATE USER IF NOT EXISTS 'lsf_user'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON *.* TO 'lsf_user'@'%';" mysql -u root -proot --execute="CREATE DATABASE IF NOT EXISTS \`UTE\`; CREATE USER IF NOT EXISTS 'tracy_user'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON *.* TO 'tracy_user'@'%';" -cd database - rm -rf lsf_migrations rm -rf tracy_migrations rm -rf migrations.json From 7584ffa12f6bfccb3a4d2fb762c0d6c527ed453e Mon Sep 17 00:00:00 2001 From: rukwashai Date: Mon, 27 Jul 2026 11:59:42 -0400 Subject: [PATCH 08/15] fix the review comment from Minran, all of them --- app/controllers/main_routes/main_routes.py | 44 ++++++++----------- ...tilization.py => allocationUtilization.py} | 32 ++++++++++++-- app/templates/main/departmentPortal.html | 4 +- database/base_data.py | 5 +++ database/demo_data.py | 5 +++ database/migrate_db.sh | 2 + database/migrate_db_tracy.sh | 2 + 7 files changed, 63 insertions(+), 31 deletions(-) rename app/logic/{allocation_utilization.py => allocationUtilization.py} (68%) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index ca45fa7c7..60ce9b65e 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -1,6 +1,5 @@ from flask import render_template, request, json, redirect, url_for, send_file, g, flash, jsonify from peewee import JOIN, DoesNotExist -from flask_bootstrap import forms from functools import reduce import operator from app.models.department import Department @@ -17,9 +16,8 @@ from app.login_manager import require_login, logout from app.logic.getTableData import getDatatableData from app.logic.banner import Banner -from app.logic.allocation_utilization import get_department_allocation_summary +from app.logic.allocationUtilization import getDepartmentAllocationSummary from app.models.allocation import Allocation -from app.logic.tracy import Tracy from app.models.positionHistory import PositionHistory @main_bp.route('/logout', methods=['GET']) @@ -56,37 +54,33 @@ def supervisorPortal(): @main_bp.route('/department/', methods=['GET']) @main_bp.route('/department//', methods=['GET']) def departmentPortal(org=None,account=None): - open_term = g.openTerm - term_code = open_term.termCode - - if org and account: - try: - dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) - except (NameError, DoesNotExist): - dept = None - else: + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): dept = None - - - if g.currentUser.isLaborAdmin: departments = list(Department.select().order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) else: departments = list(getDepartmentsForSupervisor(g.currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) - try: - allocation = Allocation.select(Allocation, Term).join(Term).where(Allocation.department == dept, Allocation.termCode == term_code).get() - except DoesNotExist: + + allocation_summary = getDepartmentAllocationSummary(dept) + recentTerm = allocation_summary["term"] + + if recentTerm: + try: + allocation = Allocation.select(Allocation, Term).join(Term).where(Allocation.department == dept, Allocation.termCode == recentTerm.termCode).get() + except DoesNotExist: + allocation = None + else: allocation = None - - allocation_summary = get_department_allocation_summary(dept, term_code) - - return render_template('main/departmentPortal.html', + + return render_template('main/departmentPortal.html', departments = departments, department = dept, allocation = allocation, - total_allocation = allocation_summary["total_positions"], - used_allocation = allocation_summary["used_allocation"], - term = open_term, + allocated = allocation_summary["allocated"], + used = allocation_summary["used"], + term = recentTerm, usedPositions = allocation_summary["used_positions"], break_hours = allocation_summary["break_hours"], ) diff --git a/app/logic/allocation_utilization.py b/app/logic/allocationUtilization.py similarity index 68% rename from app/logic/allocation_utilization.py rename to app/logic/allocationUtilization.py index 77df38e73..5fc14abfc 100644 --- a/app/logic/allocation_utilization.py +++ b/app/logic/allocationUtilization.py @@ -2,10 +2,33 @@ from app.models.allocation import Allocation from app.models.laborStatusForm import LaborStatusForm +from app.models.term import Term -def get_department_allocation_summary(department, term_code): - """Return allocation-utilization values for one department and term.""" +def getDepartmentAllocationSummary(department): + """Return allocation-utilization values for a department's most recent term.""" + departmentAllocations = list( + Allocation.select(Allocation, Term).join(Term).where(Allocation.department == department) + ) + if not departmentAllocations: + return { + "term": None, + "allocated": 0, + "used": 0, + "used_positions": { + "used_10": 0, + "used_12": 0, + "used_15": 0, + "used_20": 0, + "used_5_sec": 0, + "used_10_sec": 0, + }, + "break_hours": 0, + } + + recentTerm = Term.order_by_term([a.termCode for a in departmentAllocations], reverse=True)[0] + term_code = recentTerm.termCode + total_positions = ( Allocation.select( fn.SUM(Allocation.primary_10) @@ -64,8 +87,9 @@ def count_workers(job_type, hours_bucket): ) return { - "total_positions": total_positions or 0, - "used_allocation": used_allocation, + "term": recentTerm, + "allocated": total_positions or 0, + "used": used_allocation, "used_positions": used_positions, "break_hours": break_hours, } diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index 6d17feae5..0fa624887 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -37,10 +37,10 @@

    {% if department %} {{department.DEPT_NAME}} Portal {% e

-

Allocations

+

Allocations

-

{{ term.termName }}

{{used_allocation}}/{{total_allocation or 0}} Positions

+

AY 2025-2026

{{used}}/{{allocated or 0}} Positions

    diff --git a/database/base_data.py b/database/base_data.py index c081fb752..f42d57ce9 100644 --- a/database/base_data.py +++ b/database/base_data.py @@ -1,3 +1,8 @@ +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + from app.models.status import Status from app.models.historyType import HistoryType from app.models.emailTemplate import EmailTemplate diff --git a/database/demo_data.py b/database/demo_data.py index 3bce122a8..d231963ab 100644 --- a/database/demo_data.py +++ b/database/demo_data.py @@ -2,6 +2,11 @@ Chech phpmyadmin to see if your changes are reflected This file will need to be changed if the format of models changes (new fields, dropping fields, renaming...)''' +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + from app import app from app.models.Tracy import db diff --git a/database/migrate_db.sh b/database/migrate_db.sh index dea226d8e..c693c7086 100755 --- a/database/migrate_db.sh +++ b/database/migrate_db.sh @@ -1,4 +1,6 @@ +export PYTHONPATH="$(cd "$(dirname "$0")/.." && pwd):$PYTHONPATH" + pem init # See: https://stackoverflow.com/questions/394230/how-to-detect-the-os-from-a-bash-script/18434831 diff --git a/database/migrate_db_tracy.sh b/database/migrate_db_tracy.sh index 0b5466dc8..fabca9d30 100755 --- a/database/migrate_db_tracy.sh +++ b/database/migrate_db_tracy.sh @@ -1,4 +1,6 @@ +export FLASK_APP="$(cd "$(dirname "$0")/.." && pwd)/app.py" + DB_DIR=tracy_migrations flask db init -d $DB_DIR From 20b424cc04434031b48b3a03ea4b9c72e83a6fc9 Mon Sep 17 00:00:00 2001 From: rukwashai Date: Wed, 29 Jul 2026 14:42:20 -0400 Subject: [PATCH 09/15] Fix import after allocationUtilization rename to getAllocation, add integration tests --- app/controllers/main_routes/main_routes.py | 7 +- ...ocationUtilization.py => getAllocation.py} | 0 tests/code/test_getAllocation.py | 208 ++++++++++++++++++ 3 files changed, 212 insertions(+), 3 deletions(-) rename app/logic/{allocationUtilization.py => getAllocation.py} (100%) create mode 100644 tests/code/test_getAllocation.py diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 40f53aa14..ae9a4d49d 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -9,6 +9,8 @@ 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.models.positionHistory import PositionHistory from app.controllers.admin_routes.allPendingForms import checkAdjustment from app.controllers.main_routes import main_bp from app.logic.download import CSVMaker, saveFormSearchResult, retrieveFormSearchResult @@ -16,9 +18,8 @@ from app.login_manager import require_login, logout from app.logic.getTableData import getDatatableData from app.logic.banner import Banner -from app.logic.allocationUtilization import getDepartmentAllocationSummary -from app.models.allocation import Allocation -from app.models.positionHistory import PositionHistory +from app.logic.getAllocation import getDepartmentAllocationSummary + from app.logic.getPositions import getActivePositions @main_bp.route('/logout', methods=['GET']) diff --git a/app/logic/allocationUtilization.py b/app/logic/getAllocation.py similarity index 100% rename from app/logic/allocationUtilization.py rename to app/logic/getAllocation.py diff --git a/tests/code/test_getAllocation.py b/tests/code/test_getAllocation.py new file mode 100644 index 000000000..fb8c6d51a --- /dev/null +++ b/tests/code/test_getAllocation.py @@ -0,0 +1,208 @@ +import pytest +from app.models import mainDB +from app.models.department import Department +from app.models.term import Term +from app.models.allocation import Allocation +from app.models.laborStatusForm import LaborStatusForm +from app.models.student import Student +from app.models.supervisor import Supervisor +from app.logic.getAllocation import getDepartmentAllocationSummary + + +@pytest.mark.integration +def test_getDepartmentAllocationSummary_no_allocation(): + """ + Test that a department with no Allocation rows gets a zeroed-out summary + with term=None, instead of an error. + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=200, DEPT_NAME="Physics", ACCOUNT="6750", ORG="2120", isActive=True) + + summary = getDepartmentAllocationSummary(dept) + + assert summary["term"] is None + assert summary["allocated"] == 0 + assert summary["used"] == 0 + assert summary["break_hours"] == 0 + assert summary["used_positions"] == { + "used_10": 0, + "used_12": 0, + "used_15": 0, + "used_20": 0, + "used_5_sec": 0, + "used_10_sec": 0, + } + + transaction.rollback() + + +@pytest.mark.integration +def test_getDepartmentAllocationSummary_uses_most_recent_term(): + """ + Test that when a department has allocations across multiple terms, the + summary reflects only the most recent term's data. + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=201, DEPT_NAME="Chemistry", ACCOUNT="6751", ORG="2121", isActive=True) + + oldTerm = Term.create(termCode=900000, termName="AY Test Old") + newTerm = Term.create(termCode=900100, termName="AY Test New") + + Allocation.create( + termCode=oldTerm, department=dept, isFinal=True, justification="old", + primary_10=1, primary_12=0, primary_15=0, primary_20=0, + secondary_5=0, secondary_10=0, breakHours=50, + ) + Allocation.create( + termCode=newTerm, department=dept, isFinal=True, justification="new", + primary_10=2, primary_12=3, primary_15=0, primary_20=0, + secondary_5=1, secondary_10=0, breakHours=100, + ) + + supervisor = Supervisor.create(ID="SUP001", isActive=True) + student = Student.create(ID="STU001", isActive=True) + + # Under the OLD term - should be excluded from the summary + LaborStatusForm.create( + termCode=oldTerm, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Old Job", POSN_CODE="S001", + weeklyHours=10, contractHours=None, + ) + # Under the NEW (most recent) term - should be counted + LaborStatusForm.create( + termCode=newTerm, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="New Job", POSN_CODE="S002", + weeklyHours=10, contractHours=None, + ) + + summary = getDepartmentAllocationSummary(dept) + + assert summary["term"].termCode == 900100 + assert summary["allocated"] == 6 # 2 + 3 + 0 + 0 + 1 + 0, from the new term only + assert summary["used"] == 1 # only the new term's LaborStatusForm counts + assert summary["used_positions"]["used_10"] == 1 + assert summary["break_hours"] == 0 + + transaction.rollback() + + +@pytest.mark.integration +def test_getDepartmentAllocationSummary_break_hours(): + """ + Test that break_hours only sums forms with contractHours set (break-term + contracts), and that those forms are excluded from the weekly "used" count. + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=202, DEPT_NAME="Biology", ACCOUNT="6752", ORG="2122", isActive=True) + term = Term.create(termCode=900200, termName="AY Test Break") + + Allocation.create( + termCode=term, department=dept, isFinal=True, justification="test", + primary_10=1, primary_12=0, primary_15=0, primary_20=0, + secondary_5=0, secondary_10=0, breakHours=200, + ) + + supervisor = Supervisor.create(ID="SUP002", isActive=True) + student = Student.create(ID="STU002", isActive=True) + + LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Break Worker", POSN_CODE="S003", + weeklyHours=None, contractHours=40, + ) + + summary = getDepartmentAllocationSummary(dept) + + assert summary["break_hours"] == 40 + assert summary["used"] == 0 + + transaction.rollback() + + +@pytest.mark.integration +def test_getDepartmentAllocationSummary_department_none(): + """ + Test that passing department=None (e.g. when Department.get() fails in + the departmentPortal route) returns the zeroed-out fallback instead of + raising an error. + """ + summary = getDepartmentAllocationSummary(None) + + assert summary["term"] is None + assert summary["allocated"] == 0 + assert summary["used"] == 0 + assert summary["break_hours"] == 0 + assert summary["used_positions"] == { + "used_10": 0, + "used_12": 0, + "used_15": 0, + "used_20": 0, + "used_5_sec": 0, + "used_10_sec": 0, + } + + +@pytest.mark.integration +def test_getDepartmentAllocationSummary_multiple_allocations_same_term(): + """ + Test that if a department has more than one Allocation row for the same + most-recent term (e.g. a draft and a final revision, which the model's + (termCode, department, isFinal) index allows), the totals sum across + both rows rather than picking just one. + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=203, DEPT_NAME="Mathematics", ACCOUNT="6753", ORG="2123", isActive=True) + term = Term.create(termCode=900300, termName="AY Test Multi") + + Allocation.create( + termCode=term, department=dept, isFinal=False, justification="draft", + primary_10=1, primary_12=0, primary_15=0, primary_20=0, + secondary_5=0, secondary_10=0, breakHours=10, + ) + Allocation.create( + termCode=term, department=dept, isFinal=True, justification="final", + primary_10=2, primary_12=0, primary_15=0, primary_20=0, + secondary_5=0, secondary_10=0, breakHours=20, + ) + + summary = getDepartmentAllocationSummary(dept) + + assert summary["term"].termCode == 900300 + assert summary["allocated"] == 3 # 1 + 2, summed across both rows + + transaction.rollback() + + +@pytest.mark.integration +def test_getDepartmentAllocationSummary_allocation_no_labor_status_forms(): + """ + Test that a department with an allocation for the most recent term but no + LaborStatusForm records at all shows allocated > 0 with used/break_hours + at 0, rather than erroring on an empty result set. + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=204, DEPT_NAME="History", ACCOUNT="6754", ORG="2124", isActive=True) + term = Term.create(termCode=900400, termName="AY Test Empty") + + Allocation.create( + termCode=term, department=dept, isFinal=True, justification="test", + primary_10=3, primary_12=2, primary_15=0, primary_20=0, + secondary_5=1, secondary_10=0, breakHours=150, + ) + + summary = getDepartmentAllocationSummary(dept) + + assert summary["term"].termCode == 900400 + assert summary["allocated"] == 6 + assert summary["used"] == 0 + assert summary["break_hours"] == 0 + assert summary["used_positions"] == { + "used_10": 0, + "used_12": 0, + "used_15": 0, + "used_20": 0, + "used_5_sec": 0, + "used_10_sec": 0, + } + + transaction.rollback() From ee0e066beba6f1508c9dbb5d5444f7df224324dd Mon Sep 17 00:00:00 2001 From: rukwashai Date: Thu, 30 Jul 2026 09:34:58 -0400 Subject: [PATCH 10/15] Address PR review comments: consolidate getAllocation return dict, extract countWorkers/getBreakHours with FormHistory approval filter, dynamic term in departmentPortal.html, and revert environment-specific path edits in base_data.py/demo_data.py. Add integration test coverage for the new logic functions. --- app/logic/getAllocation.py | 108 +++++++++-------- app/templates/main/departmentPortal.html | 2 +- database/base_data.py | 5 - database/demo_data.py | 10 -- tests/code/test_getAllocation.py | 141 ++++++++++++++++++++++- 5 files changed, 197 insertions(+), 69 deletions(-) diff --git a/app/logic/getAllocation.py b/app/logic/getAllocation.py index 5fc14abfc..84e6e7389 100644 --- a/app/logic/getAllocation.py +++ b/app/logic/getAllocation.py @@ -3,31 +3,65 @@ from app.models.allocation import Allocation from app.models.laborStatusForm import LaborStatusForm from app.models.term import Term +from app.models.formHistory import FormHistory + + +def countWorkers(department, term_code, job_type, hours_bucket): + workerCount = ( + LaborStatusForm.select() + .where( + LaborStatusForm.department == department, + LaborStatusForm.termCode == term_code, + LaborStatusForm.jobType == job_type, + LaborStatusForm.weeklyHours == hours_bucket, + LaborStatusForm.contractHours.is_null(True), + ) + .count() + ) + return workerCount + + +def getBreakHours(department, term_code): + breakHoursTotal = ( + LaborStatusForm.select(fn.SUM(LaborStatusForm.contractHours)) + .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) + .where( + LaborStatusForm.department == department, + LaborStatusForm.termCode == term_code, + FormHistory.historyType == "Labor Status Form", + FormHistory.status == "Approved", + ) + .scalar() + ) or 0 + return breakHoursTotal def getDepartmentAllocationSummary(department): """Return allocation-utilization values for a department's most recent term.""" + result = { + "term": None, + "allocated": 0, + "used": 0, + "used_positions": { + "used_10": 0, + "used_12": 0, + "used_15": 0, + "used_20": 0, + "used_5_sec": 0, + "used_10_sec": 0, + }, + "break_hours": 0, + } + departmentAllocations = list( Allocation.select(Allocation, Term).join(Term).where(Allocation.department == department) ) if not departmentAllocations: - return { - "term": None, - "allocated": 0, - "used": 0, - "used_positions": { - "used_10": 0, - "used_12": 0, - "used_15": 0, - "used_20": 0, - "used_5_sec": 0, - "used_10_sec": 0, - }, - "break_hours": 0, - } + return result recentTerm = Term.order_by_term([a.termCode for a in departmentAllocations], reverse=True)[0] term_code = recentTerm.termCode + result["term"] = recentTerm total_positions = ( Allocation.select( @@ -44,6 +78,7 @@ def getDepartmentAllocationSummary(department): ) .scalar() ) + result["allocated"] = total_positions or 0 used_allocation = ( LaborStatusForm.select() @@ -54,42 +89,17 @@ def getDepartmentAllocationSummary(department): ) .count() ) + result["used"] = used_allocation - def count_workers(job_type, hours_bucket): - return ( - LaborStatusForm.select() - .where( - LaborStatusForm.department == department, - LaborStatusForm.termCode == term_code, - LaborStatusForm.jobType == job_type, - LaborStatusForm.weeklyHours == hours_bucket, - LaborStatusForm.contractHours.is_null(True), - ) - .count() - ) - - used_positions = { - "used_10": count_workers("Primary", 10), - "used_12": count_workers("Primary", 12), - "used_15": count_workers("Primary", 15), - "used_20": count_workers("Primary", 20), - "used_5_sec": count_workers("Secondary", 5), - "used_10_sec": count_workers("Secondary", 10), + result["used_positions"] = { + "used_10": countWorkers(department, term_code, "Primary", 10), + "used_12": countWorkers(department, term_code, "Primary", 12), + "used_15": countWorkers(department, term_code, "Primary", 15), + "used_20": countWorkers(department, term_code, "Primary", 20), + "used_5_sec": countWorkers(department, term_code, "Secondary", 5), + "used_10_sec": countWorkers(department, term_code, "Secondary", 10), } - break_hours = sum( - form.contractHours or 0 - for form in LaborStatusForm.select(LaborStatusForm.contractHours).where( - LaborStatusForm.department == department, - LaborStatusForm.termCode == term_code, - LaborStatusForm.contractHours.is_null(False), - ) - ) + result["break_hours"] = getBreakHours(department, term_code) - return { - "term": recentTerm, - "allocated": total_positions or 0, - "used": used_allocation, - "used_positions": used_positions, - "break_hours": break_hours, - } + return result diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index 4cb45ee2a..c3d6eae2c 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -40,7 +40,7 @@

    {% if department %} {{department.DEPT_NAME}} Portal {% e

    Allocations

-

AY 2025-2026

{{used}}/{{allocated or 0}} Positions

+

{{ term.termName if term else "No term data" }}

{{used}}/{{allocated or 0}} Positions

    diff --git a/database/base_data.py b/database/base_data.py index f42d57ce9..c081fb752 100644 --- a/database/base_data.py +++ b/database/base_data.py @@ -1,8 +1,3 @@ -import os -import sys - -sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) - from app.models.status import Status from app.models.historyType import HistoryType from app.models.emailTemplate import EmailTemplate diff --git a/database/demo_data.py b/database/demo_data.py index ab2d0db27..08784e51c 100644 --- a/database/demo_data.py +++ b/database/demo_data.py @@ -1,14 +1,4 @@ -'''Add new fields to this file and run it to add new enteries into your local database. -Chech phpmyadmin to see if your changes are reflected -This file will need to be changed if the format of models changes (new fields, dropping fields, renaming...)''' - -import os -import sys - -sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) - from app import app - from app.models.Tracy import db from app.models.Tracy.studata import STUDATA from app.models.Tracy.stuposn import STUPOSN diff --git a/tests/code/test_getAllocation.py b/tests/code/test_getAllocation.py index fb8c6d51a..6ac0fc246 100644 --- a/tests/code/test_getAllocation.py +++ b/tests/code/test_getAllocation.py @@ -1,3 +1,5 @@ +from datetime import date + import pytest from app.models import mainDB from app.models.department import Department @@ -6,7 +8,26 @@ from app.models.laborStatusForm import LaborStatusForm from app.models.student import Student from app.models.supervisor import Supervisor -from app.logic.getAllocation import getDepartmentAllocationSummary +from app.models.formHistory import FormHistory +from app.models.historyType import HistoryType +from app.models.status import Status +from app.models.user import User +from app.logic.getAllocation import getDepartmentAllocationSummary, countWorkers, getBreakHours + + +def _createFormHistory(form, statusName): + """Attach a FormHistory row to a LaborStatusForm, since getBreakHours now + only counts forms with an approved "Labor Status Form" history entry.""" + user = User.create(username=f"testuser_{form.laborStatusFormID}") + historyType = HistoryType.get(HistoryType.historyTypeName == "Labor Status Form") + status = Status.get(Status.statusName == statusName) + return FormHistory.create( + formID=form, + historyType=historyType, + createdBy=user, + createdDate=date.today(), + status=status, + ) @pytest.mark.integration @@ -89,8 +110,9 @@ def test_getDepartmentAllocationSummary_uses_most_recent_term(): @pytest.mark.integration def test_getDepartmentAllocationSummary_break_hours(): """ - Test that break_hours only sums forms with contractHours set (break-term - contracts), and that those forms are excluded from the weekly "used" count. + Test that break_hours only sums approved forms with contractHours set + (break-term contracts), and that those forms are excluded from the + weekly "used" count. """ with mainDB.atomic() as transaction: dept = Department.create(departmentID=202, DEPT_NAME="Biology", ACCOUNT="6752", ORG="2122", isActive=True) @@ -105,11 +127,12 @@ def test_getDepartmentAllocationSummary_break_hours(): supervisor = Supervisor.create(ID="SUP002", isActive=True) student = Student.create(ID="STU002", isActive=True) - LaborStatusForm.create( + breakForm = LaborStatusForm.create( termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Primary", WLS="10", POSN_TITLE="Break Worker", POSN_CODE="S003", weeklyHours=None, contractHours=40, ) + _createFormHistory(breakForm, "Approved") summary = getDepartmentAllocationSummary(dept) @@ -206,3 +229,113 @@ def test_getDepartmentAllocationSummary_allocation_no_labor_status_forms(): } transaction.rollback() + + +@pytest.mark.integration +def test_countWorkers(): + """ + Test that countWorkers only counts LaborStatusForm rows matching the + given department, term, job type, and weekly-hours bucket, and excludes + forms with a different job type/hours bucket or a break-term contract + (contractHours set instead of weeklyHours). + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=205, DEPT_NAME="English", ACCOUNT="6755", ORG="2125", isActive=True) + term = Term.create(termCode=900500, termName="AY Test Workers") + + supervisor = Supervisor.create(ID="SUP003", isActive=True) + student = Student.create(ID="STU003", isActive=True) + + # Matches department, term, job type, and hours bucket - should count + LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Match", POSN_CODE="S010", + weeklyHours=10, contractHours=None, + ) + # Different job type - should not count toward ("Primary", 10) + LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Secondary", WLS="10", POSN_TITLE="Wrong Job Type", POSN_CODE="S011", + weeklyHours=10, contractHours=None, + ) + # Different hours bucket - should not count toward ("Primary", 10) + LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="12", POSN_TITLE="Wrong Hours", POSN_CODE="S012", + weeklyHours=12, contractHours=None, + ) + # Break-term contract (contractHours set) - should not count even though + # job type and weeklyHours otherwise match + LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Break Contract", POSN_CODE="S013", + weeklyHours=10, contractHours=40, + ) + + assert countWorkers(dept, term.termCode, "Primary", 10) == 1 + assert countWorkers(dept, term.termCode, "Secondary", 10) == 1 + assert countWorkers(dept, term.termCode, "Primary", 12) == 1 + assert countWorkers(dept, term.termCode, "Primary", 15) == 0 + + transaction.rollback() + + +@pytest.mark.integration +def test_getBreakHours(): + """ + Test that getBreakHours sums only APPROVED forms with contractHours set + (break-term contracts) for the given department and term, excludes + weekly-hours forms, excludes forms under a different term, and excludes + forms that are not approved (e.g. still pending). + """ + with mainDB.atomic() as transaction: + dept = Department.create(departmentID=206, DEPT_NAME="Philosophy", ACCOUNT="6756", ORG="2126", isActive=True) + term = Term.create(termCode=900600, termName="AY Test Break Hours") + otherTerm = Term.create(termCode=900601, termName="AY Test Other Term") + + supervisor = Supervisor.create(ID="SUP004", isActive=True) + student = Student.create(ID="STU004", isActive=True) + + # Approved break-term contracts under the target term - should be summed + formA = LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Break A", POSN_CODE="S020", + weeklyHours=None, contractHours=40, + ) + _createFormHistory(formA, "Approved") + + formB = LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Secondary", WLS="5", POSN_TITLE="Break B", POSN_CODE="S021", + weeklyHours=None, contractHours=60, + ) + _createFormHistory(formB, "Approved") + + # Weekly-hours form (contractHours=None) - should be excluded regardless + formC = LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Weekly Job", POSN_CODE="S022", + weeklyHours=10, contractHours=None, + ) + _createFormHistory(formC, "Approved") + + # Break-term contract under a DIFFERENT term - should be excluded + formD = LaborStatusForm.create( + termCode=otherTerm, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Break Other Term", POSN_CODE="S023", + weeklyHours=None, contractHours=100, + ) + _createFormHistory(formD, "Approved") + + # Break-term contract that is still PENDING - should be excluded + formE = LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Break Pending", POSN_CODE="S024", + weeklyHours=None, contractHours=999, + ) + _createFormHistory(formE, "Pending") + + assert getBreakHours(dept, term.termCode) == 100 # 40 + 60, excludes the pending form + assert getBreakHours(dept, otherTerm.termCode) == 100 + + transaction.rollback() From 8618b68d1a74bc53fe883111c8dd4d3c6306eb19 Mon Sep 17 00:00:00 2001 From: rukwashai Date: Thu, 30 Jul 2026 11:12:53 -0400 Subject: [PATCH 11/15] Adopt approval-status filtering from UsedAllocFunction branch in getAllocation.py, excluding denied forms from countWorkers and the used count. Update tests to cover the new behavior. --- app/logic/getAllocation.py | 6 ++++++ tests/code/test_getAllocation.py | 30 +++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/logic/getAllocation.py b/app/logic/getAllocation.py index 84e6e7389..893da61bc 100644 --- a/app/logic/getAllocation.py +++ b/app/logic/getAllocation.py @@ -9,12 +9,15 @@ def countWorkers(department, term_code, job_type, hours_bucket): workerCount = ( LaborStatusForm.select() + .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) .where( LaborStatusForm.department == department, LaborStatusForm.termCode == term_code, LaborStatusForm.jobType == job_type, LaborStatusForm.weeklyHours == hours_bucket, LaborStatusForm.contractHours.is_null(True), + FormHistory.historyType == "Labor Status Form", + ~(FormHistory.status % "Denied%"), ) .count() ) @@ -82,10 +85,13 @@ def getDepartmentAllocationSummary(department): used_allocation = ( LaborStatusForm.select() + .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) .where( LaborStatusForm.department == department, LaborStatusForm.termCode == term_code, LaborStatusForm.contractHours.is_null(True), + FormHistory.historyType == "Labor Status Form", + ~(FormHistory.status % "Denied%"), ) .count() ) diff --git a/tests/code/test_getAllocation.py b/tests/code/test_getAllocation.py index 6ac0fc246..3d9259066 100644 --- a/tests/code/test_getAllocation.py +++ b/tests/code/test_getAllocation.py @@ -90,11 +90,12 @@ def test_getDepartmentAllocationSummary_uses_most_recent_term(): weeklyHours=10, contractHours=None, ) # Under the NEW (most recent) term - should be counted - LaborStatusForm.create( + newForm = LaborStatusForm.create( termCode=newTerm, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Primary", WLS="10", POSN_TITLE="New Job", POSN_CODE="S002", weeklyHours=10, contractHours=None, ) + _createFormHistory(newForm, "Approved") summary = getDepartmentAllocationSummary(dept) @@ -236,8 +237,8 @@ def test_countWorkers(): """ Test that countWorkers only counts LaborStatusForm rows matching the given department, term, job type, and weekly-hours bucket, and excludes - forms with a different job type/hours bucket or a break-term contract - (contractHours set instead of weeklyHours). + forms with a different job type/hours bucket, a break-term contract + (contractHours set instead of weeklyHours), or a denied history status. """ with mainDB.atomic() as transaction: dept = Department.create(departmentID=205, DEPT_NAME="English", ACCOUNT="6755", ORG="2125", isActive=True) @@ -247,30 +248,45 @@ def test_countWorkers(): student = Student.create(ID="STU003", isActive=True) # Matches department, term, job type, and hours bucket - should count - LaborStatusForm.create( + matchForm = LaborStatusForm.create( termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Primary", WLS="10", POSN_TITLE="Match", POSN_CODE="S010", weeklyHours=10, contractHours=None, ) + _createFormHistory(matchForm, "Approved") + # Different job type - should not count toward ("Primary", 10) - LaborStatusForm.create( + wrongJobTypeForm = LaborStatusForm.create( termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Secondary", WLS="10", POSN_TITLE="Wrong Job Type", POSN_CODE="S011", weeklyHours=10, contractHours=None, ) + _createFormHistory(wrongJobTypeForm, "Approved") + # Different hours bucket - should not count toward ("Primary", 10) - LaborStatusForm.create( + wrongHoursForm = LaborStatusForm.create( termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Primary", WLS="12", POSN_TITLE="Wrong Hours", POSN_CODE="S012", weeklyHours=12, contractHours=None, ) + _createFormHistory(wrongHoursForm, "Approved") + # Break-term contract (contractHours set) - should not count even though # job type and weeklyHours otherwise match - LaborStatusForm.create( + breakContractForm = LaborStatusForm.create( termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Primary", WLS="10", POSN_TITLE="Break Contract", POSN_CODE="S013", weeklyHours=10, contractHours=40, ) + _createFormHistory(breakContractForm, "Approved") + + # Matches everything but was DENIED - should not count + deniedForm = LaborStatusForm.create( + termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + jobType="Primary", WLS="10", POSN_TITLE="Denied Match", POSN_CODE="S014", + weeklyHours=10, contractHours=None, + ) + _createFormHistory(deniedForm, "Denied by Admin") assert countWorkers(dept, term.termCode, "Primary", 10) == 1 assert countWorkers(dept, term.termCode, "Secondary", 10) == 1 From a0ba238cfdde9199ab6f075e7f7d672c26d0f1aa Mon Sep 17 00:00:00 2001 From: munsakad Date: Mon, 3 Aug 2026 11:19:36 -0400 Subject: [PATCH 12/15] Polish the allocation card: dynamic Fall/Spring term label, "X of Y" wording, and aligned Primary/Secondary table Replace the raw "AY 2025-2026" term name with a computed current-semester label (e.g. "Fall 2025") derived from the term's own year and today's month, matching the Fall/Spring termCode convention used elsewhere. Reword ratios as "X of Y" instead of "X/Y", rename the card title to "Current Allocation", and rebuild the Primary/Secondary breakdown as a single table so every row (term info, headers, hour buckets, break hours) shares the same column alignment and stays legible down to mobile widths. --- app/controllers/main_routes/main_routes.py | 1 + app/logic/getAllocation.py | 19 ++++++++ app/static/css/departmentPortal.css | 53 ++++++++++++++-------- app/templates/main/departmentPortal.html | 52 +++++++++++++-------- tests/code/test_getAllocation.py | 25 +++++++++- 5 files changed, 113 insertions(+), 37 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 442d39722..5d9d160dd 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -91,6 +91,7 @@ def departmentPortal(org=None,account=None): allocated = allocation_summary["allocated"], used = allocation_summary["used"], term = recentTerm, + currentSemester = allocation_summary["current_semester"], usedPositions = allocation_summary["used_positions"], break_hours = allocation_summary["break_hours"], supervisors = supervisors, diff --git a/app/logic/getAllocation.py b/app/logic/getAllocation.py index 893da61bc..f5d5d281f 100644 --- a/app/logic/getAllocation.py +++ b/app/logic/getAllocation.py @@ -1,3 +1,5 @@ +from datetime import date + from peewee import fn from app.models.allocation import Allocation @@ -6,6 +8,21 @@ from app.models.formHistory import FormHistory +def getCurrentSemesterLabel(term): + """Return the current Fall/Spring semester label (e.g. "Fall 2025") for + the academic year that the given term belongs to. The season is picked + from today's month and the year comes from the term's own termCode, + following the AY/Fall/Spring termCode convention in termManagement.py + (AY code, code+11 = Fall of that year, code+12 = Spring of the next). + """ + if not term: + return None + academicYear = int(str(term.termCode)[:4]) + if date.today().month >= 8: + return f"Fall {academicYear}" + return f"Spring {academicYear + 1}" + + def countWorkers(department, term_code, job_type, hours_bucket): workerCount = ( LaborStatusForm.select() @@ -43,6 +60,7 @@ def getDepartmentAllocationSummary(department): """Return allocation-utilization values for a department's most recent term.""" result = { "term": None, + "current_semester": None, "allocated": 0, "used": 0, "used_positions": { @@ -65,6 +83,7 @@ def getDepartmentAllocationSummary(department): recentTerm = Term.order_by_term([a.termCode for a in departmentAllocations], reverse=True)[0] term_code = recentTerm.termCode result["term"] = recentTerm + result["current_semester"] = getCurrentSemesterLabel(recentTerm) total_positions = ( Allocation.select( diff --git a/app/static/css/departmentPortal.css b/app/static/css/departmentPortal.css index 9904ae8bc..e0b15ed9d 100644 --- a/app/static/css/departmentPortal.css +++ b/app/static/css/departmentPortal.css @@ -43,27 +43,31 @@ vertical-align: middle; color:#6e6e6e; } -.header-container { - display: flex; - justify-content: space-between; - align-items: center; - padding-left: 2%; +.allocation-table-wrapper { + overflow-x: auto; + margin: 10px 0; +} +.allocation-table { + width: 100%; + border-collapse: collapse; + font-size: 1.2em; +} +.allocation-table th, +.allocation-table td { + text-align: left; + white-space: nowrap; +} +.allocation-table th:first-child, +.allocation-table td:first-child { padding-right: 10px; } -.allocation-list { - display: flex; - justify-content: space-between; - align-items: center; - padding-left: 10px; - padding-right: 10px; -} -.primary-chart { - font-size: 1.2em; - padding-left: 7%; +.allocation-table th { + font-weight: 700; + padding-top: 10px; } -.secondary-chart { - font-size: 1.2em; - padding-right: 7%; +.allocation-table .term-row h4, +.allocation-table .break-row h4 { + margin: 10px 0; } .bi-people-fill { /* Bootstrap Icon for Members Card */ @@ -96,3 +100,16 @@ flex-direction: column; } } + +@media (min-width: 1200px) and (max-width: 1450px), (max-width: 480px) { + .allocation-table { + font-size: 1em; + } + .allocation-table td { + padding: 2px 5px; + } + .allocation-table .term-row h4, + .allocation-table .break-row h4 { + font-size: 0.9rem; + } +} diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index 3e37d3b25..c800784c3 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -42,25 +42,41 @@

    {% if department %} {{department.DEPT_NAME}} Portal {% e

-

Allocations

+

Current Allocation

-
-

{{ term.termName if term else "No term data" }}

{{used}}/{{allocated or 0}} Positions

-
-
-
    -
  • 10 Hour - {{usedPositions.used_10}}/{{allocation.primary_10}}
  • -
  • 12 Hour - {{usedPositions.used_12}}/{{allocation.primary_12}}
  • -
  • 15 Hour - {{usedPositions.used_15}}/{{allocation.primary_15}}
  • -
  • 20 Hour - {{usedPositions.used_20}}/{{allocation.primary_20}}
  • -
-
    -
  • 5 Hour - {{usedPositions.used_5_sec}}/{{allocation.secondary_5}}
  • -
  • 10 Hour - {{usedPositions.used_10_sec}}/{{allocation.secondary_10}}
  • -
-
-
-

Break Hours

{{break_hours}}/{{allocation.breakHours or 0}} Hours

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

{{ currentSemester if currentSemester else "No term data" }}

{{used}} of {{allocated or 0}} Positions

PrimarySecondary
10 Hour - {{usedPositions.used_10}} of {{allocation.primary_10}}5 Hour - {{usedPositions.used_5_sec}} of {{allocation.secondary_5}}
12 Hour - {{usedPositions.used_12}} of {{allocation.primary_12}}10 Hour - {{usedPositions.used_10_sec}} of {{allocation.secondary_10}}
15 Hour - {{usedPositions.used_15}} of {{allocation.primary_15}}
20 Hour - {{usedPositions.used_20}} of {{allocation.primary_20}}

Break Hours

{{break_hours}} of {{allocation.breakHours or 0}} Hours

diff --git a/tests/code/test_getAllocation.py b/tests/code/test_getAllocation.py index 3d9259066..82d137106 100644 --- a/tests/code/test_getAllocation.py +++ b/tests/code/test_getAllocation.py @@ -1,4 +1,5 @@ from datetime import date +from unittest.mock import patch import pytest from app.models import mainDB @@ -12,7 +13,7 @@ from app.models.historyType import HistoryType from app.models.status import Status from app.models.user import User -from app.logic.getAllocation import getDepartmentAllocationSummary, countWorkers, getBreakHours +from app.logic.getAllocation import getDepartmentAllocationSummary, countWorkers, getBreakHours, getCurrentSemesterLabel def _createFormHistory(form, statusName): @@ -30,6 +31,28 @@ def _createFormHistory(form, statusName): ) +def test_getCurrentSemesterLabel_none_term(): + assert getCurrentSemesterLabel(None) is None + + +def test_getCurrentSemesterLabel_fall(): + """A term whose termCode's academic year is 2025 should read as Fall 2025 + when today falls in the Aug-Dec half of the academic year.""" + term = Term(termCode=202500) + with patch("app.logic.getAllocation.date") as mockDate: + mockDate.today.return_value = date(2025, 9, 15) + assert getCurrentSemesterLabel(term) == "Fall 2025" + + +def test_getCurrentSemesterLabel_spring(): + """The same academic-year term should read as Spring 2026 when today + falls in the Jan-Jul half of the academic year.""" + term = Term(termCode=202500) + with patch("app.logic.getAllocation.date") as mockDate: + mockDate.today.return_value = date(2026, 2, 10) + assert getCurrentSemesterLabel(term) == "Spring 2026" + + @pytest.mark.integration def test_getDepartmentAllocationSummary_no_allocation(): """ From 45b13e0b291f554f1d137a257c6118dbe6b7d7c4 Mon Sep 17 00:00:00 2001 From: munsakad Date: Mon, 3 Aug 2026 14:35:07 -0400 Subject: [PATCH 13/15] Fix duplicate main.managePositions endpoint left over from the department-portal-base merge The base branch (department-portal-base) moved managePositions out of main_routes.py into its own departmentPortal.py with proper permission checks (commit ee19d87e), but merging that branch in and accepting both sides left the old, now-dead copy in main_routes.py alongside the new file, so Flask registered two view functions under the same endpoint name and crashed on startup with "View function mapping is overwriting an existing endpoint function: main.managePositions". Remove the stale duplicate (it also referenced an unimported Tracy class) and the duplicate departmentPortal import in __init__.py from the same merge. --- app/controllers/main_routes/__init__.py | 1 - app/controllers/main_routes/main_routes.py | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 5ab0bf423..e8c4c6bb0 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -26,4 +26,3 @@ def injectGlobalData(): from app.controllers.main_routes import search from app.controllers.main_routes import studentResponse from app.controllers.main_routes import departmentPortal -from app.controllers.main_routes import departmentPortal diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 5d9d160dd..e9ec85e8f 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -100,20 +100,6 @@ def departmentPortal(org=None,account=None): positions = positionsList, posURL = posURL, ) -@main_bp.route('/department///managepositions', methods=['GET']) -def managePositions(org, account): - try: - dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) - except DoesNotExist: - return render_template('errors/404.html'), 404 - - positions = Tracy().getPositionsFromDepartment(org, account) - print(positions) - return render_template('main/managepositions.html', - department = dept, - department_name = dept.DEPT_NAME, - positions = positions - ) @main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form From 74bdd3ae687c6e45ae9c35a12cd38587ef35618d Mon Sep 17 00:00:00 2001 From: munsakad Date: Mon, 3 Aug 2026 15:03:24 -0400 Subject: [PATCH 14/15] Rework allocation rows to the "N hr: X contracts (out of Y allocations)" format and drop Break Hours Match the supervisor's whiteboard mockup: each Primary/Secondary hour bucket now reads as two lines ("10 hr: 2 contracts" / "(out of 5 allocations)") with correct singular/plural wording, via a small Jinja macro to avoid repeating the format six times. Break Hours is removed from the card entirely. Column alignment (left edges shared across the term row, headers, and hour rows) is unchanged, and the pinch-zone media query is retuned for the new, longer per-row text so nothing clips or overflows at narrow widths. --- app/static/css/departmentPortal.css | 16 ++++++---------- app/templates/main/departmentPortal.html | 19 +++++++++---------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/app/static/css/departmentPortal.css b/app/static/css/departmentPortal.css index e0b15ed9d..b04f98ad1 100644 --- a/app/static/css/departmentPortal.css +++ b/app/static/css/departmentPortal.css @@ -56,17 +56,14 @@ .allocation-table td { text-align: left; white-space: nowrap; -} -.allocation-table th:first-child, -.allocation-table td:first-child { - padding-right: 10px; + padding: 4px 10px 4px 0; + line-height: 1.3; } .allocation-table th { font-weight: 700; padding-top: 10px; } -.allocation-table .term-row h4, -.allocation-table .break-row h4 { +.allocation-table .term-row h4 { margin: 10px 0; } @@ -103,13 +100,12 @@ @media (min-width: 1200px) and (max-width: 1450px), (max-width: 480px) { .allocation-table { - font-size: 1em; + font-size: 0.85em; } .allocation-table td { - padding: 2px 5px; + padding: 2px 5px 2px 0; } - .allocation-table .term-row h4, - .allocation-table .break-row h4 { + .allocation-table .term-row h4 { font-size: 0.9rem; } } diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index 41eab4f91..b6178bf99 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -44,6 +44,9 @@

{% if department %} {{department.DEPT_NAME}} Portal {% e

Current Allocation

+ {% macro allocationCell(hours, used, allocated) -%} + {{ hours }} hr: {{ used }} contract{{ 's' if used != 1 else '' }}
(out of {{ allocated }} allocation{{ 's' if allocated != 1 else '' }}) + {%- endmacro %}
@@ -56,25 +59,21 @@

Current Allocation

- - + {{ allocationCell(10, usedPositions.used_10, allocation.primary_10) }} + {{ allocationCell(5, usedPositions.used_5_sec, allocation.secondary_5) }} - - + {{ allocationCell(12, usedPositions.used_12, allocation.primary_12) }} + {{ allocationCell(10, usedPositions.used_10_sec, allocation.secondary_10) }} - + {{ allocationCell(15, usedPositions.used_15, allocation.primary_15) }} - + {{ allocationCell(20, usedPositions.used_20, allocation.primary_20) }} - - - -
Secondary
10 Hour - {{usedPositions.used_10}} of {{allocation.primary_10}}5 Hour - {{usedPositions.used_5_sec}} of {{allocation.secondary_5}}
12 Hour - {{usedPositions.used_12}} of {{allocation.primary_12}}10 Hour - {{usedPositions.used_10_sec}} of {{allocation.secondary_10}}
15 Hour - {{usedPositions.used_15}} of {{allocation.primary_15}}
20 Hour - {{usedPositions.used_20}} of {{allocation.primary_20}}

Break Hours

{{break_hours}} of {{allocation.breakHours or 0}} Hours

From a7ca47461753507b9b94f8b84bfd834e03d92a3c Mon Sep 17 00:00:00 2001 From: munsakad Date: Tue, 4 Aug 2026 10:19:49 -0400 Subject: [PATCH 15/15] Address allocation card review: camelCase naming, stacked layout, consolidated tests - Rename allocation_summary and its snake_case keys/locals to camelCase (allocationSummary, currentSemester, usedPositions, breakHours, used10, usedSecondary5, ...) across the logic, route, template, and tests - Shorten the getCurrentSemesterLabel docstring - Reword the card to Contracted/Allocated in both the tooltip and the position count - Stack Secondary below Primary (and the count below the term) on narrow cards by splitting the paired table into two, instead of shrinking the font - Point the not-yet-built View Allocations page at "#" like the Members card, so the button no longer 404s - Collapse the per-scenario tests into one test per function, drop the leading underscore from createFormHistory, and mark the unit test so run_tests.sh stops deselecting it - Cover the used-count denial filter and make the term filter and the getBreakHours other-term assertion actually meaningful - Revert unrelated whitespace/formatting churn in main_routes.py --- app/controllers/main_routes/main_routes.py | 24 +- app/logic/getAllocation.py | 72 +++--- app/static/css/departmentPortal.css | 35 ++- app/templates/main/departmentPortal.html | 66 +++--- tests/code/test_getAllocation.py | 255 +++++++++------------ 5 files changed, 212 insertions(+), 240 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index e9ec85e8f..de37e4b4e 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -12,6 +12,7 @@ from app.models.term import Term from app.models.allocation import Allocation from app.models.positionHistory import PositionHistory + from app.controllers.admin_routes.allPendingForms import checkAdjustment from app.controllers.main_routes import main_bp @@ -24,6 +25,7 @@ from app.logic.getSupervisors import getSupervisors from app.logic.getPositions import getActivePositions + @main_bp.route('/logout', methods=['GET']) def triggerLogout(): return redirect(logout()) @@ -71,8 +73,8 @@ def departmentPortal(org=None,account=None): supervisors, laborCoordinators = getSupervisors(dept) - allocation_summary = getDepartmentAllocationSummary(dept) - recentTerm = allocation_summary["term"] + allocationSummary = getDepartmentAllocationSummary(dept) + recentTerm = allocationSummary["term"] if recentTerm: try: @@ -82,24 +84,24 @@ def departmentPortal(org=None,account=None): else: allocation = None - positionsList, posURL = getActivePositions(dept) + positionsList, posURL = getActivePositions(dept) - return render_template('main/departmentPortal.html', + return render_template('main/departmentPortal.html', departments = departments, department = dept, allocation = allocation, - allocated = allocation_summary["allocated"], - used = allocation_summary["used"], + allocated = allocationSummary["allocated"], + used = allocationSummary["used"], term = recentTerm, - currentSemester = allocation_summary["current_semester"], - usedPositions = allocation_summary["used_positions"], - break_hours = allocation_summary["break_hours"], + currentSemester = allocationSummary["currentSemester"], + usedPositions = allocationSummary["usedPositions"], + breakHours = allocationSummary["breakHours"], supervisors = supervisors, laborCoordinators=laborCoordinators, currentUser=currentUser, positions = positionsList, - posURL = posURL, - ) + posURL = posURL) + @main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form diff --git a/app/logic/getAllocation.py b/app/logic/getAllocation.py index f5d5d281f..1f7c879d3 100644 --- a/app/logic/getAllocation.py +++ b/app/logic/getAllocation.py @@ -9,12 +9,8 @@ def getCurrentSemesterLabel(term): - """Return the current Fall/Spring semester label (e.g. "Fall 2025") for - the academic year that the given term belongs to. The season is picked - from today's month and the year comes from the term's own termCode, - following the AY/Fall/Spring termCode convention in termManagement.py - (AY code, code+11 = Fall of that year, code+12 = Spring of the next). - """ + """Return the Fall/Spring label (e.g. "Fall 2025") for the AY term's + current semester, picking the season from today's month.""" if not term: return None academicYear = int(str(term.termCode)[:4]) @@ -23,15 +19,15 @@ def getCurrentSemesterLabel(term): return f"Spring {academicYear + 1}" -def countWorkers(department, term_code, job_type, hours_bucket): +def countWorkers(department, termCode, jobType, hoursBucket): workerCount = ( LaborStatusForm.select() .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) .where( LaborStatusForm.department == department, - LaborStatusForm.termCode == term_code, - LaborStatusForm.jobType == job_type, - LaborStatusForm.weeklyHours == hours_bucket, + LaborStatusForm.termCode == termCode, + LaborStatusForm.jobType == jobType, + LaborStatusForm.weeklyHours == hoursBucket, LaborStatusForm.contractHours.is_null(True), FormHistory.historyType == "Labor Status Form", ~(FormHistory.status % "Denied%"), @@ -41,13 +37,13 @@ def countWorkers(department, term_code, job_type, hours_bucket): return workerCount -def getBreakHours(department, term_code): +def getBreakHours(department, termCode): breakHoursTotal = ( LaborStatusForm.select(fn.SUM(LaborStatusForm.contractHours)) .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) .where( LaborStatusForm.department == department, - LaborStatusForm.termCode == term_code, + LaborStatusForm.termCode == termCode, FormHistory.historyType == "Labor Status Form", FormHistory.status == "Approved", ) @@ -60,18 +56,18 @@ def getDepartmentAllocationSummary(department): """Return allocation-utilization values for a department's most recent term.""" result = { "term": None, - "current_semester": None, + "currentSemester": None, "allocated": 0, "used": 0, - "used_positions": { - "used_10": 0, - "used_12": 0, - "used_15": 0, - "used_20": 0, - "used_5_sec": 0, - "used_10_sec": 0, + "usedPositions": { + "used10": 0, + "used12": 0, + "used15": 0, + "used20": 0, + "usedSecondary5": 0, + "usedSecondary10": 0, }, - "break_hours": 0, + "breakHours": 0, } departmentAllocations = list( @@ -81,11 +77,11 @@ def getDepartmentAllocationSummary(department): return result recentTerm = Term.order_by_term([a.termCode for a in departmentAllocations], reverse=True)[0] - term_code = recentTerm.termCode + termCode = recentTerm.termCode result["term"] = recentTerm - result["current_semester"] = getCurrentSemesterLabel(recentTerm) + result["currentSemester"] = getCurrentSemesterLabel(recentTerm) - total_positions = ( + totalPositions = ( Allocation.select( fn.SUM(Allocation.primary_10) + fn.SUM(Allocation.primary_12) @@ -96,35 +92,35 @@ def getDepartmentAllocationSummary(department): ) .where( Allocation.department == department, - Allocation.termCode == term_code, + Allocation.termCode == termCode, ) .scalar() ) - result["allocated"] = total_positions or 0 + result["allocated"] = totalPositions or 0 - used_allocation = ( + usedAllocation = ( LaborStatusForm.select() .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) .where( LaborStatusForm.department == department, - LaborStatusForm.termCode == term_code, + LaborStatusForm.termCode == termCode, LaborStatusForm.contractHours.is_null(True), FormHistory.historyType == "Labor Status Form", ~(FormHistory.status % "Denied%"), ) .count() ) - result["used"] = used_allocation - - result["used_positions"] = { - "used_10": countWorkers(department, term_code, "Primary", 10), - "used_12": countWorkers(department, term_code, "Primary", 12), - "used_15": countWorkers(department, term_code, "Primary", 15), - "used_20": countWorkers(department, term_code, "Primary", 20), - "used_5_sec": countWorkers(department, term_code, "Secondary", 5), - "used_10_sec": countWorkers(department, term_code, "Secondary", 10), + result["used"] = usedAllocation + + result["usedPositions"] = { + "used10": countWorkers(department, termCode, "Primary", 10), + "used12": countWorkers(department, termCode, "Primary", 12), + "used15": countWorkers(department, termCode, "Primary", 15), + "used20": countWorkers(department, termCode, "Primary", 20), + "usedSecondary5": countWorkers(department, termCode, "Secondary", 5), + "usedSecondary10": countWorkers(department, termCode, "Secondary", 10), } - result["break_hours"] = getBreakHours(department, term_code) + result["breakHours"] = getBreakHours(department, termCode) return result diff --git a/app/static/css/departmentPortal.css b/app/static/css/departmentPortal.css index b04f98ad1..88ae06e29 100644 --- a/app/static/css/departmentPortal.css +++ b/app/static/css/departmentPortal.css @@ -47,8 +47,24 @@ overflow-x: auto; margin: 10px 0; } +.allocation-summary { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + gap: 0 1rem; +} +.allocation-summary h4 { + margin: 10px 0; +} +.allocation-columns { + display: flex; + flex-wrap: wrap; + gap: 0 2rem; +} .allocation-table { - width: 100%; + /* wraps onto its own line when the card is too narrow, instead of shrinking */ + flex: 1 1 180px; border-collapse: collapse; font-size: 1.2em; } @@ -63,9 +79,6 @@ font-weight: 700; padding-top: 10px; } -.allocation-table .term-row h4 { - margin: 10px 0; -} .bi-people-fill { /* Bootstrap Icon for Members Card */ border: 1px solid #c0c0c0; @@ -98,14 +111,14 @@ } } +/* Narrow card: stack Secondary below Primary, and the position count below the + term, rather than shrinking the text to keep them side by side. */ @media (min-width: 1200px) and (max-width: 1450px), (max-width: 480px) { - .allocation-table { - font-size: 0.85em; - } - .allocation-table td { - padding: 2px 5px 2px 0; + .allocation-summary { + flex-direction: column; + gap: 0; } - .allocation-table .term-row h4 { - font-size: 0.9rem; + .allocation-columns .allocation-table { + flex-basis: 100%; } } diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index b6178bf99..ce0b051fe 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -9,11 +9,11 @@ {% endblock %} + {% block app_content %}

{% if department %} {{department.DEPT_NAME}} Portal {% else %} Choose a Department: {% endif %}

-
-{% if department %} + {% if department %}
@@ -42,45 +42,43 @@

{% if department %} {{department.DEPT_NAME}} Portal {% e

-

Current Allocation

+

Current Allocations

- {% macro allocationCell(hours, used, allocated) -%} - {{ hours }} hr: {{ used }} contract{{ 's' if used != 1 else '' }}
(out of {{ allocated }} allocation{{ 's' if allocated != 1 else '' }}) + {% macro allocationRow(hours, used, allocated) -%} + {{ hours }} hr: {{ used }} contract{{ 's' if used != 1 else '' }}
(out of {{ allocated }} allocation{{ 's' if allocated != 1 else '' }}) {%- endmacro %}
- - - - - - - - - - - - {{ allocationCell(10, usedPositions.used_10, allocation.primary_10) }} - {{ allocationCell(5, usedPositions.used_5_sec, allocation.secondary_5) }} - - - {{ allocationCell(12, usedPositions.used_12, allocation.primary_12) }} - {{ allocationCell(10, usedPositions.used_10_sec, allocation.secondary_10) }} - - - {{ allocationCell(15, usedPositions.used_15, allocation.primary_15) }} - - - - {{ allocationCell(20, usedPositions.used_20, allocation.primary_20) }} - - - -

{{ currentSemester if currentSemester else "No term data" }}

{{used}} of {{allocated or 0}} Positions

PrimarySecondary
+
+

{{ currentSemester if currentSemester else "No term data" }}

+

{{used}} contracted of {{allocated or 0}} allocated Positions

+
+
+ + + + + + {{ allocationRow(10, usedPositions.used10, allocation.primary_10) }} + {{ allocationRow(12, usedPositions.used12, allocation.primary_12) }} + {{ allocationRow(15, usedPositions.used15, allocation.primary_15) }} + {{ allocationRow(20, usedPositions.used20, allocation.primary_20) }} + +
Primary
+ + + + + + {{ allocationRow(5, usedPositions.usedSecondary5, allocation.secondary_5) }} + {{ allocationRow(10, usedPositions.usedSecondary10, allocation.secondary_10) }} + +
Secondary
+
diff --git a/tests/code/test_getAllocation.py b/tests/code/test_getAllocation.py index 82d137106..72e115db3 100644 --- a/tests/code/test_getAllocation.py +++ b/tests/code/test_getAllocation.py @@ -16,9 +16,9 @@ from app.logic.getAllocation import getDepartmentAllocationSummary, countWorkers, getBreakHours, getCurrentSemesterLabel -def _createFormHistory(form, statusName): - """Attach a FormHistory row to a LaborStatusForm, since getBreakHours now - only counts forms with an approved "Labor Status Form" history entry.""" +def createFormHistory(form, statusName): + """Attach a "Labor Status Form" history entry with the given status, since + the allocation queries only count forms that have one.""" user = User.create(username=f"testuser_{form.laborStatusFormID}") historyType = HistoryType.get(HistoryType.historyTypeName == "Labor Status Form") status = Status.get(Status.statusName == statusName) @@ -31,74 +31,82 @@ def _createFormHistory(form, statusName): ) -def test_getCurrentSemesterLabel_none_term(): +@pytest.mark.unit +def test_getCurrentSemesterLabel(): + """ + Test that a term maps to the Fall/Spring label for whichever half of the + academic year today falls in, and that a missing term has no label. + """ + # No term (e.g. a department with no allocations) - nothing to label assert getCurrentSemesterLabel(None) is None - -def test_getCurrentSemesterLabel_fall(): - """A term whose termCode's academic year is 2025 should read as Fall 2025 - when today falls in the Aug-Dec half of the academic year.""" term = Term(termCode=202500) + + # Aug-Dec half of the academic year - reads as Fall of the term's own year with patch("app.logic.getAllocation.date") as mockDate: mockDate.today.return_value = date(2025, 9, 15) assert getCurrentSemesterLabel(term) == "Fall 2025" - -def test_getCurrentSemesterLabel_spring(): - """The same academic-year term should read as Spring 2026 when today - falls in the Jan-Jul half of the academic year.""" - term = Term(termCode=202500) + # Jan-Jul half of the same academic-year term - reads as Spring of the next year with patch("app.logic.getAllocation.date") as mockDate: mockDate.today.return_value = date(2026, 2, 10) assert getCurrentSemesterLabel(term) == "Spring 2026" @pytest.mark.integration -def test_getDepartmentAllocationSummary_no_allocation(): +def test_getDepartmentAllocationSummary(): """ - Test that a department with no Allocation rows gets a zeroed-out summary - with term=None, instead of an error. + Test that the summary reports allocated/used/breakHours for a department's + most recent term, covering a missing department, a department with no + Allocation rows, allocations spread across terms, several Allocation rows + in one term, break-term contracts, and an allocation with no forms. """ + zeroedUsedPositions = { + "used10": 0, + "used12": 0, + "used15": 0, + "used20": 0, + "usedSecondary5": 0, + "usedSecondary10": 0, + } + + # department=None (e.g. when Department.get() fails in the departmentPortal + # route) returns the zeroed-out fallback instead of raising an error + summary = getDepartmentAllocationSummary(None) + + assert summary["term"] is None + assert summary["allocated"] == 0 + assert summary["used"] == 0 + assert summary["breakHours"] == 0 + assert summary["usedPositions"] == zeroedUsedPositions + with mainDB.atomic() as transaction: - dept = Department.create(departmentID=200, DEPT_NAME="Physics", ACCOUNT="6750", ORG="2120", isActive=True) + # A department with no Allocation rows gets the same zeroed-out summary + # with term=None + emptyDept = Department.create(departmentID=200, DEPT_NAME="Physics", ACCOUNT="6750", ORG="2120", isActive=True) - summary = getDepartmentAllocationSummary(dept) + summary = getDepartmentAllocationSummary(emptyDept) assert summary["term"] is None assert summary["allocated"] == 0 assert summary["used"] == 0 - assert summary["break_hours"] == 0 - assert summary["used_positions"] == { - "used_10": 0, - "used_12": 0, - "used_15": 0, - "used_20": 0, - "used_5_sec": 0, - "used_10_sec": 0, - } - - transaction.rollback() - + assert summary["breakHours"] == 0 + assert summary["usedPositions"] == zeroedUsedPositions -@pytest.mark.integration -def test_getDepartmentAllocationSummary_uses_most_recent_term(): - """ - Test that when a department has allocations across multiple terms, the - summary reflects only the most recent term's data. - """ - with mainDB.atomic() as transaction: - dept = Department.create(departmentID=201, DEPT_NAME="Chemistry", ACCOUNT="6751", ORG="2121", isActive=True) + # With allocations across multiple terms, the summary reflects only the + # most recent term's data + multiTermDept = Department.create(departmentID=201, DEPT_NAME="Chemistry", ACCOUNT="6751", ORG="2121", isActive=True) oldTerm = Term.create(termCode=900000, termName="AY Test Old") newTerm = Term.create(termCode=900100, termName="AY Test New") Allocation.create( - termCode=oldTerm, department=dept, isFinal=True, justification="old", + termCode=oldTerm, department=multiTermDept, isFinal=True, justification="old", primary_10=1, primary_12=0, primary_15=0, primary_20=0, secondary_5=0, secondary_10=0, breakHours=50, ) Allocation.create( - termCode=newTerm, department=dept, isFinal=True, justification="new", + termCode=newTerm, department=multiTermDept, isFinal=True, justification="new", primary_10=2, primary_12=3, primary_15=0, primary_20=0, secondary_5=1, secondary_10=0, breakHours=100, ) @@ -106,151 +114,106 @@ def test_getDepartmentAllocationSummary_uses_most_recent_term(): supervisor = Supervisor.create(ID="SUP001", isActive=True) student = Student.create(ID="STU001", isActive=True) - # Under the OLD term - should be excluded from the summary - LaborStatusForm.create( - termCode=oldTerm, studentSupervisee=student, supervisor=supervisor, department=dept, + # Approved under the OLD term - excluded by the term filter alone + oldForm = LaborStatusForm.create( + termCode=oldTerm, studentSupervisee=student, supervisor=supervisor, department=multiTermDept, jobType="Primary", WLS="10", POSN_TITLE="Old Job", POSN_CODE="S001", weeklyHours=10, contractHours=None, ) + createFormHistory(oldForm, "Approved") + # Under the NEW (most recent) term - should be counted newForm = LaborStatusForm.create( - termCode=newTerm, studentSupervisee=student, supervisor=supervisor, department=dept, + termCode=newTerm, studentSupervisee=student, supervisor=supervisor, department=multiTermDept, jobType="Primary", WLS="10", POSN_TITLE="New Job", POSN_CODE="S002", weeklyHours=10, contractHours=None, ) - _createFormHistory(newForm, "Approved") + createFormHistory(newForm, "Approved") - summary = getDepartmentAllocationSummary(dept) + # Denied under the NEW term - should not count toward used + deniedForm = LaborStatusForm.create( + termCode=newTerm, studentSupervisee=student, supervisor=supervisor, department=multiTermDept, + jobType="Primary", WLS="12", POSN_TITLE="Denied Job", POSN_CODE="S004", + weeklyHours=12, contractHours=None, + ) + createFormHistory(deniedForm, "Denied by Admin") + + summary = getDepartmentAllocationSummary(multiTermDept) assert summary["term"].termCode == 900100 assert summary["allocated"] == 6 # 2 + 3 + 0 + 0 + 1 + 0, from the new term only - assert summary["used"] == 1 # only the new term's LaborStatusForm counts - assert summary["used_positions"]["used_10"] == 1 - assert summary["break_hours"] == 0 + assert summary["used"] == 1 # only the new term's approved LaborStatusForm counts + assert summary["usedPositions"]["used10"] == 1 + assert summary["usedPositions"]["used12"] == 0 # the denied form is not counted + assert summary["breakHours"] == 0 - transaction.rollback() - - -@pytest.mark.integration -def test_getDepartmentAllocationSummary_break_hours(): - """ - Test that break_hours only sums approved forms with contractHours set - (break-term contracts), and that those forms are excluded from the - weekly "used" count. - """ - with mainDB.atomic() as transaction: - dept = Department.create(departmentID=202, DEPT_NAME="Biology", ACCOUNT="6752", ORG="2122", isActive=True) - term = Term.create(termCode=900200, termName="AY Test Break") + # breakHours only sums approved forms with contractHours set (break-term + # contracts), and those forms are excluded from the weekly "used" count + breakDept = Department.create(departmentID=202, DEPT_NAME="Biology", ACCOUNT="6752", ORG="2122", isActive=True) + breakTerm = Term.create(termCode=900200, termName="AY Test Break") Allocation.create( - termCode=term, department=dept, isFinal=True, justification="test", + termCode=breakTerm, department=breakDept, isFinal=True, justification="test", primary_10=1, primary_12=0, primary_15=0, primary_20=0, secondary_5=0, secondary_10=0, breakHours=200, ) - supervisor = Supervisor.create(ID="SUP002", isActive=True) - student = Student.create(ID="STU002", isActive=True) + breakSupervisor = Supervisor.create(ID="SUP002", isActive=True) + breakStudent = Student.create(ID="STU002", isActive=True) breakForm = LaborStatusForm.create( - termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, + termCode=breakTerm, studentSupervisee=breakStudent, supervisor=breakSupervisor, department=breakDept, jobType="Primary", WLS="10", POSN_TITLE="Break Worker", POSN_CODE="S003", weeklyHours=None, contractHours=40, ) - _createFormHistory(breakForm, "Approved") + createFormHistory(breakForm, "Approved") - summary = getDepartmentAllocationSummary(dept) + summary = getDepartmentAllocationSummary(breakDept) - assert summary["break_hours"] == 40 + assert summary["breakHours"] == 40 assert summary["used"] == 0 - transaction.rollback() - - -@pytest.mark.integration -def test_getDepartmentAllocationSummary_department_none(): - """ - Test that passing department=None (e.g. when Department.get() fails in - the departmentPortal route) returns the zeroed-out fallback instead of - raising an error. - """ - summary = getDepartmentAllocationSummary(None) - - assert summary["term"] is None - assert summary["allocated"] == 0 - assert summary["used"] == 0 - assert summary["break_hours"] == 0 - assert summary["used_positions"] == { - "used_10": 0, - "used_12": 0, - "used_15": 0, - "used_20": 0, - "used_5_sec": 0, - "used_10_sec": 0, - } - - -@pytest.mark.integration -def test_getDepartmentAllocationSummary_multiple_allocations_same_term(): - """ - Test that if a department has more than one Allocation row for the same - most-recent term (e.g. a draft and a final revision, which the model's - (termCode, department, isFinal) index allows), the totals sum across - both rows rather than picking just one. - """ - with mainDB.atomic() as transaction: - dept = Department.create(departmentID=203, DEPT_NAME="Mathematics", ACCOUNT="6753", ORG="2123", isActive=True) - term = Term.create(termCode=900300, termName="AY Test Multi") + # More than one Allocation row for the same most-recent term (e.g. a + # draft and a final revision, which the model's (termCode, department, + # isFinal) index allows) sums across both rows rather than picking one + multiRowDept = Department.create(departmentID=203, DEPT_NAME="Mathematics", ACCOUNT="6753", ORG="2123", isActive=True) + multiRowTerm = Term.create(termCode=900300, termName="AY Test Multi") Allocation.create( - termCode=term, department=dept, isFinal=False, justification="draft", + termCode=multiRowTerm, department=multiRowDept, isFinal=False, justification="draft", primary_10=1, primary_12=0, primary_15=0, primary_20=0, secondary_5=0, secondary_10=0, breakHours=10, ) Allocation.create( - termCode=term, department=dept, isFinal=True, justification="final", + termCode=multiRowTerm, department=multiRowDept, isFinal=True, justification="final", primary_10=2, primary_12=0, primary_15=0, primary_20=0, secondary_5=0, secondary_10=0, breakHours=20, ) - summary = getDepartmentAllocationSummary(dept) + summary = getDepartmentAllocationSummary(multiRowDept) assert summary["term"].termCode == 900300 assert summary["allocated"] == 3 # 1 + 2, summed across both rows - transaction.rollback() - - -@pytest.mark.integration -def test_getDepartmentAllocationSummary_allocation_no_labor_status_forms(): - """ - Test that a department with an allocation for the most recent term but no - LaborStatusForm records at all shows allocated > 0 with used/break_hours - at 0, rather than erroring on an empty result set. - """ - with mainDB.atomic() as transaction: - dept = Department.create(departmentID=204, DEPT_NAME="History", ACCOUNT="6754", ORG="2124", isActive=True) - term = Term.create(termCode=900400, termName="AY Test Empty") + # An allocation for the most recent term with no LaborStatusForm records + # at all shows allocated > 0 with used/breakHours at 0, rather than + # erroring on an empty result set + noFormsDept = Department.create(departmentID=204, DEPT_NAME="History", ACCOUNT="6754", ORG="2124", isActive=True) + noFormsTerm = Term.create(termCode=900400, termName="AY Test Empty") Allocation.create( - termCode=term, department=dept, isFinal=True, justification="test", + termCode=noFormsTerm, department=noFormsDept, isFinal=True, justification="test", primary_10=3, primary_12=2, primary_15=0, primary_20=0, secondary_5=1, secondary_10=0, breakHours=150, ) - summary = getDepartmentAllocationSummary(dept) + summary = getDepartmentAllocationSummary(noFormsDept) assert summary["term"].termCode == 900400 assert summary["allocated"] == 6 assert summary["used"] == 0 - assert summary["break_hours"] == 0 - assert summary["used_positions"] == { - "used_10": 0, - "used_12": 0, - "used_15": 0, - "used_20": 0, - "used_5_sec": 0, - "used_10_sec": 0, - } + assert summary["breakHours"] == 0 + assert summary["usedPositions"] == zeroedUsedPositions transaction.rollback() @@ -276,7 +239,7 @@ def test_countWorkers(): jobType="Primary", WLS="10", POSN_TITLE="Match", POSN_CODE="S010", weeklyHours=10, contractHours=None, ) - _createFormHistory(matchForm, "Approved") + createFormHistory(matchForm, "Approved") # Different job type - should not count toward ("Primary", 10) wrongJobTypeForm = LaborStatusForm.create( @@ -284,7 +247,7 @@ def test_countWorkers(): jobType="Secondary", WLS="10", POSN_TITLE="Wrong Job Type", POSN_CODE="S011", weeklyHours=10, contractHours=None, ) - _createFormHistory(wrongJobTypeForm, "Approved") + createFormHistory(wrongJobTypeForm, "Approved") # Different hours bucket - should not count toward ("Primary", 10) wrongHoursForm = LaborStatusForm.create( @@ -292,7 +255,7 @@ def test_countWorkers(): jobType="Primary", WLS="12", POSN_TITLE="Wrong Hours", POSN_CODE="S012", weeklyHours=12, contractHours=None, ) - _createFormHistory(wrongHoursForm, "Approved") + createFormHistory(wrongHoursForm, "Approved") # Break-term contract (contractHours set) - should not count even though # job type and weeklyHours otherwise match @@ -301,7 +264,7 @@ def test_countWorkers(): jobType="Primary", WLS="10", POSN_TITLE="Break Contract", POSN_CODE="S013", weeklyHours=10, contractHours=40, ) - _createFormHistory(breakContractForm, "Approved") + createFormHistory(breakContractForm, "Approved") # Matches everything but was DENIED - should not count deniedForm = LaborStatusForm.create( @@ -309,7 +272,7 @@ def test_countWorkers(): jobType="Primary", WLS="10", POSN_TITLE="Denied Match", POSN_CODE="S014", weeklyHours=10, contractHours=None, ) - _createFormHistory(deniedForm, "Denied by Admin") + createFormHistory(deniedForm, "Denied by Admin") assert countWorkers(dept, term.termCode, "Primary", 10) == 1 assert countWorkers(dept, term.termCode, "Secondary", 10) == 1 @@ -341,14 +304,14 @@ def test_getBreakHours(): jobType="Primary", WLS="10", POSN_TITLE="Break A", POSN_CODE="S020", weeklyHours=None, contractHours=40, ) - _createFormHistory(formA, "Approved") + createFormHistory(formA, "Approved") formB = LaborStatusForm.create( termCode=term, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Secondary", WLS="5", POSN_TITLE="Break B", POSN_CODE="S021", weeklyHours=None, contractHours=60, ) - _createFormHistory(formB, "Approved") + createFormHistory(formB, "Approved") # Weekly-hours form (contractHours=None) - should be excluded regardless formC = LaborStatusForm.create( @@ -356,15 +319,15 @@ def test_getBreakHours(): jobType="Primary", WLS="10", POSN_TITLE="Weekly Job", POSN_CODE="S022", weeklyHours=10, contractHours=None, ) - _createFormHistory(formC, "Approved") + createFormHistory(formC, "Approved") # Break-term contract under a DIFFERENT term - should be excluded formD = LaborStatusForm.create( termCode=otherTerm, studentSupervisee=student, supervisor=supervisor, department=dept, jobType="Primary", WLS="10", POSN_TITLE="Break Other Term", POSN_CODE="S023", - weeklyHours=None, contractHours=100, + weeklyHours=None, contractHours=25, ) - _createFormHistory(formD, "Approved") + createFormHistory(formD, "Approved") # Break-term contract that is still PENDING - should be excluded formE = LaborStatusForm.create( @@ -372,9 +335,9 @@ def test_getBreakHours(): jobType="Primary", WLS="10", POSN_TITLE="Break Pending", POSN_CODE="S024", weeklyHours=None, contractHours=999, ) - _createFormHistory(formE, "Pending") + createFormHistory(formE, "Pending") - assert getBreakHours(dept, term.termCode) == 100 # 40 + 60, excludes the pending form - assert getBreakHours(dept, otherTerm.termCode) == 100 + assert getBreakHours(dept, term.termCode) == 100 # 40 + 60, excludes the pending form + assert getBreakHours(dept, otherTerm.termCode) == 25 # only the other term's contract transaction.rollback()