diff --git a/app/__init__.py b/app/__init__.py index 11c90a0a9..354dea139 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,5 @@ import os +from datetime import date from flask import Flask from flask_restful import Api @@ -78,6 +79,23 @@ def load_openTerm(): if term: session['openTerm'] = model_to_dict(term) g.openTerm = term + +def getCurrentAY(): + """ + Returns the current academic year as a tuple + (the year when it starts, and the year when it ends) + """ + today = date.today() + year = today.year + + if today.month < 7: + return year - 1, year + + return year, year + 1 + +@app.before_request +def load_currentAY(): + g.currentAY = getCurrentAY() @app.context_processor def inject_environment(): @@ -86,5 +104,4 @@ def inject_environment(): @app.before_request def queryCount(): if session: - session['querycount'] = 0 - + session['querycount'] = 0 \ No newline at end of file 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..238eb2859 --- /dev/null +++ b/app/controllers/admin_routes/manageDepartments.py @@ -0,0 +1,172 @@ +from datetime import date + +from flask import g, request, redirect, jsonify, abort, flash + +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 * +from app.logic.allocationManager import allocationExists +from app.logic.academicYearManager import getCurrentAndNextAY + + + +@admin.route('/admin/manageDepartments/', methods=['GET']) +def manageDepartments(): + """ + 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 + + currentAY, nextAY = getCurrentAndNextAY() + chosenAY = Term.get(Term.termCode == currentAY.termCode) + + 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, + 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. + """ + + + # getting the name of the currently chosen department (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 current and following academic years + currentAY, nextAY = getCurrentAndNextAY() + + + # checking if the allocation has already been approved + if allocationExists(nextAY.termCode, dept, isFinal=True): + flash("You cannot reapprove an allocation request.", "info") + return redirect('/admin/manageDepartments/') + + + # checking if the department has requested any allocation review + if not allocationExists(nextAY.termCode, dept, isFinal=False): + flash(f"The {dept.DEPT_NAME} department has not requested an allocation review yet.", "info") + return redirect('/admin/manageDepartments/') + + + # getting the current and the requested allocations + currentAlloc = Allocation.get_or_none(Allocation.termCode == currentAY.termCode, Allocation.department == dept, Allocation.isFinal == True) + requestedAlloc = Allocation.get(Allocation.termCode == nextAY.termCode, Allocation.department == dept, Allocation.isFinal == False) + + + return render_template('admin/allocationReview.html', + department = dept, + nextAY = nextAY, + currentAlloc = currentAlloc, + requestedAlloc = requestedAlloc + ) + + + +@admin.route('/admin/allocationReview/approve', methods=['POST']) +def approveAllocationReview(): + + # Retrieving the current and following academic years + currentAY, nextAY = getCurrentAndNextAY() + + # getting the ID of the user who approves the request + approverID = g.currentUser.userID + + # getting the name of the requesting department + requester = request.form.get("requester", type=int, default=None) + + # saving the newly approved allocation + newApprovedAlloc = Allocation.create(termCode = nextAY.termCode, + department = requester, + isFinal = True, + approvedBy = approverID, + approvedOn = date.today(), + primary_10 = request.form.get("primary_10", type=int, default=None), + primary_12 = request.form.get("primary_12", type=int, default=None), + primary_15 = request.form.get("primary_15", type=int, default=None), + primary_20 = request.form.get("primary_20", type=int, default=None), + secondary_5 = request.form.get("secondary_5", type=int, default=None), + secondary_10 = request.form.get("secondary_10", type=int, default=None), + breakHours = request.form.get("breakHours", type=int, default=None) + ) + newApprovedAlloc.save() + + return redirect("/admin/manageDepartments") 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/controllers/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py index b0c33f5e7..2a23ea140 100644 --- a/app/controllers/main_routes/departmentPortal.py +++ b/app/controllers/main_routes/departmentPortal.py @@ -1,9 +1,62 @@ -from flask import render_template, g +from flask import render_template, g, request, redirect, flash +from app.login_manager import require_login from app.controllers.main_routes import main_bp from app.logic.getPositions import getPositions from peewee import DoesNotExist from app.models.department import Department +from app.models.allocation import Allocation from app.models.supervisorDepartment import SupervisorDepartment +from app.logic.allocationRequest import getOrUpdateRequestedAllocation +from app.logic.allocationManager import allocationExists +from app.logic.academicYearManager import getCurrentAndNextAY + + +@main_bp.route('/department///allocations/request', methods=['GET']) +def allocationRequest(org, account): + + # getting the name of the currently chosen department (based on the org and account numbers) + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except DoesNotExist: + return render_template('errors/404.html'), 404 + + + # checking if the user can visit this page + if not g.currentUser.isLaborAdmin: + if not SupervisorDepartment.select().where( + (SupervisorDepartment.supervisor == g.currentUser.supervisor) & + (SupervisorDepartment.department == dept.departmentID) + ).exists(): + return render_template('errors/403.html'), 403 + + + # Retrieving the current and following academic years + currentAY, nextAY = getCurrentAndNextAY() + + + # checking if the allocation has already been approved (in other words, if an approved allocation exists) + if allocationExists(nextAY.termCode, dept, isFinal=True): + flash(f"The allocation for the {nextAY.termName.split(' ')[1]} academic year has already been approved; therefore, you can no longer resubmit it.", "info") + return redirect(f'/department/{org}/{account}') + + + # getting the current approved allocation + currentAlloc = Allocation.get_or_none(Allocation.termCode == currentAY.termCode, Allocation.department == dept, Allocation.isFinal == True) + + + return render_template('main/allocationRequest.html', + department = dept, + nextAY = nextAY, + currentAlloc = currentAlloc + ) + + +@main_bp.route('/allocationRequest/submit', methods=['POST']) +def submitAllocationRequest(): + getOrUpdateRequestedAllocation() + submitter = Department.get(Department.departmentID == request.form.get("submitter", type=int, default=None)) + return redirect(f"/department/{submitter.ORG}/{submitter.ACCOUNT}") + @main_bp.route('/department///positions', methods=['GET']) def managePositions(org, account): diff --git a/app/logic/academicYearManager.py b/app/logic/academicYearManager.py new file mode 100644 index 000000000..400141db8 --- /dev/null +++ b/app/logic/academicYearManager.py @@ -0,0 +1,26 @@ +from flask import g +from app.models.term import * + +def getCurrentAndNextAY(): + """ + Returns two Term peewee objects: one is the current academic year, + and the other is the next academic year (note that a new academic year + begins from the start of July). + """ + + currentYear, nextYear = g.currentAY + + currentAYCode = currentYear * 100 + nextAYCode = nextYear * 100 + + currentAY, _ = Term.get_or_create( + termCode=currentAYCode, + defaults={"termName": "AY {}-{}".format(currentYear, currentYear + 1), "isAcademicYear": True} + ) + + nextAY, _ = Term.get_or_create( + termCode=nextAYCode, + defaults={"termName": "AY {}-{}".format(nextYear, nextYear + 1), "isAcademicYear": True} + ) + + return (currentAY, nextAY) \ No newline at end of file diff --git a/app/logic/allocationManager.py b/app/logic/allocationManager.py index f1c58cbc6..16d707532 100644 --- a/app/logic/allocationManager.py +++ b/app/logic/allocationManager.py @@ -9,15 +9,15 @@ def getAllocation(termCode: int, dept: int, isFinal = True): ''' - This function returns a peewee object containing the selected allocation for given + This function returns a dictionary containing the selected allocation for given department and term. If you want the pending allocation, pass in False for isFinal. ''' academicYearCode = int(str(termCode)[:4] + "00") - allocationObject = Allocation.select().where( + allocationDict = Allocation.select().where( Allocation.termCode.in_([termCode,academicYearCode]), Allocation.department == dept, Allocation.isFinal == isFinal).dicts().get() - return allocationObject + return allocationDict def getTotalAllocations(termCode: int, dept: int): @@ -105,4 +105,10 @@ def getContractedAllocations(termCode: int, dept: int): usedPositions["used_primaries"] = sum(list(usedPositions.values())[:4]) usedPositions["used_secondaries"] = sum(list(usedPositions.values())[4:6]) usedPositions["used_total"] = sum(list(usedPositions.values())[:6]) - return usedPositions \ No newline at end of file + return usedPositions + +def allocationExists(termCode: int, dept: int, isFinal: bool): + """ + Checks if there is an allocation that matches certain criteria. + """ + return bool(Allocation.get_or_none(Allocation.termCode == termCode, Allocation.department == dept, Allocation.isFinal == isFinal)) \ No newline at end of file diff --git a/app/logic/allocationRequest.py b/app/logic/allocationRequest.py new file mode 100644 index 000000000..141d26986 --- /dev/null +++ b/app/logic/allocationRequest.py @@ -0,0 +1,37 @@ +from flask import request, g +from app.models.allocation import Allocation +from app.logic.allocationManager import * +from app.logic.academicYearManager import getCurrentAndNextAY + + +def getOrUpdateRequestedAllocation(): + """ + Gets or updates the requested allocation (used for the Allocation Request page specificially). + """ + currentAY, nextAY = getCurrentAndNextAY() + + requester = request.form.get("submitter", type=int, default=None) # the requesting department + + # the list of the fields updated after submitting the allocation request + updatedFields = { + "termCode": nextAY, + "department": requester, + "isFinal": False, + "justification": request.form.get("justification", default=""), + "primary_10": request.form.get("primary_10", type=int, default=None), + "primary_12": request.form.get("primary_12", type=int, default=None), + "primary_15": request.form.get("primary_15", type=int, default=None), + "primary_20": request.form.get("primary_20", type=int, default=None), + "secondary_5": request.form.get("secondary_5", type=int, default=None), + "secondary_10": request.form.get("secondary_10", type=int, default=None), + "breakHours": request.form.get("breakHours", type=int, default=None) + } + + # saving the newly approved allocation + requestedAlloc, wasCreated = Allocation.get_or_create(termCode=nextAY, department=requester, isFinal=False, defaults={**updatedFields}) + + if not wasCreated: # if the allocation has already existed (it is being resubmitted/updated) + for key, value in updatedFields.items(): + setattr(requestedAlloc, key, value) # updating all the fields based on updatedFields values + + requestedAlloc.save() \ No newline at end of file diff --git a/app/logic/manageDepartments.py b/app/logic/manageDepartments.py new file mode 100644 index 000000000..9f823c784 --- /dev/null +++ b/app/logic/manageDepartments.py @@ -0,0 +1,110 @@ +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 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 \ No newline at end of file diff --git a/app/models/allocation.py b/app/models/allocation.py index eb83877a0..79d6426bf 100644 --- a/app/models/allocation.py +++ b/app/models/allocation.py @@ -1,6 +1,6 @@ from app.models import * from app.models.department import Department -from app.models.supervisor import Supervisor +from app.models.user import User from app.models.term import Term class Allocation(baseModel): @@ -8,8 +8,8 @@ class Allocation(baseModel): department = ForeignKeyField(Department) isFinal = BooleanField(default=False) approvedOn = DateField(null=True) - approvedBy = ForeignKeyField(Supervisor, null=True) - justification = TextField() + approvedBy = ForeignKeyField(User, null=True) + justification = TextField(default="", null=False) primary_10 = IntegerField() primary_12 = IntegerField() primary_15 = IntegerField() diff --git a/app/static/css/allocationRequest.css b/app/static/css/allocationRequest.css new file mode 100644 index 000000000..b476b79e6 --- /dev/null +++ b/app/static/css/allocationRequest.css @@ -0,0 +1,73 @@ +@media(min-width:970px) and (max-width:1340px) { + .container { + width: 80%; + } +} + +@media(min-width:1340px) and (max-width:1800px) { + .container { + width: 55%; + } +} + +@media(min-width:1800px) { + .container { + width: 40%; + } +} + +#allocationRequestSubtitle{ + margin-bottom: 30px; +} + +.separationLine { + border: 0; + border-top: 1px solid black; +} + +#breakHours { + margin-top: 0px; + margin-bottom: 30px; +} + +.allocationRequestSection{ + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.positionNumericSpinner { + width: 45px; +} + +.breakHoursNumericSpinner { + width: 65px; +} + +#requestedPositions { + margin-bottom: -10px; +} + +#allocationJustification { + margin-top: 30px; +} + +#justificationTextField { + resize: none; + width: 100%; + margin-bottom: 15px; +} + +#allocationRequestNote { + text-align: center; + margin: 0 auto; + max-width:70%; + margin-bottom: 15px; + color: grey; +} + +.cancel-or-submit { + display: flex; + flex-direction: row; + justify-content: space-between; +} \ 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..c7e5dffe4 --- /dev/null +++ b/app/static/css/allocationReview.css @@ -0,0 +1,72 @@ +@media(min-width:970px) and (max-width:1340px) { + .container { + width: 80%; + } +} + +@media(min-width:1340px) and (max-width:1800px) { + .container { + width: 55%; + } +} + +@media(min-width:1800px) { + .container { + width: 40%; + } +} + +#allocationReviewSubtitle{ + margin-bottom: 30px; +} + +.separationLine { + border: 0; + border-top: 1px solid black; +} + +#breakHours { + margin-top: 0px; + margin-bottom: 30px; +} + +.allocationReviewSection{ + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.positionNumericSpinner { + width: 45px; +} + +.breakHoursNumericSpinner { + width: 65px; +} + +#requestedPositions { + margin-bottom: -10px; +} + +.currentAndAllocated { + transition-duration: 150ms; +} + +.currentAndAllocated:hover { + color: grey; +} + +#allocationReviewNote { + text-align: center; + margin: 0 auto; + max-width:70%; + margin-top: 15px; + margin-bottom: 15px; + color: grey; +} + +.cancel-or-approve { + display: flex; + flex-direction: row; + justify-content: space-between; +} \ 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/allocationRequest.js b/app/static/js/allocationRequest.js new file mode 100644 index 000000000..81102573a --- /dev/null +++ b/app/static/js/allocationRequest.js @@ -0,0 +1,11 @@ +$(document).ready( function(){ + // not allowing users to type anything in a numeric spinner + $("input[type='number'].breakHoursNumericSpinner").keypress(function (evt) { + if (!/[0-9]/.test(evt.key)) { + evt.preventDefault(); + } + }); + $("input[type='number'].positionNumericSpinner").keypress(function (evt) { + evt.preventDefault(); + }); +}); \ 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..5c6c8428a --- /dev/null +++ b/app/static/js/allocationReview.js @@ -0,0 +1,14 @@ +$(document).ready( function(){ + + $('[data-toggle="popover"]').popover(); + + // not allowing users to type anything in a numeric spinner + $("input[type='number'].breakHoursNumericSpinner").keypress(function (evt) { + if (!/[0-9]/.test(evt.key)) { + evt.preventDefault(); + } + }); + $("input[type='number'].positionNumericSpinner").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..732273a86 --- /dev/null +++ b/app/templates/admin/allocationReview.html @@ -0,0 +1,114 @@ +{% 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]}} + +

