-
- Insert Allocations Card Here
+
diff --git a/database/demo_data.py b/database/demo_data.py
index 20c4517d..2e46ca61 100644
--- a/database/demo_data.py
+++ b/database/demo_data.py
@@ -1,9 +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...)'''
-
from app import app
-
from app.models.Tracy import db
from app.models.Tracy.studata import STUDATA
from app.models.Tracy.stuposn import STUPOSN
@@ -19,6 +14,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 +40,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 +132,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 = [
{
@@ -461,6 +495,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",
@@ -617,6 +667,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,
@@ -642,6 +810,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()
@@ -939,4 +1124,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()
diff --git a/database/migrate_db.sh b/database/migrate_db.sh
index dea226d8..c693c708 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 0b5466dc..fabca9d3 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
diff --git a/database/reset_database.sh b/database/reset_database.sh
index 82f6cff5..ba87204d 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
diff --git a/tests/code/test_getAllocation.py b/tests/code/test_getAllocation.py
new file mode 100644
index 00000000..72e115db
--- /dev/null
+++ b/tests/code/test_getAllocation.py
@@ -0,0 +1,343 @@
+from datetime import date
+from unittest.mock import patch
+
+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.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, getCurrentSemesterLabel
+
+
+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)
+ return FormHistory.create(
+ formID=form,
+ historyType=historyType,
+ createdBy=user,
+ createdDate=date.today(),
+ status=status,
+ )
+
+
+@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
+
+ 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"
+
+ # 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():
+ """
+ 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:
+ # 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(emptyDept)
+
+ assert summary["term"] is None
+ assert summary["allocated"] == 0
+ assert summary["used"] == 0
+ assert summary["breakHours"] == 0
+ assert summary["usedPositions"] == zeroedUsedPositions
+
+ # 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=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=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,
+ )
+
+ supervisor = Supervisor.create(ID="SUP001", isActive=True)
+ student = Student.create(ID="STU001", isActive=True)
+
+ # 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=multiTermDept,
+ jobType="Primary", WLS="10", POSN_TITLE="New Job", POSN_CODE="S002",
+ weeklyHours=10, contractHours=None,
+ )
+ createFormHistory(newForm, "Approved")
+
+ # 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 approved LaborStatusForm counts
+ assert summary["usedPositions"]["used10"] == 1
+ assert summary["usedPositions"]["used12"] == 0 # the denied form is not counted
+ assert summary["breakHours"] == 0
+
+ # 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=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,
+ )
+
+ breakSupervisor = Supervisor.create(ID="SUP002", isActive=True)
+ breakStudent = Student.create(ID="STU002", isActive=True)
+
+ breakForm = LaborStatusForm.create(
+ 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")
+
+ summary = getDepartmentAllocationSummary(breakDept)
+
+ assert summary["breakHours"] == 40
+ assert summary["used"] == 0
+
+ # 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=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=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(multiRowDept)
+
+ assert summary["term"].termCode == 900300
+ assert summary["allocated"] == 3 # 1 + 2, summed across both rows
+
+ # 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=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(noFormsDept)
+
+ assert summary["term"].termCode == 900400
+ assert summary["allocated"] == 6
+ assert summary["used"] == 0
+ assert summary["breakHours"] == 0
+ assert summary["usedPositions"] == zeroedUsedPositions
+
+ 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, 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)
+ 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
+ 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)
+ 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)
+ 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
+ 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
+ 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=25,
+ )
+ 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) == 25 # only the other term's contract
+
+ transaction.rollback()