Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
643d58e
created a branch off of dep_portal_ad_ManageDepartments and then add…
johnlolonga19 Aug 3, 2026
8bbf144
added Annual positionn review email template
johnlolonga19 Aug 3, 2026
5b76836
Add sendAnnualPositionReviewRequests logic for Annual Position Review
johnlolonga19 Aug 3, 2026
3361ecb
Add POST route to trigger Annual Position Review requests
johnlolonga19 Aug 3, 2026
f9091b0
add js to anchor the request button and emailTemplates.js link to the…
johnlolonga19 Aug 3, 2026
338844b
manageDepartments.html: handle functionality of the Annual Position r…
johnlolonga19 Aug 3, 2026
9a31d84
added the test suite for the logic
johnlolonga19 Aug 4, 2026
6fb8246
fixed some requested changes
johnlolonga19 Aug 4, 2026
667112d
Merge branch 'dep_portal_ad_ManageDepartments' into annual-position-r…
johnlolonga19 Aug 4, 2026
73eaeb0
fixed requested change to validate rsp and academicYear
johnlolonga19 Aug 4, 2026
60a30f8
removed the duplicate fuction
johnlolonga19 Aug 4, 2026
d088351
ressolve changes
johnlolonga19 Aug 5, 2026
fd6e4c5
changes to route
johnlolonga19 Aug 5, 2026
48d3a54
ressolve changes
johnlolonga19 Aug 5, 2026
f6366b3
Merge branch 'dep_portal_ad_ManageDepartments' of https://github.com/…
johnlolonga19 Aug 7, 2026
80aa951
fix damages caused by merge conflicts
johnlolonga19 Aug 7, 2026
b92ba12
consolidated the try and accept
johnlolonga19 Aug 7, 2026
f0f122e
Fold Annual Position Review into emailHandler instead of a separate m…
johnlolonga19 Aug 7, 2026
829421e
remove dropdown for MD page
johnlolonga19 Aug 7, 2026
9279efc
used g.currentyear and added demo data
johnlolonga19 Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from datetime import date

from flask import Flask
from flask_restful import Api
Expand Down Expand Up @@ -78,7 +79,21 @@ def load_openTerm():
if term:
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'])
Expand Down
40 changes: 35 additions & 5 deletions app/controllers/admin_routes/manageDepartments.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from app.models.allocation import *
from app.models.laborStatusForm import *

from app.logic.manageDepartments import *
from app.logic.manageDepartments import *
from app.logic.emailHandler import emailHandler



@admin.route('/admin/manageDepartments/', methods=['GET'])
@admin.route('/admin/manageDepartments/<academicYear>', methods=['GET'])
def manageDepartments(academicYear = None):
"""
Returns the Manage Departments page, which allows the admin to view all the departments
Expand All @@ -37,12 +39,14 @@ def manageDepartments(academicYear = None):
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:
# The condition below may be deleted if the routing to the Manage Departments page is changed.
if academicYear == None:
academicYear = g.currentYear[0] * 100
else:
academicYear = int(academicYear)

print("Academic Year Term Code asasasas:", academicYear)


currentAY, nextAY = generateAdjacentYears(academicYear)
chosenAY = Term.get(Term.termCode == academicYear)
Expand All @@ -65,6 +69,7 @@ def manageDepartments(academicYear = None):
allSupervisors = allSupervisors,
currentAY = currentAY,
nextAY = nextAY,
chosenAY = chosenAY,
academicYear = chosenAY.termName,
breakHoursByDepartment = breakHoursByDepartment,
allocationStatus = allocationStatus
Expand All @@ -91,6 +96,31 @@ def complianceStatusCheck():



@admin.route('/admin/manageDepartments/annualPositionReview', methods=['POST'])
def annualPositionReviewRequest():
"""
Sends an Annual Position Review request email to every active department's

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using g.openterm will create a lot of bugs in case user have two open term, if the position review is for next ay while the openterm is for this ay