+ +

+ {{requestedAlloc.justification}} +

+ +
+ +

+ + + (requested: {{requestedAlloc.breakHours}}; current: {{currentAlloc.breakHours or 0}}) +

+ +

+ +

+ +
+
+

+ + Primary + +

+ +

+ 10 hours:  + +  (requested: {{requestedAlloc.primary_10}}; current: {{currentAlloc.primary_10 or 0}}) +

+ +

+ 12 hours:  + +  (requested: {{requestedAlloc.primary_12}}; current: {{currentAlloc.primary_12 or 0}}) +

+ +

+ 15 hours:  + +  (requested: {{requestedAlloc.primary_15}}; current: {{currentAlloc.primary_15 or 0}}) +

+ +

+ 20 hours:  + +  (requested: {{requestedAlloc.primary_20}}; current: {{currentAlloc.primary_20 or 0}}) +

+
+
+

+ + Secondary + +

+ +

+ 5 hours:    + +  (requested: {{requestedAlloc.secondary_5}}; current: {{currentAlloc.secondary_5 or 0}}) +

+ +

+ 10 hours:  + +  (requested: {{requestedAlloc.secondary_10}}; current: {{currentAlloc.secondary_10 or 0}}) +

+
+
+
+ + Once this request is approved, the {{department.DEPT_NAME}} department can no longer submit any new allocation requests for {{nextAY.termName.split(" ")[1]}}. + +
+ +
+ + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/manageDepartments.html b/app/templates/admin/manageDepartments.html index ddb1b0431..88fcf297e 100755 --- a/app/templates/admin/manageDepartments.html +++ b/app/templates/admin/manageDepartments.html @@ -1,136 +1,245 @@ -{% 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 -
-
-

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. +

+ +
-
- -
-
-
- - -
-
- - - - - - - - - {% for department in activeDepartments %} - - - - - {% endfor %} - -
DepartmentStatus
{{department.DEPT_NAME}}({{department.ORG}}, {{department.ACCOUNT}}) - -
-
+
-
- - - - - - - - {% for department in inactiveDepartments %} - - - - {% endfor %} - -
Department
{{department.DEPT_NAME}}({{department.ORG}}, {{department.ACCOUNT}})
-
-
-
+
+
+
+ {% include "snips/uploadAllocations.html" %} + +
+ +
+ {% include "snips/annualAllocationReview.html" %} + + {% include "snips/annualPositionReview.html" %} + +
+ +
+ +
+
+
+
+ + +
+ +
+ + + + + + + + + + + + + {% for department in activeDepartments %} + + + + + + + + + + + + + {% endfor %} + +
DepartmentStatusCurrent Allocations
({{ academicYear }})
Requested Allocations
({{ nextAY.termName }})
Actions
+ {{department.DEPT_NAME}}
({{department.ORG}}, + {{department.ACCOUNT}}) +
+ + + + Primary: + {{department.lsfCountPrimaries}} of {{department.totalPrimaries}} + +
+ + Secondary: + {{department.lsfCountSecondaries}} of {{department.totalSecondaries}} + +
+ + Break: {{ breakHoursByDepartment.get(department.departmentID, 0) }} of {{ + department.allocation.breakHours }} hours + + +
+ + Primary: + {{department.lsfCountPrimaries}} of {{department.totalPrimaries}} + +
+ + Secondary: + {{department.lsfCountSecondaries}} of {{department.totalSecondaries}} + +
+ + Break: {{ breakHoursByDepartment.get(department.departmentID, 0) }} of {{ department.allocation.breakHours }} hours + + +
+ + +
+
+ +
+ + + + + + + + {% 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/main/allocationRequest.html b/app/templates/main/allocationRequest.html new file mode 100644 index 000000000..681bd3593 --- /dev/null +++ b/app/templates/main/allocationRequest.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} {% block styles %} {{super()}} + + +{% endblock %} {% block scripts %} {{super()}} + + +{% endblock %} {% block app_content %} + +

+ + Allocation Request + +

+ +

+ + Submit an allocation request to Berea's Work Study Program Department + +

+ +
+

+ {{department.DEPT_NAME}} Department + +

+ +

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

+ +
+ +

+ + + (currently allocated: {{currentAlloc.breakHours or 0}}) +

+ +

+ +

+ +
+
+

+ + Primary + +

+ +

+ 10 hours:  + +  (currently allocated: {{currentAlloc.primary_10 or 0}}) +

+ +

+ 12 hours:  + +  (currently allocated: {{currentAlloc.primary_12 or 0}}) +

+ +

+ 15 hours:  + +  (currently allocated: {{currentAlloc.primary_15 or 0}}) +

+ +

+ 20 hours:  + +  (currently allocated: {{currentAlloc.primary_20 or 0}}) +

+
+
+

+ + Secondary + +

+ +

+ 5 hours:    + +  (currently allocated: {{currentAlloc.secondary_5 or 0}}) +

+ +

+ 10 hours:  + +  (currently allocated: {{currentAlloc.secondary_10 or 0}}) +

+
+
+ +

+ +

+ + + +
+ + This allocation request for {{nextAY.termName.split(" ")[1]}} can be updated by resubmission. However, once the Path to Purpose Office approves it, you can no longer change it. + +
+ +
+ + +
+ +
+ +{% 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..979f49ce8 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", @@ -585,6 +637,26 @@ "adjustmentCutOff": f"2025-09-01", "isBreak": 1, }, + { + "termCode": f"202600", + "termName": f"AY 2026-2027", + "termStart": f"2026-08-01", + "termEnd": f"2027-05-01", + "termState": 0, + "primaryCutOff": f"2026-09-01", + "adjustmentCutOff": f"2026-09-01", + "isBreak": 1, + }, + { + "termCode": f"202700", + "termName": f"AY 2027-2028", + "termStart": f"2027-08-01", + "termEnd": f"2028-05-01", + "termState": 0, + "primaryCutOff": f"2027-09-01", + "adjustmentCutOff": f"2027-09-01", + "isBreak": 1, + }, ] Term.insert_many(terms).on_conflict_replace().execute() @@ -644,6 +716,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 @@ -740,11 +1127,71 @@ ########################### allocations = [ { - "termCode": 202500, - "department": 3, + "termCode": 202600, + "department": 1, + "isFinal": True, + "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": 202700, + "department": 1, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "We need even more students to help with the increased workload in the department", + "primary_10": 8, + "primary_12": 12, + "primary_15": 5, + "primary_20": 2, + "secondary_5": 8, + "secondary_10": 1, + "breakHours": 560, + }, + { + "termCode": 202600, + "department": 2, + "isFinal": True, + "approvedOn": None, + "approvedBy": None, + "justification": "Increase in student enrollment due to an exodus from the CS department", + "primary_10": 4, + "primary_12": 2, + "primary_15": 7, + "primary_20": 4, + "secondary_5": 2, + "secondary_10": 0, + "breakHours": 750, + }, + { + "termCode": 202700, + "department": 2, "isFinal": False, "approvedOn": None, "approvedBy": None, + "justification": "We need more students than last year", + "primary_10": 5, + "primary_12": 3, + "primary_15": 8, + "primary_20": 5, + "secondary_5": 3, + "secondary_10": 0, + "breakHours": 900, + }, + { + "termCode": 202600, + "department": 3, + "isFinal": True, + "approvedOn": None, + "approvedBy": None, "justification": "Downscaling due to decrease in student enrollment caused by current economic conditions", "primary_10": 2, "primary_12": 2, @@ -755,39 +1202,38 @@ "breakHours": 260, }, { - "termCode": 202500, - "department": 2, + "termCode": 202700, + "department": 3, "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, + "justification": "Having more students, as economic conditions seem to improve", + "primary_10": 5, + "primary_12": 3, + "primary_15": 3, + "primary_20": 0, "secondary_5": 2, "secondary_10": 0, - "breakHours": 750, + "breakHours": 360, }, { - "termCode": 202500, - "department": 1, - "isFinal": False, + "termCode": 202700, + "department": 3, + "isFinal": True, "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, + "primary_10": 2, + "primary_12": 2, + "primary_15": 1, + "primary_20": 0, + "secondary_5": 1, "secondary_10": 0, - "breakHours": 550, + "breakHours": 260, }, { - "termCode": 202500, + "termCode": 202600, "department": 4, - "isFinal": False, + "isFinal": True, "approvedOn": None, "approvedBy": None, "justification": "Downscaling the number of students in the department due to budget cuts", @@ -800,9 +1246,9 @@ "breakHours": 300, }, { - "termCode": 202500, + "termCode": 202600, "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", @@ -814,7 +1260,21 @@ "secondary_10": 1, "breakHours": 900, }, - + { + "termCode": 202700, + "department": 5, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "Due to rapid department growth, we need to hire even more students to help with the increased workload", + "primary_10": 9, + "primary_12": 11, + "primary_15": 9, + "primary_20": 12, + "secondary_5": 3, + "secondary_10": 9, + "breakHours": 1200, + } ] Allocation.insert_many(allocations).on_conflict_replace().execute() diff --git a/tests/code/test_academicYearManager.py b/tests/code/test_academicYearManager.py new file mode 100644 index 000000000..d180bf42d --- /dev/null +++ b/tests/code/test_academicYearManager.py @@ -0,0 +1,42 @@ +import pytest + +from flask import g +from app.models.term import * + +from app.logic.academicYearManager import * + +@pytest.mark.integration +def test_getCurrentAndNextAY(): + with app.app_context(): + g.currentAY = (1967, 1968) + currentYear, nextYear = getCurrentAndNextAY() + + assert currentYear.termCode == 196700 + assert currentYear.termName == "AY 1967-1968" + + assert nextYear.termCode == 196800 + assert nextYear.termName == "AY 1968-1969" + + + g.currentAY = (2102, 2103) + currentYear, nextYear = getCurrentAndNextAY() + + assert currentYear.termCode == 210200 + assert currentYear.termName == "AY 2102-2103" + + assert nextYear.termCode == 210300 + assert nextYear.termName == "AY 2103-2104" + + # Testing data types + assert isinstance(currentYear.termCode, int) + assert isinstance(nextYear.termCode, int) + + assert isinstance(currentYear.termName, str) + assert isinstance(nextYear.termName, str) + + # Testing whether termName is formatted correctly + assert currentYear.termName.split(" ")[0] == "AY" + assert nextYear.termName.split(" ")[0] == "AY" + + assert currentYear.termName.split(" ")[1] == "2102-2103" + assert nextYear.termName.split(" ")[1] == "2103-2104" \ No newline at end of file diff --git a/tests/code/test_allocationManger.py b/tests/code/test_allocationManger.py index ea7da6782..6d15b58ec 100644 --- a/tests/code/test_allocationManger.py +++ b/tests/code/test_allocationManger.py @@ -204,4 +204,47 @@ def test_getContractedAllocations(testLaborStatusForm, testTerm, testDepartment, assert contractedAllocation['used_secondaries'] == 0 assert contractedAllocation['used_total'] == 1 - assert contractedAllocation['break_hours'] == 500 \ No newline at end of file + assert contractedAllocation['break_hours'] == 500 + +@pytest.mark.integration +def test_allocationExists(testTerm, testDepartment, testAllocation, testPendingAllocation): + + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=False) == True + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=True) == True + + assert allocationExists(testTerm.termCode + 100, testDepartment.departmentID, isFinal=False) == False + assert allocationExists(testTerm.termCode + 100, testDepartment.departmentID, isFinal=True) == False + + assert allocationExists(testTerm.termCode, 456, isFinal=False) == False + assert allocationExists(testTerm.termCode, 456, isFinal=True) == False + + testAllocation.delete_instance() + + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=False) == True + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=True) == False + + testPendingAllocation.delete_instance() + + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=False) == False + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=True) == False + + with mainDB.atomic() as transaction: + allocation = Allocation.create( + termCode = testTerm.termCode, + department = testDepartment.departmentID, + isFinal = True, + approvedOn = None, + approvedBy = None, + justification = "brovich", + primary_10 = 22, + primary_12 = 6, + primary_15 = 7, + primary_20 = 12, + secondary_5 = 45, + secondary_10 = 22, + breakHours = 894) + + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=False) == False + assert allocationExists(testTerm.termCode, testDepartment.departmentID, isFinal=True) == True + + transaction.rollback() diff --git a/tests/code/test_allocationRequest.py b/tests/code/test_allocationRequest.py new file mode 100644 index 000000000..f06c4557d --- /dev/null +++ b/tests/code/test_allocationRequest.py @@ -0,0 +1,82 @@ +import pytest + +from flask import request, g +from werkzeug.datastructures import ImmutableMultiDict + +from app import app +from app.models.allocation import Allocation +from app.models import mainDB +from app.models.term import Term + +from app.logic.allocationRequest import * + + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as client: + yield client + + +@pytest.mark.integration +def test_getOrUpdateRequestedAllocation(client): + with app.test_request_context('/allocationRequest/submit', method='POST', data={ + 'submitter': "2", + 'breakHours': "750", + 'primary_10': "4", + 'primary_12': "13", + 'primary_15': "7", + 'primary_20': "5", + 'secondary_5': "2", + 'secondary_10': "0", + 'breakHours': "100", + 'justification': "" + }): + with mainDB.atomic() as transaction: + g.openTerm, _ = Term.get_or_create( + termCode=200200, + defaults={"termName": "AY 2002-2003", "isAcademicYear": True} + ) + + nextYear = Term.create(termCode=200300) + + getOrUpdateRequestedAllocation() + + allocation = Allocation.get(Allocation.termCode == g.openTerm.termCode + 100, Allocation.department == request.form.get("submitter", type=int, default=None)) + + assert isinstance(allocation.termCode, Term) + assert isinstance(allocation.termCode.termCode, int) + assert allocation.termCode.termCode == 200300 + + assert isinstance(allocation.department, Department) + assert isinstance(allocation.department.departmentID, int) + assert allocation.department.departmentID == 2 + + assert isinstance(allocation.isFinal, bool) + assert allocation.isFinal == False + + assert isinstance(allocation.justification, str) + assert allocation.justification == "" + + assert isinstance(allocation.primary_10, int) + assert allocation.primary_10 == 4 + + assert isinstance(allocation.primary_12, int) + assert allocation.primary_12 == 13 + + assert isinstance(allocation.primary_15, int) + assert allocation.primary_15 == 7 + + assert isinstance(allocation.primary_20, int) + assert allocation.primary_20 == 5 + + assert isinstance(allocation.secondary_5, int) + assert allocation.secondary_5 == 2 + + assert isinstance(allocation.secondary_10, int) + assert allocation.secondary_10 == 0 + + assert isinstance(allocation.breakHours, int) + assert allocation.breakHours == 100 + + transaction.rollback() \ No newline at end of file diff --git a/tests/code/test_manageDepartments.py b/tests/code/test_manageDepartments.py new file mode 100644 index 000000000..fc165a826 --- /dev/null +++ b/tests/code/test_manageDepartments.py @@ -0,0 +1,17 @@ +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. \ No newline at end of file