diff --git a/app/__init__.py b/app/__init__.py index 11c90a0a9..a131ae4fb 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,9 +1,9 @@ import os - -from flask import Flask -from flask_restful import Api +from datetime import date +from flask import Flask, g, request, session from flask_bootstrap import Bootstrap -from playhouse.shortcuts import model_to_dict, dict_to_model +from flask_restful import Api +from playhouse.shortcuts import dict_to_model, model_to_dict app = Flask(__name__) @@ -55,17 +55,27 @@ def new_execute(*args, **kwargs): from app.controllers.api_routes.routes import initializeApiRoutes initializeApiRoutes(api) -from flask import g from app.models.user import User -from app.login_manager import require_login +from app.login_manager import getUsernameFromEnv, require_login @app.before_request def load_user(): - try: - g.currentUser = dict_to_model(User, session['currentUser']) + requestUsername = getUsernameFromEnv(request.environ) + try: + cachedUser = session['currentUser'] + + if cachedUser.get('username') == requestUsername: + g.currentUser = dict_to_model(User, cachedUser) + return + + session.pop('currentUser', None) + session.pop('username', None) + except Exception as e: - user = require_login() - session['currentUser'] = model_to_dict(user) - g.currentUser = user + pass + + user = require_login() + session['currentUser'] = model_to_dict(user) + g.currentUser = user from app.models.term import Term from app.login_manager import getOpenTerm @@ -79,6 +89,20 @@ def load_openTerm(): session['openTerm'] = model_to_dict(term) g.openTerm = term +def getCurrentYear(): + today = date.today() + year = today.year + + if today.month < 7: + return year - 1, year + + return year, year + 1 + +@app.before_request +def load_currentYear(): + g.currentYear = getCurrentYear() + + @app.context_processor def inject_environment(): return dict(env=app.config['ENV']) @@ -87,4 +111,3 @@ def inject_environment(): def queryCount(): if session: session['querycount'] = 0 - diff --git a/app/controllers/admin_routes/manageDepartments.py b/app/controllers/admin_routes/manageDepartments.py index c202513a9..ec1aef0a4 100644 --- a/app/controllers/admin_routes/manageDepartments.py +++ b/app/controllers/admin_routes/manageDepartments.py @@ -1,6 +1,6 @@ from datetime import date -from flask import g, request, redirect, jsonify, abort +from flask import g, request, redirect, jsonify, abort, flash from app.controllers.admin_routes import * from app.login_manager import require_login @@ -16,11 +16,13 @@ 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(academicYear = None): +def manageDepartments(): """ Returns the Manage Departments page, which allows the admin to view all the departments and their allocations. @@ -36,16 +38,8 @@ def manageDepartments(academicYear = None): 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, nextAY = generateAdjacentYears(academicYear) - chosenAY = Term.get(Term.termCode == academicYear) + currentAY, nextAY = getCurrentAndNextAY() + chosenAY = Term.get(Term.termCode == currentAY.termCode) breakHoursByDepartment = {row["department"]: str(row["totalHours"] or 0) for row in getUsedBreakHours(chosenAY)} @@ -98,12 +92,14 @@ def allocationReview(org=None, account=None): the Manage Departments page. """ - # Retrieving the departments based on the org and account numbers + + # 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 @@ -114,9 +110,63 @@ def allocationReview(org=None, account=None): 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 + # 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 = require_login().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/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py index b0c33f5e7..2673a874b 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 + + + # cheching 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('/admin/manageDepartments/') + + + # 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..f66efd15a --- /dev/null +++ b/app/logic/academicYearManager.py @@ -0,0 +1,27 @@ +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 = g.currentYear[0] + nextYear = currentYear + 1 + + 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 index 4f3fbb9d8..9f823c784 100644 --- a/app/logic/manageDepartments.py +++ b/app/logic/manageDepartments.py @@ -14,45 +14,6 @@ -def generateAdjacentYears(academicYearTermCode=None): - """ - Generates the current, the previous, and the following academic years. - """ - - currentYear = g.openTerm.termCode // 100 - nextYear = currentYear + 1 - - - currentAYCode = currentYear * 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, nextAYCode): - abort(400) - - - 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) - - - - -#################################################################################################################################### -# 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. @@ -146,43 +107,4 @@ def getAllocationStatus(term, department): (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 + 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 index 2d72c7067..c7e5dffe4 100644 --- a/app/static/css/allocationReview.css +++ b/app/static/css/allocationReview.css @@ -1,18 +1,18 @@ -@media(min-width:970px) and (max-width:1240px) { +@media(min-width:970px) and (max-width:1340px) { .container { - width: 80%; + width: 80%; } } -@media(min-width:1240px) and (max-width:1800px) { +@media(min-width:1340px) and (max-width:1800px) { .container { - width: 55%; + width: 55%; } } @media(min-width:1800px) { .container { - width: 40%; + width: 40%; } } @@ -20,33 +20,53 @@ margin-bottom: 30px; } +.separationLine { + border: 0; + border-top: 1px solid black; +} + #breakHours { margin-top: 0px; + margin-bottom: 30px; } -.numericSpinner { - width: 55px; +.allocationReviewSection{ + display: flex; + flex-direction: row; + justify-content: space-between; } -#requestedPositions { - margin-top: 30px; - margin-bottom: -10px; +.positionNumericSpinner { + width: 45px; } -.noBorders { - border: none !important; +.breakHoursNumericSpinner { + width: 65px; } +#requestedPositions { + margin-bottom: -10px; +} -#allocationJustification { - margin-top: 30px; +.currentAndAllocated { + transition-duration: 150ms; } -.unresizeable { - resize: none; +.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/js/allocationRequest.js b/app/static/js/allocationRequest.js new file mode 100644 index 000000000..be2a39f29 --- /dev/null +++ b/app/static/js/allocationRequest.js @@ -0,0 +1,9 @@ +$(document).ready( function(){ + // not allowing users to type anything in a numeric spinner + $("input[type='number'].breakHoursNumericSpinner").keypress(function (evt) { + 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 index 49c14f105..d661f5a39 100644 --- a/app/static/js/allocationReview.js +++ b/app/static/js/allocationReview.js @@ -1,6 +1,12 @@ $(document).ready( function(){ + + $('[data-toggle="popover"]').popover(); + // not allowing users to type anything in a numeric spinner - $("input[type='number'].numericSpinner").keypress(function (evt) { + $("input[type='number'].breakHoursNumericSpinner").keypress(function (evt) { + evt.preventDefault(); + }); + $("input[type='number'].positionNumericSpinner").keypress(function (evt) { evt.preventDefault(); }); }); \ No newline at end of file diff --git a/app/templates/admin/allocationReview.html b/app/templates/admin/allocationReview.html index 73b929e3a..732273a86 100644 --- a/app/templates/admin/allocationReview.html +++ b/app/templates/admin/allocationReview.html @@ -19,29 +19,35 @@

