diff --git a/app/controllers/admin_routes/__init__.py b/app/controllers/admin_routes/__init__.py index ff3f60844..c9bceb785 100644 --- a/app/controllers/admin_routes/__init__.py +++ b/app/controllers/admin_routes/__init__.py @@ -13,7 +13,7 @@ def injectGlobalData(): return {'currentUser': currentUser, 'lastStaticUpdate': lastStaticUpdate} -from app.controllers.admin_routes import manage_departments +from app.controllers.admin_routes import manageDepartments from app.controllers.admin_routes import termManagement from app.controllers.admin_routes import adminManagement from app.controllers.admin_routes import allPendingForms diff --git a/app/controllers/admin_routes/manageDepartments.py b/app/controllers/admin_routes/manageDepartments.py new file mode 100644 index 000000000..308af4942 --- /dev/null +++ b/app/controllers/admin_routes/manageDepartments.py @@ -0,0 +1,124 @@ +from datetime import date + +from flask import g, request, redirect, jsonify, abort + +from app.controllers.admin_routes import * +from app.login_manager import require_login + +from app.controllers.admin_routes import admin +from app.controllers.errors_routes.handlers import * + +from app.models.formHistory import FormHistory +from app.models.user import * +from app.models.term import * +from app.models.department import * +from app.models.allocation import * +from app.models.laborStatusForm import * + +from app.logic.manageDepartments import * + + + +@admin.route('/admin/manageDepartments/', methods=['GET']) +@admin.route('/admin/manageDepartments/', methods=['GET']) +def manageDepartments(academicYear = None): + """ + Returns the Manage Departments page, which allows the admin to view all the departments + and their allocations. + """ + + # Checking Admin Rights + currentUser = require_login() + if not currentUser: # If the current user is not logged in + return render_template('errors/403.html') + if not currentUser.isLaborAdmin: + if currentUser.student: + return redirect('/laborHistory/' + currentUser.student.ID) + elif currentUser.supervisor: + return render_template('errors/403.html'), 403 + + + # The condition below may be deleted if the routing to the Manage Departments page is changed. + if academicYear == None: + academicYear = g.openTerm.termCode + else: + academicYear = int(academicYear) + + + currentAY, previousAY, nextAY = generateAdjacentYears(academicYear) + chosenAY = Term.get(Term.termCode == academicYear) + + breakHoursByDepartment = {row["department"]: str(row["totalHours"] or 0) for row in getUsedBreakHours(chosenAY)} + + activeDepartments = getActiveDepartmentsWithAllocation(chosenAY) + inactiveDepartments = Department.select().where(Department.isActive == False) + + allocationStatus = { + department.departmentID: getAllocationStatus(chosenAY, department) + for department in activeDepartments + } + + allSupervisors= Supervisor.select().order_by(Supervisor.LAST_NAME) + + return render_template( 'admin/manageDepartments.html', + activeDepartments = activeDepartments, + inactiveDepartments = inactiveDepartments, + allSupervisors = allSupervisors, + currentAY = currentAY, + previousAY = previousAY, + nextAY = nextAY, + academicYear = chosenAY.termName, + breakHoursByDepartment = breakHoursByDepartment, + allocationStatus = allocationStatus + ) + + + +@admin.route('/admin/complianceStatus', methods=['POST']) +def complianceStatusCheck(): + """ + This function changes the compliance status in the database for labor status forms. + It works in collaboration with the ajax call in manageDepartments.js + """ + try: + rsp = request.get_json() + if rsp: + department = Department.get(int(rsp['deptName'])) + department.departmentCompliance = not department.departmentCompliance + department.save() + return jsonify({"Success": True}) + except Exception as e: + print(e) + return jsonify({"Success": False}) + + + +@admin.route('/admin/manageDepartments///allocationReview', methods=['GET']) +def allocationReview(org=None, account=None): + """ + Returns the Allocation Review page/form, which can only be accessed through + the Manage Departments page. + """ + + # Retrieving the departments based on the org and account numbers + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + abort(404) + + # Checking admin rights + currentUser = require_login() + if not currentUser: # If the current user is not logged in + return render_template('errors/403.html') + if not currentUser.isLaborAdmin: + if currentUser.student: + return redirect('/laborHistory/' + currentUser.student.ID) + elif currentUser.supervisor: + return render_template('errors/403.html'), 403 + + # Retrieving the next year + # DON'T DELETE THE UNDERSCORES + _, _, nextAY = generateAdjacentYears() + # The generateAdjacentYears() function returns a tuple of three elements, and we only need the third value + + return render_template('admin/allocationReview.html', department = dept, nextAY = nextAY) \ No newline at end of file diff --git a/app/controllers/admin_routes/manage_departments.py b/app/controllers/admin_routes/manage_departments.py deleted file mode 100644 index 6b16877cb..000000000 --- a/app/controllers/admin_routes/manage_departments.py +++ /dev/null @@ -1,102 +0,0 @@ -from app.controllers.admin_routes import * -from app.models.user import * -from app.models.supervisorDepartment import SupervisorDepartment -from app.login_manager import require_login -from app.logic.search import getSupervisorsForDepartment -from app.controllers.admin_routes import admin -from app.controllers.errors_routes.handlers import * -#from app.models.manageDepartments import * -from app.models.term import * -from flask_bootstrap import bootstrap_find_resource -from app.models.department import * -from flask import request, redirect -from flask import jsonify -from playhouse.shortcuts import model_to_dict -from app.logic.tracy import Tracy - -@admin.route('/admin/manageDepartments', methods=['GET']) -# @login_required -def manage_departments(): - """ - Updates the Labor Status Forms database with any new departments in the Tracy database on page load. - Returns the departments to be used in the HTML for the manage departments page. - """ - try: - currentUser = require_login() - if not currentUser: # Not logged in - return render_template('errors/403.html') - if not currentUser.isLaborAdmin: # Not an admin - if currentUser.student: # logged in as a student - return redirect('/laborHistory/' + currentUser.student.ID) - elif currentUser.supervisor: - return render_template('errors/403.html'), 403 - - - activeDepartments = Department.select().where(Department.isActive == True) - inactiveDepartments = Department.select().where(Department.isActive == False) - allSupervisors= Supervisor.select().order_by(Supervisor.LAST_NAME) - return render_template( 'admin/manageDepartments.html', - title = ("Manage Departments"), - activeDepartments = activeDepartments, - inactiveDepartments = inactiveDepartments, - allSupervisors = allSupervisors - ) - except Exception as e: - print("Error Loading all Departments", e) - return render_template('errors/500.html'), 500 - -@admin.route("/admin/manageDepartments/", methods=['GET']) -def getSupervisorsInDepartment(departmentID): - currentUser = require_login() - if not currentUser: # Not logged in - return render_template('errors/403.html') - if not currentUser.isLaborAdmin: # Not an admin - if currentUser.student: # logged in as a student - return redirect('/laborHistory/' + currentUser.student.ID) - elif currentUser.supervisor: - return render_template('errors/403.html'), 403 - - supervisors = getSupervisorsForDepartment(departmentID) - supervisors = [model_to_dict(supervisor) for supervisor in supervisors] - return jsonify(supervisors) - -@admin.route('/admin/manageDepartments/removeSupervisorFromDepartment', methods=['POST']) -def removeSupervisorFromDepartment(): - try: - currentUser = require_login() - if not currentUser: # Not logged in - return render_template('errors/403.html') - if not currentUser.isLaborAdmin: # Not an admin - if currentUser.student: # logged in as a student - return redirect('/laborHistory/' + currentUser.student.ID) - elif currentUser.supervisor: - return render_template('errors/403.html'), 403 - - formData = request.form - supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor = formData['supervisorID'], department = formData['departmentID']) - - if supervisorDeptRecord: - supervisorDeptRecord.delete_instance() - return "True" - else: - return "False" - - except Exception as e: - print(f'Could not remove user from department: {e}') - return "", 500 - -@admin.route('/admin/complianceStatus', methods=['POST']) -def complianceStatusCheck(): - """ - This function changes the compliance status in the database for labor status forms. It works in collaboration with the ajax call in manageDepartments.js - """ - try: - rsp = eval(request.data.decode("utf-8")) # This fixes byte indices must be intergers or slices error - if rsp: - department = Department.get(int(rsp['deptName'])) - department.departmentCompliance = not department.departmentCompliance - department.save() - return jsonify({"Success": True}) - except Exception as e: - print(e) - return jsonify({"Success": False}) diff --git a/app/controllers/admin_routes/termManagement.py b/app/controllers/admin_routes/termManagement.py index 8a17d169f..30e7673f1 100644 --- a/app/controllers/admin_routes/termManagement.py +++ b/app/controllers/admin_routes/termManagement.py @@ -37,26 +37,42 @@ def createTerms(termYear): This function creates the terms for the given Academic Year """ code = termYear * 100 + createdTerms = [] for i in range(8): try: if i == 0: - Term.create(termCode = code, termName = "AY {}-{}".format(termYear, termYear + 1), isAcademicYear=True) + term = Term.create(termCode = code, termName = "AY {}-{}".format(termYear, termYear + 1), isAcademicYear=True) elif i == 1: - Term.create(termCode = (code + 11), termName = "Fall {}".format(termYear)) + term = Term.create(termCode = (code + 11), termName = "Fall {}".format(termYear)) elif i == 7: - Term.create(termCode = (code + 4), termName = "Fall Break {}".format(termYear), isBreak=True) + term = Term.create(termCode = (code + 4), termName = "Fall Break {}".format(termYear), isBreak=True) elif i == 2: - Term.create(termCode = (code + 1), termName = "Thanksgiving Break {}".format(termYear), isBreak=True) + term = Term.create(termCode = (code + 1), termName = "Thanksgiving Break {}".format(termYear), isBreak=True) elif i == 3: - Term.create(termCode = (code + 2), termName = "Christmas Break {}".format( termYear), isBreak=True) + term = Term.create(termCode = (code + 2), termName = "Christmas Break {}".format( termYear), isBreak=True) elif i == 4: - Term.create(termCode = (code + 12), termName = "Spring {}".format(termYear + 1)) + term = Term.create(termCode = (code + 12), termName = "Spring {}".format(termYear + 1)) elif i == 5: - Term.create(termCode = (code + 3), termName = "Spring Break {}".format(termYear + 1), isBreak=True) + term = Term.create(termCode = (code + 3), termName = "Spring Break {}".format(termYear + 1), isBreak=True) elif i == 6: - Term.create(termCode = (code + 13), termName = "Summer {}".format(termYear + 1), isBreak=True, isSummer=True) + term = Term.create(termCode = (code + 13), termName = "Summer {}".format(termYear + 1), isBreak=True, isSummer=True) except IntegrityError as e: - pass + termCodeMap = { + 0: code, + 1: code + 11, + 2: code + 1, + 3: code + 2, + 4: code + 12, + 5: code + 3, + 6: code + 13, + 7: code + 4, + } + term = Term.get_or_none(Term.termCode == termCodeMap[i]) + + if term is not None: + createdTerms.append(term) + + return createdTerms @admin.route("/termManagement/setDate/", methods=['POST']) def ourDate(): diff --git a/app/logic/manageDepartments.py b/app/logic/manageDepartments.py new file mode 100644 index 000000000..41e96bd50 --- /dev/null +++ b/app/logic/manageDepartments.py @@ -0,0 +1,195 @@ +from flask import g, abort +from peewee import fn + +from app.controllers.main_routes import departmentPortal +from app.controllers.admin_routes.termManagement import createTerms + +from app.models.laborStatusForm import * +from app.models.formHistory import * +from app.models.allocation import * +from app.models.department import * +from app.models.term import * + +from app.login_manager import require_login + + + +def generateAdjacentYears(academicYearTermCode=None): + """ + Generates the current, the previous, and the following academic years. + """ + + currentYear = g.openTerm.termCode // 100 + previousYear = currentYear - 1 + nextYear = currentYear + 1 + + + currentAYCode = currentYear * 100 + previousAYCode = previousYear * 100 + nextAYCode = nextYear * 100 + + # Admins cannot view allocations for the years that are beyond the current, the previous, or the following academic year + if academicYearTermCode not in (None, currentAYCode, previousAYCode, nextAYCode): + abort(400) + + + currentAY, _ = Term.get_or_create( + termCode=currentAYCode, + defaults={"termName": "AY {}-{}".format(currentYear, currentYear + 1), "isAcademicYear": True} + ) + + previousAY, _ = Term.get_or_create( + termCode=previousAYCode, + defaults={"termName": "AY {}-{}".format(previousYear, previousYear + 1), "isAcademicYear": True} + ) + + nextAY, _ = Term.get_or_create( + termCode=nextAYCode, + defaults={"termName": "AY {}-{}".format(nextYear, nextYear + 1), "isAcademicYear": True} + ) + + return (currentAY, previousAY, nextAY) + + + + +#################################################################################################################################### +# Everything below this line will eventually be deleted + + + + + +def getUsedBreakHours(term): + """ + Returns the total number of break hours used by each department for a given term. + """ + + # THE PREVIOUS IMPLEMENTATION OF THIS FUNCTION (CAN BE USED IN CASE THE CURRENT IMPLEMENTATION DOESN'T WORK PROPERLY) + # totalBreakSum = FormHistory.select(fn.SUM(LaborStatusForm.contractHours)).where( (FormHistory.historyType_id == "Labor Status Form ") & (FormHistory.status_id == "Approved")) + + totalBreakSum = ( + FormHistory + .select( + LaborStatusForm.department, + LaborStatusForm.termCode.termCode, + fn.SUM(LaborStatusForm.contractHours).alias('totalHours') + + ) + .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 == term) + ) + .group_by(LaborStatusForm.department, LaborStatusForm.termCode).dicts() +) + + return totalBreakSum + + + +# USED IN THE getActiveDepartmentsWithAllocation() FUNCTION +def getLSFCountPrimaries(currentTerm, department): + """ + Returns the count of primary LSFs for a given department during a given term. (WIP) + """ + lsfCountPrimaries = FormHistory.select().join(LaborStatusForm).join(Department).where(FormHistory.status == "Approved", LaborStatusForm.termCode == currentTerm.termCode, LaborStatusForm.jobType == "Primary", Department.departmentID == department.departmentID).count() + return lsfCountPrimaries + + + +# USED IN THE getActiveDepartmentsWithAllocation() FUNCTION +def getLSFCountSecondaries(currentTerm, department): + """ + Returns the count of secondary LSFs for a given department during a given term. (WIP) + """ + lsfCountSecondaries = FormHistory.select().join(LaborStatusForm).join(Department).where(FormHistory.status == "Approved", LaborStatusForm.termCode == currentTerm.termCode, LaborStatusForm.jobType == "Secondary", Department.departmentID == department.departmentID).count() + return lsfCountSecondaries + + + +def getActiveDepartmentsWithAllocation(term): + """ + Returns a list of active departments with allocations for the given term. + """ + + # This was left just incase anything went wrong. Delete this if everything works as expected. Not necessary in current implementation. + # activeDepartments = Department.select().where(Department.isActive == True) + # allAllocations = Allocation.select().where(Allocation.termCode == currentAY) + + activeDepartments = (Department + .select(Department, Allocation) + .join(Allocation) + .where( + Department.isActive == True, + Allocation.termCode == term.termCode + ) + ) + + for dept in activeDepartments: + dept.totalPrimaries = (dept.allocation.primary_10 + dept.allocation.primary_12 + dept.allocation.primary_15 + dept.allocation.primary_20) + dept.totalSecondaries = (dept.allocation.secondary_5 + dept.allocation.secondary_10) + + dept.lsfCountPrimaries = getLSFCountPrimaries(term, dept) + dept.lsfCountSecondaries = getLSFCountSecondaries(term, dept) + + return activeDepartments + + + +def getAllocationStatus(term, department): + """ + Returns the allocation status for a given department during a given term. + """ + allocation = Allocation.get( + (Allocation.termCode == term) & + (Allocation.department == department) + ) + return allocation.isFinal + + + + + + +# THE FUNCTIONS BELOW ARE NO LONGER USED IN THE CODE (BECAUSE WE CAN ONLY CHOOSE AN ACADEMIC YEAR IN THE CODE). +# IF SOMETHING CHANGES,YOU CAN USE THE CODE BELOW + +# # USED IN THE generateTermsForAdjacentYears() FUNCTION +# def generateTerms(termCode): +# """ +# Generates all the terms in an academic year. +# """ + +# # Truncating term codes to hundreds. That's how we get the academic year. +# academicYearCode = (termCode // 100) + +# return createTerms(academicYearCode) + + + +# def generateTermsForAdjacentYears(academicYear): +# """ +# Generates all the terms for the current, the previous, and the future academic years. +# """ + +# previousAYCode = g.openTerm.termCode - 100 +# currentAYCode = g.openTerm.termCode +# nextATCode = g.openTerm.termCode + 100 + +# if (academicYear != previousAYCode) and (academicYear != currentAYCode) and (academicYear != nextATCode): +# abort(400) + +# PreviousAYTerms = generateTerms(previousAYCode) +# CurrentAYTerms = generateTerms(currentAYCode) +# NextAYTerms = generateTerms(nextATCode) + +# return (PreviousAYTerms, CurrentAYTerms, NextAYTerms) \ No newline at end of file diff --git a/app/static/css/allocationReview.css b/app/static/css/allocationReview.css new file mode 100644 index 000000000..2d72c7067 --- /dev/null +++ b/app/static/css/allocationReview.css @@ -0,0 +1,52 @@ +@media(min-width:970px) and (max-width:1240px) { + .container { + width: 80%; + } +} + +@media(min-width:1240px) and (max-width:1800px) { + .container { + width: 55%; + } +} + +@media(min-width:1800px) { + .container { + width: 40%; + } +} + +#allocationReviewSubtitle{ + margin-bottom: 30px; +} + +#breakHours { + margin-top: 0px; +} + +.numericSpinner { + width: 55px; +} + +#requestedPositions { + margin-top: 30px; + margin-bottom: -10px; +} + +.noBorders { + border: none !important; +} + + +#allocationJustification { + margin-top: 30px; +} + +.unresizeable { + resize: none; +} + +#allocationReviewNote { + max-width:70%; + color: grey; +} \ No newline at end of file diff --git a/app/static/css/manageDepartments.css b/app/static/css/manageDepartments.css index 81fcfb05a..2d2a2f51f 100755 --- a/app/static/css/manageDepartments.css +++ b/app/static/css/manageDepartments.css @@ -1,14 +1,3 @@ -/*.flasher{ - margin-top: 100px; -} - -#flash_container { - margin-top: 100px; - margin-right: 140px; - margin-left: 20px; -} -*/ - h1 { text-align: center; padding-bottom: 5px; @@ -29,9 +18,15 @@ h1 { width:20px; } .complianceBtn{ - width:150px; + width:140px; } #flasher{ z-index: 999999; +} + +#activeDepartmentsTable th, +#activeDepartmentsTable td { + vertical-align: middle; + text-align: center; } \ No newline at end of file diff --git a/app/static/js/allocationReview.js b/app/static/js/allocationReview.js new file mode 100644 index 000000000..49c14f105 --- /dev/null +++ b/app/static/js/allocationReview.js @@ -0,0 +1,6 @@ +$(document).ready( function(){ + // not allowing users to type anything in a numeric spinner + $("input[type='number'].numericSpinner").keypress(function (evt) { + evt.preventDefault(); + }); +}); \ No newline at end of file diff --git a/app/static/js/manageDepartments.js b/app/static/js/manageDepartments.js index 814f6138e..add7a8d62 100755 --- a/app/static/js/manageDepartments.js +++ b/app/static/js/manageDepartments.js @@ -1,19 +1,32 @@ // Opens collapse menu for this page $("#admin").collapse("show"); + + $(document).ready( function(){ activeDepartmentsTable = $('#activeDepartmentsTable'); activeDepartmentsTable.DataTable({ - pageLength: 25 + columnDefs: [{ + targets: '.noSorting', + orderable: false // hiding the sort icon only on the third, fifth and sixth columns + }], + pageLength: 25, + language: { + lengthMenu: " _MENU_ entries per page" + } }); inactiveDepartmentsTable = $('#inactiveDepartmentsTable'); inactiveDepartmentsTable.DataTable({ - pageLength: 25 + pageLength: 25, + language: { + lengthMenu: " _MENU_ entries per page" + } }); $("#inactiveTable").hide(); + $("#activeTab").on("click", function() { $("#activeTab").addClass("active"); $("#activeTable").show(); @@ -21,6 +34,7 @@ $(document).ready( function(){ $("#inactiveTable").hide(); }) + $("#inactiveTab").on("click", function() { $("#activeTab").removeClass("active"); $("#activeTable").hide(); @@ -28,16 +42,20 @@ $(document).ready( function(){ $("#inactiveTable").show(); }) + attachModalToDepartment() $('.deptTable').on('draw.dt', function() { attachModalToDepartment() }) + + $('#manageDepartmentSupervisorModal').on('hidden.bs.modal', function() { clearDropdowns() }) }); + function attachModalToDepartment() { $('.deptTable .departmentName').off('click') $('.deptTable .departmentName').on('click', function() { @@ -50,6 +68,7 @@ function attachModalToDepartment() { } + $("#supervisorModalSelect").on('change', function() { let supervisorID = $('#supervisorModalSelect :selected').val() let departmentID = $('#departmentModalSelect').data('department-id') @@ -59,52 +78,6 @@ $("#supervisorModalSelect").on('change', function() { -function showSupervisorsInDepartment(departmentID) { - $.ajax({ - method: "GET", - url: `/admin/manageDepartments/${departmentID}`, - success: function(supervisors) { - let supervisorContent = '
' - for (let i=0; i -
${supervisors[i]['ID']} ${supervisorFirstName} ${supervisors[i]['LAST_NAME']}
-
Remove
- `)} - supervisorContent += ("
") - $('#manageSupervisorContent .modal-body .changing-content').replaceWith(supervisorContent) - - $('#manageDepartmentSupervisorModal').modal('show') - $('.removeSupervisorFromDepartment').on('click', removeSupervisorFromDepartment) - } - }) - } - -function removeSupervisorFromDepartment () { - let departmentID = $(`#${this.id}`).data('department') - let supervisorID = $(`#${this.id}`).data('supervisor') - let data = {"supervisorID": supervisorID, "departmentID": departmentID} - $.ajax({ - method: "POST", - url: "/admin/manageDepartments/removeSupervisorFromDepartment", - data: data, - success: function(response) { - if (response == "True") { - msgFlash("Supervisor has been removed from department.", 'success') - showSupervisorsInDepartment(departmentID) - } else { - msgFlash("Supervisor is not a member of this department.", "warning") - } - }, - error: function() { - msgFlash("Failed to remove supervisor, please try again.", "fail") - }, -}) -} - function status(department, dept_name) { /* POSTs the compliance status change for the department. Updates UI with correct button and feedback to user. diff --git a/app/templates/admin/allocationReview.html b/app/templates/admin/allocationReview.html new file mode 100644 index 000000000..73b929e3a --- /dev/null +++ b/app/templates/admin/allocationReview.html @@ -0,0 +1,112 @@ +{% extends "base.html" %} {% block styles %} {{super()}} + + +{% endblock %} {% block scripts %} {{super()}} + + +{% endblock %} {% block app_content %} + +