Labor Coordinators and supervisors for the selected academic year, and
records the request. Triggered from the Manage Departments page.
"""
currentUser = require_login()
if not currentUser or not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent):
return jsonify({"Success": False}), 403

rsp = request.get_json(silent=True)

try:
academicYear = int(rsp["academicYear"])
except (TypeError, ValueError, KeyError):
return jsonify({"Success": False, "message": "Request must include a valid academicYear."}), 400

try:
handler = emailHandler(academicYearTermCode=academicYear)
result = handler.sendAnnualPositionReviewRequests(currentUser)
return jsonify({"Success": True, **result})
except Exception:
return jsonify({"Success": False})

@admin.route('/admin/manageDepartments/<org>/<account>/allocationReview', methods=['GET'])
def allocationReview(org=None, account=None):
"""
Expand Down
202 changes: 131 additions & 71 deletions app/logic/emailHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,82 +16,96 @@
from app import app
import os
from datetime import datetime, date
from app.models.department import Department
from app.models.term import Term
from app.models.positionReview import PositionReview
from app.logic.getSupervisors import getSupervisors


class emailHandler():
def __init__(self, formHistoryKey):
def __init__(self, formHistoryKey=None, academicYearTermCode=None):
self.mail = Mail(app)

self.formHistory = FormHistory.get(FormHistory.formHistoryID == formHistoryKey)
self.laborStatusForm = self.formHistory.formID
self.term = self.laborStatusForm.termCode
self.student = self.laborStatusForm.studentSupervisee
self.studentEmail = self.student.STU_EMAIL
self.creatorEmail = self.formHistory.createdBy.email
self.supervisorEmail = self.laborStatusForm.supervisor.EMAIL
self.date = self.laborStatusForm.startDate.strftime("%m/%d/%Y")
self.weeklyHours = str(self.laborStatusForm.weeklyHours)
self.contractHours = str(self.laborStatusForm.contractHours)
self.adminName = ""
self.positions = LaborStatusForm.select().where(LaborStatusForm.termCode == self.term, LaborStatusForm.studentSupervisee == self.student)
self.supervisors = []
for position in self.positions:
self.supervisors.append(position.supervisor)

if not self.term.isBreak:
try:
ayTermCode = str(self.laborStatusForm.termCode.termCode)[:-2] + '00'
self.primaryEmail = None
self.primaryForm = None
self.primaryForm = FormHistory.select().join_from(FormHistory, LaborStatusForm) \
.join_from(FormHistory, HistoryType).join_from(FormHistory, Status) \
.where((FormHistory.formID.jobType == "Primary") &
(FormHistory.formID.studentSupervisee == self.laborStatusForm.studentSupervisee) &
((FormHistory.formID.termCode == self.laborStatusForm.termCode) | (FormHistory.formID.termCode == ayTermCode)) &
(FormHistory.historyType.historyTypeName == "Labor Status Form") &
~(FormHistory.status.statusName % "Denied%")).get()
self.primaryEmail = self.primaryForm.formID.supervisor.EMAIL
except DoesNotExist:
# This case happens from some of the old data
pass

self.link = ""
self.releaseReason = ""
self.releaseDate = ""
self.newAdjustmentField = ""
self.oldAdjustmentField = ""

# generating a confirmation link for student approval
self.confirmationLink = ""
if self.laborStatusForm.confirmationToken:
self.confirmationLink = f"{request.host_url}studentResponse/confirm?token={self.laborStatusForm.confirmationToken}"


if self.formHistory.adjustedForm:
if self.formHistory.adjustedForm.fieldAdjusted == "supervisor":
from app.logic.userInsertFunctions import createSupervisorFromTracy
newSupervisor = createSupervisorFromTracy(bnumber=self.formHistory.adjustedForm.newValue)
self.newAdjustmentField = "Pending new Supervisor: {0} {1}".format(newSupervisor.FIRST_NAME, newSupervisor.LAST_NAME)
self.oldAdjustmentField = "Current Supervisor: {0} {1}".format(self.formHistory.formID.supervisor.FIRST_NAME, self.formHistory.formID.supervisor.LAST_NAME)
elif self.formHistory.adjustedForm.fieldAdjusted == "position":
currentPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.oldValue)
newPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.newValue)
self.oldAdjustmentField = "Current Position: {0} ({1})".format(currentPosition.POSN_TITLE, currentPosition.WLS)
self.newAdjustmentField = "Pending new Position: {0} ({1})".format(newPosition.POSN_TITLE, newPosition.WLS)
else:
self.oldAdjustmentField = "Current Hours: {0}".format(self.formHistory.adjustedForm.oldValue)
self.newAdjustmentField = "Pending new Hours: {0}".format(self.formHistory.adjustedForm.newValue)

