diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py
index b651fe49..cc6f3b86 100755
--- a/app/controllers/main_routes/main_routes.py
+++ b/app/controllers/main_routes/main_routes.py
@@ -10,8 +10,8 @@
from app.models.laborStatusForm import LaborStatusForm
from app.models.formHistory import FormHistory
from app.models.term import Term
-from app.models.positionHistory import PositionHistory
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,7 +24,7 @@
from app.logic.getSupervisors import getSupervisors
from app.logic.getPositions import getActivePositions
from app.logic.allocationManager import getBreakContracts, getContractedAllocations, getTotalAllocations
-from app.logic.getTerms import getTerms
+from app.logic.getTerms import getTerms, getCurrentSemester
@main_bp.route('/logout', methods=['GET'])
@@ -74,11 +74,21 @@ def departmentPortal(org=None,account=None):
supervisors, laborCoordinators = getSupervisors(dept)
+
+ currentAY, fallTerm, springTerm = getTerms()
+ allocationDict = getTotalAllocations(currentAY, dept)
+ currentSemester = getCurrentSemester()
+ contracts = getContractedAllocations(currentSemester, dept)
+
+
positionsList, posURL = getActivePositions(dept)
return render_template('main/departmentPortal.html',
departments = departments,
department = dept,
+ contracts = contracts,
+ allocation = allocationDict,
+ currentSemester = currentSemester.termName,
supervisors = supervisors,
laborCoordinators=laborCoordinators,
currentUser=currentUser,
diff --git a/app/logic/allocationManager.py b/app/logic/allocationManager.py
index dbd35c18..53e23862 100644
--- a/app/logic/allocationManager.py
+++ b/app/logic/allocationManager.py
@@ -98,36 +98,22 @@ def countContracts(jobType: str, weeklyContractHours: int, termCode: int, dept:
def getContractedAllocations(termCode: int, dept: int):
'''
- This function returns a dictionary with a breakdown of all types of contracts
+ This function returns a dictionary with a breakdown of all types of contracts
for the given department and term in the form of a dictionary.
'''
academicYearCode = int(str(termCode)[:4] + "00")
- allocationObject = getAllocation(termCode, dept)
- breakAllocation = FormHistory.select(
- LaborStatusForm.department,
- LaborStatusForm.termCode,
- fn.SUM(LaborStatusForm.contractHours).alias('total_hours')
- ).join(
- LaborStatusForm,
- on=(FormHistory.formID == LaborStatusForm.laborStatusFormID),
- ).join(
- Term,
- on = (LaborStatusForm.termCode == Term.termCode )
- ).where(
- (FormHistory.historyType == "Labor Status Form") &
- (FormHistory.status == "Approved") &
- (LaborStatusForm.termCode.in_([termCode,academicYearCode]))
- ).group_by(
- LaborStatusForm.department,
- LaborStatusForm.termCode).dicts()
-
- breakSum = {"total_hours": 0}
- if dept:
- for row in breakAllocation:
- if row["department"] == dept:
- breakSum = row
- break
-
+ breakHoursTotal = (
+ FormHistory.select(fn.SUM(LaborStatusForm.contractHours))
+ .join(LaborStatusForm, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
+ .where(
+ FormHistory.historyType == "Labor Status Form",
+ FormHistory.status == "Approved",
+ LaborStatusForm.termCode.in_([termCode, academicYearCode]),
+ LaborStatusForm.department == dept,
+ )
+ .scalar()
+ ) or 0
+
# dictionary definition:
usedPositions = {
"used_10": countContracts("Primary", "10", termCode, dept),
@@ -139,7 +125,7 @@ def getContractedAllocations(termCode: int, dept: int):
"used_primaries": 0,
"used_secondaries": 0,
"used_total": 0, # all contracts with weekly hours, i.e. primaries + secondaries (not break contracts)
- "break_hours": breakSum["total_hours"] # all break hours contracted (but not necessarily worked)
+ "break_hours": breakHoursTotal # all break hours contracted (but not necessarily worked)
}
usedPositions["used_primaries"] = sum(list(usedPositions.values())[:4])
usedPositions["used_secondaries"] = sum(list(usedPositions.values())[4:6])
diff --git a/app/logic/getTerms.py b/app/logic/getTerms.py
index ea3b60da..19df06ec 100644
--- a/app/logic/getTerms.py
+++ b/app/logic/getTerms.py
@@ -33,3 +33,15 @@ def getTerms(academicYear: str = None):
springTerm = Term.select().where(Term.termCode == currentDate.year * 100 + 12).get()
return currentAY, fallTerm, springTerm
+def getCurrentSemester():
+ '''
+ The difference between this function and the one above is that it gets just the current term
+ It does not get both Fall and Spring, it just gets one depending on the month.
+ '''
+ currentDate = date.today()
+ if currentDate.month <= 6:
+ springTerm = Term.select().where(Term.termCode == currentDate.year * 100 + 12 - 100).get()
+ return springTerm
+ else:
+ fallTerm = Term.select().where(Term.termCode == currentDate.year * 100 + 11).get()
+ return fallTerm
diff --git a/app/static/css/departmentPortal.css b/app/static/css/departmentPortal.css
index d9acbba7..0f64aa2e 100644
--- a/app/static/css/departmentPortal.css
+++ b/app/static/css/departmentPortal.css
@@ -11,25 +11,71 @@
padding: 1rem;
min-width: 100%;
}
-
-.members-icon {
- display: inline-block;
+.bi-suitcase-lg-fill { /* Bootstrap Icon */
border: 1px solid #c0c0c0;
border-radius: 8px;
- padding: 3px 3.5px 1.5px;
- font-size: 36px;
- color: #6e6e6e;
+ padding: 3px 3.5px 1.5px 3.5px;
+ font-size: 3rem;
+ color:#6e6e6e;
}
- margin-bottom: 20px;
- min-height: 200px;
+.bi-clock {
+ border: 1px solid #c0c0c0;
+ border-radius: 8px;
+ padding: 3px 3.5px 1.5px 3.5px;
+ font-size: 3rem;
+ color:#6e6e6e;
}
-.bi-suitcase-lg-fill { /* Bootstrap Icon */
+.bi-info-circle {
+ padding: 3px 3.5px 1.5px 3.5px;
+ font-size: 1.5rem;
+ vertical-align: middle;
+ color:#6e6e6e;
+}
+.allocation-table-wrapper {
+ 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 {
+ /* 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;
+}
+.allocation-table th,
+.allocation-table td {
+ text-align: left;
+ white-space: nowrap;
+ padding: 4px 10px 4px 0;
+ line-height: 1.3;
+}
+.allocation-table th {
+ font-weight: 700;
+ padding-top: 10px;
+}
+
+.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;
}
@@ -52,3 +98,15 @@
flex-direction: column;
}
}
+
+/* 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-summary {
+ flex-direction: column;
+ gap: 0;
+ }
+ .allocation-columns .allocation-table {
+ flex-basis: 100%;
+ }
+}
diff --git a/app/static/js/departmentPortal.js b/app/static/js/departmentPortal.js
index cb2023a3..06ae72e7 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 a9b56e29..d82a4f3f 100644
--- a/app/templates/main/departmentPortal.html
+++ b/app/templates/main/departmentPortal.html
@@ -33,8 +33,50 @@
{% if department %} {{department.DEPT_NAME}} Portal {% e
{% if department %}
-
- Insert Allocations Card Here
+
+
diff --git a/database/demo_data.py b/database/demo_data.py
index d1e500bb..c002a2e0 100644
--- a/database/demo_data.py
+++ b/database/demo_data.py
@@ -14,6 +14,7 @@
from app.models.user import User
from app.models.term import Term
from app.models.laborStatusForm import LaborStatusForm
+from app.models.laborReleaseForm import LaborReleaseForm
from app.models.formHistory import FormHistory
from app.models.notes import Notes
from app.models.supervisorDepartment import SupervisorDepartment
@@ -510,6 +511,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",
@@ -743,6 +760,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,
@@ -1830,3 +1965,121 @@
).on_conflict_replace().execute()
print(" * position description sections added")
+
+
+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")
+
+
+dummy_lsf = [
+ {
+ "laborStatusFormID": 13,
+ "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": 14,
+ "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": 15,
+ "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": 16,
+ "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": 17,
+ "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/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_allocationManager.py b/tests/code/test_allocationManager.py
index eb3a149f..7b4fe6e7 100644
--- a/tests/code/test_allocationManager.py
+++ b/tests/code/test_allocationManager.py
@@ -260,6 +260,55 @@ def test_getContractedAllocations(testLaborStatusForm, testTerm, testDepartment,
assert contractedAllocation['break_hours'] == 500
@pytest.mark.integration
+def test_getContractedAllocations_withoutAnAllocationRow(testLaborStatusForm, testTerm, testDepartment, testFormHistory):
+ '''
+ getContractedAllocations must not require an Allocation row to exist for
+ the department/term (e.g. before one has been created or finalized) -
+ it should still report the LaborStatusForm-derived counts.
+ '''
+ contractedAllocation = getContractedAllocations(testTerm.termCode, testDepartment.departmentID)
+ assert contractedAllocation['used_15'] == 1
+ assert contractedAllocation['break_hours'] == 500
+
+@pytest.mark.integration
+def test_getContractedAllocations_sumsBreakHoursAcrossAcademicYearCode(testDepartment, testStudent, testSupervisor, testUser):
+ '''
+ A department can have approved break-term contracts under both a specific
+ term and that year's academic-year "00" bucket term - break_hours should
+ sum both, not silently keep only whichever one the query happens to see
+ first.
+ '''
+ specificTerm = Term.create(termCode=200610)
+ academicYearTerm = Term.create(termCode=200600) # matches testTerm's code
+
+ specificTermForm = LaborStatusForm.create(
+ laborStatusFormID=9001, termCode=specificTerm, studentSupervisee=testStudent,
+ supervisor_id=testSupervisor.ID, department=testDepartment, jobType="Primary", WLS=1,
+ POSN_TITLE="Specific Term Break", POSN_CODE="S9001", contractHours=100, weeklyHours=None,
+ )
+ FormHistory.create(
+ formHistoryID=9001, formID=specificTermForm, historyType="Labor Status Form",
+ createdBy=testUser.userID, createdDate="2025-03-02", status="Approved",
+ )
+
+ academicYearForm = LaborStatusForm.create(
+ laborStatusFormID=9002, termCode=academicYearTerm, studentSupervisee=testStudent,
+ supervisor_id=testSupervisor.ID, department=testDepartment, jobType="Primary", WLS=1,
+ POSN_TITLE="Academic Year Break", POSN_CODE="S9002", contractHours=250, weeklyHours=None,
+ )
+ FormHistory.create(
+ formHistoryID=9002, formID=academicYearForm, historyType="Labor Status Form",
+ createdBy=testUser.userID, createdDate="2025-03-02", status="Approved",
+ )
+
+ try:
+ contractedAllocation = getContractedAllocations(specificTerm.termCode, testDepartment.departmentID)
+ assert contractedAllocation['break_hours'] == 350 # 100 + 250, both terms summed
+ finally:
+ specificTermForm.delete_instance()
+ academicYearForm.delete_instance()
+ specificTerm.delete_instance()
+ academicYearTerm.delete_instance()
def test_getBreakContracts(testBreakLaborStatusForm, testBreakTerm, testDepartment,testTerm):
# Test that the formHistory object exists
@@ -295,4 +344,4 @@ def test_getBreakContracts(testBreakLaborStatusForm, testBreakTerm, testDepartme
breakContractHours = getBreakContracts(testBreakTerm, testDepartment)
assert breakContractHours == 0
-
\ No newline at end of file
+