+ + Allocation Review + +

+ +

+ + Review to Confirm an Allocation Request + +

+ +
+

+ + {{department.DEPT_NAME}} Department +

+ +

+ {{nextAY.termName.split(" ")[1]}} +

+ +
+ +

+ + + (requested: 120) +

+ +

+ +

+ +
+
+

+ + Primary + +

+ +

+ 10 hours:  + +  (requested: 120) +

+ +

+ 12 hours:  + +  (requested: 120) +

+ +

+ 15 hours:  + +  (requested: 120) +

+
+
+

+ + Secondary + +

+ +

+ 5 hours:    + +  (requested: 120) +

+ +

+ 10 hours:  + +  (requested: 120) +

+
+
+ +

+ +

+ + +
+ +
+ + *To submit the form, you must either specify the number of extra break hours or fill in one of the fields in the Requested Positions section. + +
+ +
+ + +
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/manageDepartments.html b/app/templates/admin/manageDepartments.html index ddb1b0431..d8cb2f5ca 100755 --- a/app/templates/admin/manageDepartments.html +++ b/app/templates/admin/manageDepartments.html @@ -1,136 +1,256 @@ -{% extends "base.html" %} - -{% block styles %} -{{super()}} - - -{% endblock %} - -{% block scripts %} -{{super()}} - - - -{% endblock %} - -{% block app_content %} +{% extends "base.html" %} {% block styles %} {{super()}} + + +{% endblock %} {% block scripts %} {{super()}} + + + +{% endblock %} {% block app_content %}
Click to Skip
-
-
- Click to Skip -
-
-