try:
self.releaseDate = self.formHistory.releaseForm.releaseDate.strftime("%m/%d/%Y")
self.releaseReason = self.formHistory.releaseForm.reasonForRelease
# emailHandler was originally built entirely around a single
# LaborStatusForm (formHistoryKey). Annual Position Review isn't tied
# to a form at all - it's scoped to an academic year across every
# department - so construction branches on whichever was given.
if formHistoryKey is not None:
self.formHistory = FormHistory.get(FormHistory.formHistoryID == formHistoryKey)
self.laborStatusForm = self.formHistory.formID
self.term = self.laborStatusForm.termCode
self.student = self.laborStatusForm.studentSupervisee
self.studentEmail = self.student.STU_EMAIL
self.creatorEmail = self.formHistory.createdBy.email
self.supervisorEmail = self.laborStatusForm.supervisor.EMAIL
self.date = self.laborStatusForm.startDate.strftime("%m/%d/%Y")
self.weeklyHours = str(self.laborStatusForm.weeklyHours)
self.contractHours = str(self.laborStatusForm.contractHours)
self.adminName = ""
self.positions = LaborStatusForm.select().where(LaborStatusForm.termCode == self.term, LaborStatusForm.studentSupervisee == self.student)
self.supervisors = []
for position in self.positions:
self.supervisors.append(position.supervisor)

if not self.term.isBreak:
try:
ayTermCode = str(self.laborStatusForm.termCode.termCode)[:-2] + '00'
self.primaryEmail = None
self.primaryForm = None
self.primaryForm = FormHistory.select().join_from(FormHistory, LaborStatusForm) \
.join_from(FormHistory, HistoryType).join_from(FormHistory, Status) \
.where((FormHistory.formID.jobType == "Primary") &
(FormHistory.formID.studentSupervisee == self.laborStatusForm.studentSupervisee) &
((FormHistory.formID.termCode == self.laborStatusForm.termCode) | (FormHistory.formID.termCode == ayTermCode)) &
(FormHistory.historyType.historyTypeName == "Labor Status Form") &
~(FormHistory.status.statusName % "Denied%")).get()
self.primaryEmail = self.primaryForm.formID.supervisor.EMAIL
except DoesNotExist:
# This case happens from some of the old data
pass

self.link = ""
self.releaseReason = ""
self.releaseDate = ""
self.newAdjustmentField = ""
self.oldAdjustmentField = ""

# generating a confirmation link for student approval
self.confirmationLink = ""
if self.laborStatusForm.confirmationToken:
self.confirmationLink = f"{request.host_url}studentResponse/confirm?token={self.laborStatusForm.confirmationToken}"


if self.formHistory.adjustedForm:
if self.formHistory.adjustedForm.fieldAdjusted == "supervisor":
from app.logic.userInsertFunctions import createSupervisorFromTracy
newSupervisor = createSupervisorFromTracy(bnumber=self.formHistory.adjustedForm.newValue)
self.newAdjustmentField = "Pending new Supervisor: {0} {1}".format(newSupervisor.FIRST_NAME, newSupervisor.LAST_NAME)
self.oldAdjustmentField = "Current Supervisor: {0} {1}".format(self.formHistory.formID.supervisor.FIRST_NAME, self.formHistory.formID.supervisor.LAST_NAME)
elif self.formHistory.adjustedForm.fieldAdjusted == "position":
currentPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.oldValue)
newPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.newValue)
self.oldAdjustmentField = "Current Position: {0} ({1})".format(currentPosition.POSN_TITLE, currentPosition.WLS)
self.newAdjustmentField = "Pending new Position: {0} ({1})".format(newPosition.POSN_TITLE, newPosition.WLS)
else:
self.oldAdjustmentField = "Current Hours: {0}".format(self.formHistory.adjustedForm.oldValue)
self.newAdjustmentField = "Pending new Hours: {0}".format(self.formHistory.adjustedForm.newValue)