-
+

{{department.DEPT_NAME}} Department +

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

+ +

+ {{requestedAlloc.justification}}

-
+

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

- +

-
+

@@ -51,20 +57,26 @@

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

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

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

+ +

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

@@ -76,37 +88,27 @@

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

10 hours:  - -  (requested: 120) + +  (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]}}. + +
-

- -

- - -
- -
- - *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/main/allocationRequest.html b/app/templates/main/allocationRequest.html new file mode 100644 index 000000000..48f63ae61 --- /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 the Labor 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 Labor Office approves it, you can no longer change it. + +
+ +
+ + +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/database/demo_data.py b/database/demo_data.py index b7a59297e..979f49ce8 100644 --- a/database/demo_data.py +++ b/database/demo_data.py @@ -637,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() @@ -1107,27 +1127,42 @@ ########################### allocations = [ { - "termCode": 202500, - "department": 3, - "isFinal": False, + "termCode": 202600, + "department": 1, + "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, - "primary_15": 1, - "primary_20": 0, - "secondary_5": 1, + "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": 260, + "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": 202500, + "termCode": 202600, "department": 2, "isFinal": True, "approvedOn": None, "approvedBy": None, - "justification": "Increase in student enrollment due to exodous from CS department", + "justification": "Increase in student enrollment due to an exodus from the CS department", "primary_10": 4, "primary_12": 2, "primary_15": 7, @@ -1137,24 +1172,68 @@ "breakHours": 750, }, { - "termCode": 202500, - "department": 1, + "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": "We are hiring more students to help with the increased workload in the department", + "justification": "Downscaling due to decrease in student enrollment caused by current economic conditions", + "primary_10": 2, + "primary_12": 2, + "primary_15": 1, + "primary_20": 0, + "secondary_5": 1, + "secondary_10": 0, + "breakHours": 260, + }, + { + "termCode": 202700, + "department": 3, + "isFinal": False, + "approvedOn": None, + "approvedBy": None, + "justification": "Having more students, as economic conditions seem to improve", "primary_10": 5, - "primary_12": 6, - "primary_15": 4, - "primary_20": 1, - "secondary_5": 7, + "primary_12": 3, + "primary_15": 3, + "primary_20": 0, + "secondary_5": 2, "secondary_10": 0, - "breakHours": 550, + "breakHours": 360, + }, + { + "termCode": 202700, + "department": 3, + "isFinal": True, + "approvedOn": None, + "approvedBy": None, + "primary_10": 2, + "primary_12": 2, + "primary_15": 1, + "primary_20": 0, + "secondary_5": 1, + "secondary_10": 0, + "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", @@ -1167,7 +1246,7 @@ "breakHours": 300, }, { - "termCode": 202500, + "termCode": 202600, "department": 5, "isFinal": True, "approvedOn": None, @@ -1181,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..830c4a4e9 --- /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.currentYear = (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.currentYear = (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 index 77cb2ee38..fc165a826 100644 --- a/tests/code/test_manageDepartments.py +++ b/tests/code/test_manageDepartments.py @@ -14,100 +14,4 @@ # 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 +# It is designed to ensure that the manageDepartments functionality works as expected and returns the correct data. \ No newline at end of file