Manage Departments

+
+ Click to Skip +
+
+

Manage Departments

-

Position descriptions are up to date.

-

Position descriptions are not up to date

+

+ Monitor allocation usage, compliance, and department position needs + across campus. +

-
-
-
+
+ +

+ + Position descriptions are up to date. +

+

+ + Position descriptions are not up to date. +

+ +
-
- -
-
- + + +
+
+
+ {% include "snips/uploadAllocations.html" %} + + +
+
+
+ + - -
-
- - - - - - - - - {% for department in activeDepartments %} - - - - - {% endfor %} - -
DepartmentStatus
{{department.DEPT_NAME}}({{department.ORG}}, {{department.ACCOUNT}}) - -
-
+
  • + + Future AY: {{ nextAY.termName.split(" ")[1] }} + +
  • +
    +
    -
    - - - - - - - - {% for department in inactiveDepartments %} - - - - {% endfor %} - -
    Department
    {{department.DEPT_NAME}}({{department.ORG}}, {{department.ACCOUNT}})
    -
    + +
    + {% include "snips/annualAllocationReview.html" %} + + {% include "snips/annualPositionReview.html" %} + +
    -
    -
    +
    + +
    +
    +
    +
    + + +
    + + + +
    + + + + + + + + + + + + + {% for department in activeDepartments %} + + + + + + + + + + + + + {% endfor %} + +
    DepartmentStatusPositionsAllocation Status
    ({{ academicYear }})
    Break Hours
    (Used/Given)
    Actions
    + {{department.DEPT_NAME}}
    ({{department.ORG}}, + {{department.ACCOUNT}}) +
    + + + + Primary: + {{department.lsfCountPrimaries}} of {{department.totalPrimaries}} + +
    + + Secondary: + {{department.lsfCountSecondaries}} of {{department.totalSecondaries}} + +
    + + + + + {{ breakHoursByDepartment.get(department.departmentID, 0) }} / {{ department.allocation.breakHours }} + + + +
    +
    + +
    + + + + + + + + {% for department in inactiveDepartments %} + + + + {% endfor %} + +
    Department
    + {{department.DEPT_NAME}}({{department.ORG}}, + {{department.ACCOUNT}}) +
    +
    +
    +
    +
    - -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/snips/annualAllocationReview.html b/app/templates/snips/annualAllocationReview.html new file mode 100644 index 000000000..11255cfbe --- /dev/null +++ b/app/templates/snips/annualAllocationReview.html @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/app/templates/snips/annualPositionReview.html b/app/templates/snips/annualPositionReview.html new file mode 100644 index 000000000..dac86a31e --- /dev/null +++ b/app/templates/snips/annualPositionReview.html @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/app/templates/snips/uploadAllocations.html b/app/templates/snips/uploadAllocations.html new file mode 100644 index 000000000..ec9d7e991 --- /dev/null +++ b/app/templates/snips/uploadAllocations.html @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/database/demo_data.py b/database/demo_data.py index 20c4517d7..b7a59297e 100644 --- a/database/demo_data.py +++ b/database/demo_data.py @@ -509,34 +509,41 @@ ############################# # Department ############################# +############################# +# Active Departments +############################# departments = [ { "departmentID":1, "DEPT_NAME": "Computer Science", "ACCOUNT": "6740", "ORG": "2114", - "departmentCompliance": 1 + "departmentCompliance": 1, + "isActive": 1 }, { "departmentID":2, "DEPT_NAME": "Technology and Applied Design", "ACCOUNT": "6740", "ORG": "2147", - "departmentCompliance": 1 + "departmentCompliance": 1, + "isActive": 1 }, { "departmentID":3, "DEPT_NAME": "Mathematics", "ACCOUNT": "6740", "ORG": "2150", - "departmentCompliance": 1 + "departmentCompliance": 1, + "isActive": 1 }, { "departmentID":4, "DEPT_NAME": "Biology", "ACCOUNT": "6740", "ORG": "2107", - "departmentCompliance": 1 + "departmentCompliance": 1, + "isActive": 1 }, { "departmentID":5, @@ -545,8 +552,51 @@ "ORG": "4022", "departmentCompliance": 1, "isActive": 1 + }, +############################# +# Inactive Departments +############################# + + { + "departmentID":6, + "DEPT_NAME": "Agriculture and Natural Resources", + "ACCOUNT": "6740", + "ORG": "1441", + "departmentCompliance": 1, + "isActive": 0 + }, + { + "departmentID":7, + "DEPT_NAME": "Art and Art History", + "ACCOUNT": "6740", + "ORG": "2004", + "departmentCompliance": 1, + "isActive": 0 + }, + { + "departmentID":8, + "DEPT_NAME": "Asian Studies", + "ACCOUNT": "6740", + "ORG": "9801", + "departmentCompliance": 1, + "isActive": 0 + }, + { + "departmentID":9, + "DEPT_NAME": "Appalachian Studies", + "ACCOUNT": "6740", + "ORG": "8787", + "departmentCompliance": 1, + "isActive": 0 + }, + { + "departmentID":10, + "DEPT_NAME": "Music", + "ACCOUNT": "6740", + "ORG": "4805", + "departmentCompliance": 1, + "isActive": 0 } - ] Department.insert_many(departments).on_conflict_replace().execute() print(" * departments added") @@ -556,6 +606,8 @@ ############################# +print("Current year:", "termName") + terms = [ { "termCode": f"202000", @@ -644,6 +696,321 @@ }]).on_conflict_replace().execute() +############################# +# Create Active Labor Status Form for the Break Term +############################# + +# cs department + +LaborStatusForm.insert([{ + "laborStatusFormID": 6, + "termCode_id": f"202500", + "studentName": "Pizza Taker", + "studentSupervisee_id": "B12345773", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 15, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 6, + "formID_id": "6", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 7, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 1, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 3, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 7, + "formID_id": "7", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + + + +# labor department + +LaborStatusForm.insert([{ + "laborStatusFormID": 4, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 5, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-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_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 5, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 5, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-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_id": "Approved" + }]).on_conflict_replace().execute() + +# Biology Department + +LaborStatusForm.insert([{ + "laborStatusFormID": 8, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 4, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 8, + "formID_id": "8", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 9, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 4, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 9, + "formID_id": "9", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + +# Mathematics Department + +LaborStatusForm.insert([{ + "laborStatusFormID": 10, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 3, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 10, + "formID_id": "10", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 11, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 3, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).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_id": "Approved" + }]).on_conflict_replace().execute() + +#Technology and Applied Design Department + +LaborStatusForm.insert([{ + "laborStatusFormID": 12, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 2, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).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_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 13, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 2, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 13, + "formID_id": "13", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 14, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 2, + "jobType": "Secondary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 14, + "formID_id": "14", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() + +LaborStatusForm.insert([{ + "laborStatusFormID": 15, + "termCode_id": f"202500", + "studentName": "Elaheh Jamali", + "studentSupervisee_id": "B00730361", + "supervisor_id": "B12361006", + "department_id": 2, + "jobType": "Primary", + "WLS": 1, + "POSN_TITLE": "Media Technician", + "POSN_CODE": "S61409", + "contractHours": 5, + "startDate": f"2025-04-01", + "endDate": "2025-09-01" + }]).on_conflict_replace().execute() + +FormHistory.insert([{ + "formHistoryID": 15, + "formID_id": "15", + "historyType_id": "Labor Status Form", + "createdBy_id": 1, + "createdDate": f"2025-04-14", + "status_id": "Approved" + }]).on_conflict_replace().execute() ############################# # admin Notes @@ -757,7 +1124,7 @@ { "termCode": 202500, "department": 2, - "isFinal": False, + "isFinal": True, "approvedOn": None, "approvedBy": None, "justification": "Increase in student enrollment due to exodous from CS department", @@ -772,7 +1139,7 @@ { "termCode": 202500, "department": 1, - "isFinal": False, + "isFinal": True, "approvedOn": None, "approvedBy": None, "justification": "We are hiring more students to help with the increased workload in the department", @@ -802,7 +1169,7 @@ { "termCode": 202500, "department": 5, - "isFinal": False, + "isFinal": True, "approvedOn": None, "approvedBy": None, "justification": "Due to rapid department growth, we need to hire more students to help with the increased workload", diff --git a/tests/code/test_manageDepartments.py b/tests/code/test_manageDepartments.py new file mode 100644 index 000000000..77cb2ee38 --- /dev/null +++ b/tests/code/test_manageDepartments.py @@ -0,0 +1,113 @@ +import pytest +import json +from werkzeug.exceptions import BadRequest + +from flask import g +from flask_wtf.csrf import CSRFProtect + +from app import app +from app.models import mainDB +from app.models.term import Term +from app.controllers.admin_routes import manageDepartments + +from app.logic.manageDepartments import * + + +# The following test file is for testing the manageDepartments logic file and its associated functions and queries. +# It is designed to ensure that the manageDepartments functionality works as expected and returns the correct data. + + +@pytest.mark.integration +def test_generateAdjacentYears(): + with app.app_context(): + with mainDB.atomic() as transaction: + + ################ THE FIRST TEST ################ + ################ TESTING WHETHER THE generateAdjacentYear() FUNCTION WORKS AT ALL + g.openTerm, _ = Term.get_or_create( + termCode = 202500, + defaults={"termName": "AY 2025-2026", "isAcademicYear": True} + ) + + # + currentYear, previousYear, followingYear = generateAdjacentYears(202500) + + assert currentYear.termCode == 202500 + assert currentYear.termName == "AY 2025-2026" + + assert previousYear.termCode == 202400 + assert previousYear.termName == "AY 2024-2025" + + assert followingYear.termCode == 202600 + assert followingYear.termName == "AY 2026-2027" + + + ################ THE SECOND TEST ################ + ######### TESTING VARIOUS EDGE CASES ############ + with pytest.raises(BadRequest): + generateAdjacentYears(202300) + transaction.rollback() + + with pytest.raises(BadRequest): + generateAdjacentYears(202200) + transaction.rollback() + + with pytest.raises(BadRequest): + generateAdjacentYears(2025) + transaction.rollback() + + with pytest.raises(BadRequest): + generateAdjacentYears(True) + transaction.rollback() + + with pytest.raises(BadRequest): + generateAdjacentYears(False) + transaction.rollback() + + with pytest.raises(BadRequest): + generateAdjacentYears("SELECT lsf DELETE *") + transaction.rollback() + + + ################ THE THIRD TEST ################ + ############# MISCELLANEOUS TESTS ############# + g.openTerm, _ = Term.get_or_create( + termCode = 198200, + defaults={"termName": "AY 1982-1983", "isAcademicYear": True} + ) + + # Testing different years + currentYear, previousYear, followingYear = generateAdjacentYears(198200) + + assert currentYear.termCode == 198200 + assert currentYear.termName == "AY 1982-1983" + + assert previousYear.termCode == 198100 + assert previousYear.termName == "AY 1981-1982" + + assert followingYear.termCode == 198300 + assert followingYear.termName == "AY 1983-1984" + + # Testing data types + assert isinstance(currentYear.termCode, int) + assert isinstance(previousYear.termCode, int) + assert isinstance(followingYear.termCode, int) + + # Testing whether currentYear.termName is formatted correctly + assert currentYear.termName.split(" ")[0] == "AY" + assert previousYear.termName.split(" ")[0] == "AY" + assert followingYear.termName.split(" ")[0] == "AY" + + assert currentYear.termName.split(" ")[1] == "1982-1983" + assert previousYear.termName.split(" ")[1] == "1981-1982" + assert followingYear.termName.split(" ")[1] == "1983-1984" + + + # Testing the generateAdjacentYears() function without any parameters + currentYear, previousYear, followingYear = generateAdjacentYears() + + assert currentYear.termCode == 198200 + assert previousYear.termCode == 198100 + assert followingYear.termCode == 198300 + + transaction.rollback() \ No newline at end of file