except Exception as e:
# The error you should get when the form is not a release form
# is the 'AttributeError' error. We expect to get the 'AttributeError',
# but if we get anything else then we want to print the error
if e.__class__.__name__ != "AttributeError":
print (e)
try:
self.releaseDate = self.formHistory.releaseForm.releaseDate.strftime("%m/%d/%Y")
self.releaseReason = self.formHistory.releaseForm.reasonForRelease

except Exception as e:
# The error you should get when the form is not a release form
# is the 'AttributeError' error. We expect to get the 'AttributeError',
# but if we get anything else then we want to print the error
if e.__class__.__name__ != "AttributeError":
print (e)

elif academicYearTermCode is not None:
self.term = Term.get(Term.termCode == academicYearTermCode)
else:
raise ValueError("emailHandler requires either formHistoryKey or academicYearTermCode")

def send(self, message: Message):
if app.config['ENV'] == 'production' or app.config['ALWAYS_SEND_MAIL']:
Expand All @@ -110,7 +124,53 @@ def send(self, message: Message):
else:
print("ENV: {}. Email not sent to {}, subject '{}'.".format(app.config['ENV'], message.recipients, message.subject))


def sendAnnualPositionReviewRequests(self, requestingUser):
"""
Sends an Annual Position Review request email to every active department's
Labor Coordinators and supervisors, and records that the request was made
for this handler's academic year (self.term).
"""
template = EmailTemplate.get(EmailTemplate.purpose == "Annual Position Review Request")
departments = Department.select().where(Department.isActive == True)

sentCount = 0
for department in departments:
# A review is considered "requested" for every active department as soon
# as this runs, whether or not there's currently anyone to email - a
# department with no supervisors/coordinators assigned is itself worth
# surfacing, not silently skipping.
existingReview = PositionReview.get_or_none(
PositionReview.academicYear == self.term,
PositionReview.department == department
)
if existingReview:
existingReview.requestedOn = datetime.now()
existingReview.requestedBy = requestingUser
existingReview.save()
else:
PositionReview.create(
academicYear=self.term,
department=department,
requestedOn=datetime.now(),
requestedBy=requestingUser
)

supervisors, laborCoordinators = getSupervisors(department)
recipients = {person["email"] for person in supervisors + laborCoordinators if person["email"]}
if not recipients:
continue

subject = template.subject.replace("@@AcademicYear@@", self.term.termName)
body = template.body.replace("@@Department@@", department.DEPT_NAME).replace("@@AcademicYear@@", self.term.termName)

message = Message(subject, recipients=list(recipients))
message.html = body
self.send(message)

sentCount += 1
print("Sent Annual Position Review request to {} for department {}.".format(", ".join(recipients), department.DEPT_NAME))
print("{} Annual Position Review requests sent for academic year {}.".format(sentCount, self.term.termName))
return {"sentCount": sentCount, "departmentCount": departments.count()}

# The methods of this class each handle a different email situation. Some of the methods need to handle
# "primary" and "secondary" forms differently, but a majority do not need to differentiate between the two.
Expand Down
10 changes: 5 additions & 5 deletions app/logic/manageDepartments.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@



def generateAdjacentYears(academicYearTermCode=None):
def generateAdjacentYears(academicYearTermCode=None):
"""
Generates the current, the previous, and the following academic years.
Generates the current and the following academic years.
"""

currentYear = g.openTerm.termCode // 100
nextYear = currentYear + 1
currentYear = g.currentYear[0]
nextYear = g.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
# Admins can only view the current positions and the requested positions for the incoming academic year
if academicYearTermCode not in (None, currentAYCode, nextAYCode):
abort(400)

Expand Down
14 changes: 14 additions & 0 deletions app/models/positionReview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from app.models import *

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should try to work off from positionhistory model

from app.models.department import Department
from app.models.term import Term
from app.models.user import User


class PositionReview(baseModel):
academicYear = ForeignKeyField(Term)
department = ForeignKeyField(Department)
requestedOn = DateTimeField()
requestedBy = ForeignKeyField(User)

class Meta:
indexes = ( (('academicYear', 'department'), True), )
Loading