From 643d58edbaf05df4d308ee4cc9229b2341184ee3 Mon Sep 17 00:00:00 2001 From: lolongaj Date: Mon, 3 Aug 2026 13:39:27 -0400 Subject: [PATCH 01/11] created a branch off of dep_portal_ad_ManageDepartments and then added logic file for annual position review --- app/models/positionReview.py | 14 ++++++++++++++ database/migrate_db.sh | 1 + 2 files changed, 15 insertions(+) create mode 100644 app/models/positionReview.py diff --git a/app/models/positionReview.py b/app/models/positionReview.py new file mode 100644 index 000000000..fa11d3ce4 --- /dev/null +++ b/app/models/positionReview.py @@ -0,0 +1,14 @@ +from app.models import * +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), ) diff --git a/database/migrate_db.sh b/database/migrate_db.sh index dea226d8e..79738130a 100755 --- a/database/migrate_db.sh +++ b/database/migrate_db.sh @@ -32,6 +32,7 @@ pem add app.models.studentLaborEvaluation.StudentLaborEvaluation pem add app.models.formSearchResult.FormSearchResult pem add app.models.positionHistory.PositionHistory pem add app.models.allocation.Allocation +pem add app.models.positionReview.PositionReview pem watch pem migrate From 8bbf144e01e905f85d1a21b89a68c1a0bb304b3a Mon Sep 17 00:00:00 2001 From: lolongaj Date: Mon, 3 Aug 2026 16:40:57 -0400 Subject: [PATCH 02/11] added Annual positionn review email template --- database/base_data.py | 14 ++++++++++++++ scripts/addingTemplate.py | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/database/base_data.py b/database/base_data.py index c081fb752..e72c26762 100644 --- a/database/base_data.py +++ b/database/base_data.py @@ -587,6 +587,20 @@ "subject":"Labor Overload Form Student Reason", "body":'', "audience":"Labor Office" + }, + {"purpose":"Annual Position Review Request", + "formType":"Position Review", + "action":"Annual Request", + "subject":"Annual Position Review — @@AcademicYear@@", + "body":'''

Dear @@Department@@,

+

As part of our annual position review process, please review your department's position + descriptions and submit any necessary updates for the @@AcademicYear@@ academic year.

+

 

+

Sincerely,

+

Labor Program Office

+

labor_program@berea.edu

+

859-985-3611

''', + "audience":"Department" } ] EmailTemplate.insert_many(emailtemps).on_conflict_replace().execute() diff --git a/scripts/addingTemplate.py b/scripts/addingTemplate.py index 6ea1131d1..8634e562e 100644 --- a/scripts/addingTemplate.py +++ b/scripts/addingTemplate.py @@ -48,7 +48,23 @@ '

Sincerely,
Labor Program Office
labor_program@berea.edu
859-985-3611

' ), "audience":"Student", - } + }, + "annualPositionReviewRequest": { + "purpose": "Annual Position Review Request", + "formType": "Position Review", + "action": "Annual Request", + "subject": "Annual Position Review — @@AcademicYear@@", + "body": ( + "

Dear @@Department@@,

" + "

As part of our annual position review process, please review your department's position descriptions and submit any necessary updates for the @@AcademicYear@@ academic year.

" + "

 

" + "

Sincerely,

" + "

Labor Program Office

" + "

labor_program@berea.edu

" + "

859-985-3611

" + ), + "audience": "Department", + }, } def addingTemplates(): From 5b7683631e8bdcd458402e830c3a8fa31d118798 Mon Sep 17 00:00:00 2001 From: lolongaj Date: Mon, 3 Aug 2026 16:58:39 -0400 Subject: [PATCH 03/11] Add sendAnnualPositionReviewRequests logic for Annual Position Review --- app/logic/annualPositionReview.py | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 app/logic/annualPositionReview.py diff --git a/app/logic/annualPositionReview.py b/app/logic/annualPositionReview.py new file mode 100644 index 000000000..d20f5a9d3 --- /dev/null +++ b/app/logic/annualPositionReview.py @@ -0,0 +1,73 @@ +from datetime import datetime + +from flask_mail import Mail, Message + +from app import app +from app.models.department import Department +from app.models.emailTemplate import EmailTemplate +from app.models.positionReview import PositionReview +from app.models.term import Term +from app.logic.getSupervisors import getSupervisors + + +def sendMail(mail, message: Message): + if app.config['ENV'] == 'production' or app.config['ALWAYS_SEND_MAIL']: + + # If we have set an override address + if app.config['MAIL_OVERRIDE_ALL']: + message.html = "Original message intended for {}.
".format(", ".join(message.recipients)) + message.html + message.recipients = [app.config['MAIL_OVERRIDE_ALL']] + + message.reply_to = app.config["REPLY_TO_ADDRESS"] + mail.send(message) + + elif app.config['ENV'] == 'testing': + pass + else: + print("ENV: {}. Email not sent to {}, subject '{}'.".format(app.config['ENV'], message.recipients, message.subject)) + + +def sendAnnualPositionReviewRequests(academicYearTermCode, 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 the given academic year. + """ + mail = Mail(app) + term = Term.get(Term.termCode == academicYearTermCode) + template = EmailTemplate.get(EmailTemplate.purpose == "Annual Position Review Request") + departments = Department.select().where(Department.isActive == True) + + sentCount = 0 + for department in departments: + supervisors, laborCoordinators = getSupervisors(department) + recipients = {person["email"] for person in supervisors + laborCoordinators if person["email"]} + if not recipients: + continue + + subject = template.subject.replace("@@AcademicYear@@", term.termName) + body = template.body.replace("@@Department@@", department.DEPT_NAME).replace("@@AcademicYear@@", term.termName) + + message = Message(subject, recipients=list(recipients)) + message.html = body + sendMail(mail, message) + + existingReview = PositionReview.get_or_none( + PositionReview.academicYear == term, + PositionReview.department == department + ) + if existingReview: + existingReview.requestedOn = datetime.now() + existingReview.requestedBy = requestingUser + existingReview.save() + else: + PositionReview.create( + academicYear=term, + department=department, + requestedOn=datetime.now(), + requestedBy=requestingUser + ) + + sentCount += 1 + + return {"sentCount": sentCount, "departmentCount": departments.count()} From 3361ecbdbbdccaced83a729c62df17314e18fd3c Mon Sep 17 00:00:00 2001 From: lolongaj Date: Mon, 3 Aug 2026 17:06:01 -0400 Subject: [PATCH 04/11] Add POST route to trigger Annual Position Review requests --- .../admin_routes/manageDepartments.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin_routes/manageDepartments.py b/app/controllers/admin_routes/manageDepartments.py index 308af4942..1c3aa967c 100644 --- a/app/controllers/admin_routes/manageDepartments.py +++ b/app/controllers/admin_routes/manageDepartments.py @@ -15,7 +15,8 @@ from app.models.allocation import * from app.models.laborStatusForm import * -from app.logic.manageDepartments import * +from app.logic.manageDepartments import * +from app.logic.annualPositionReview import sendAnnualPositionReviewRequests @@ -67,6 +68,7 @@ def manageDepartments(academicYear = None): currentAY = currentAY, previousAY = previousAY, nextAY = nextAY, + chosenAY = chosenAY, academicYear = chosenAY.termName, breakHoursByDepartment = breakHoursByDepartment, allocationStatus = allocationStatus @@ -93,6 +95,27 @@ def complianceStatusCheck(): +@admin.route('/admin/manageDepartments/annualPositionReview', methods=['POST']) +def annualPositionReviewRequest(): + """ + Sends an Annual Position Review request email to every active department's + 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: + return jsonify({"Success": False}), 403 + + try: + rsp = request.get_json() + result = sendAnnualPositionReviewRequests(int(rsp['academicYear']), currentUser) + return jsonify({"Success": True, **result}) + except Exception as e: + print(e) + return jsonify({"Success": False}) + + + @admin.route('/admin/manageDepartments///allocationReview', methods=['GET']) def allocationReview(org=None, account=None): """ From f9091b02bc37a208afda7d45bcbac78dd6584b7e Mon Sep 17 00:00:00 2001 From: lolongaj Date: Mon, 3 Aug 2026 17:12:35 -0400 Subject: [PATCH 05/11] add js to anchor the request button and emailTemplates.js link to the email template page --- app/static/js/annualPositionReview.js | 33 +++++++++++++++++ app/static/js/emailTemplates.js | 51 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 app/static/js/annualPositionReview.js diff --git a/app/static/js/annualPositionReview.js b/app/static/js/annualPositionReview.js new file mode 100644 index 000000000..c165f19bb --- /dev/null +++ b/app/static/js/annualPositionReview.js @@ -0,0 +1,33 @@ +function submitAnnualPositionReview() { +/* + POSTs the Annual Position Review request for the currently selected academic year. + Sends a review request email to every active department's Labor Coordinators and + supervisors, then shows a success/failure flash message. + + RETURNS: None +*/ + var academicYear = $('[data-target="#annualPositionModal"]').data('academic-year'); + + $.ajax({ + method: "POST", + url: "/admin/manageDepartments/annualPositionReview", + dataType: "json", + contentType: "application/json", + data: JSON.stringify({"academicYear": academicYear}), + success: function(response) { + $("#annualPositionModal").modal("hide"); + + var category, msg; + if (response["Success"]) { + category = "success"; + msg = "Position review requests sent to " + response["sentCount"] + " of " + response["departmentCount"] + " departments."; + } else { + category = "danger"; + msg = "Something went wrong sending the Annual Position Review requests."; + } + + $("#flash_container").html(''); + $("#flasher").delay(3000).fadeOut(); + } + }) +} diff --git a/app/static/js/emailTemplates.js b/app/static/js/emailTemplates.js index 6ef0f715c..f7aba7f02 100644 --- a/app/static/js/emailTemplates.js +++ b/app/static/js/emailTemplates.js @@ -13,10 +13,61 @@ function getEmailArray() { dataType: "json", success: function(response) { emailTemplateArray = response; + prefillFromQueryParams(); } }) } +function prefillFromQueryParams() { + // Allows deep-linking into this page (e.g. from another admin page's + // "Edit Email Template" button) with the Recipient/Form Type/Action + // selectpickers pre-selected, so the template loads without the admin + // having to click through the cascading dropdowns manually. + var params = new URLSearchParams(window.location.search); + var recipient = params.get("audience"); + var formType = params.get("formType"); + var action = params.get("action"); + if (!recipient) { + return; + } + + // populatePurpose() ends by inserting the body into the CKEditor instance, + // which initializes asynchronously (CKEDITOR.replace() in emailTemplates.html). + // Running this cascade immediately on page load (unlike a human clicking + // through the dropdowns) can race ahead of that, so wait for the editor + // to be ready before touching it. + function runPrefill() { + $("#recipient").val(recipient).selectpicker("refresh"); + populateFormType(); + + if (formType) { + $("#formType").val(formType).selectpicker("refresh"); + populateAction(); + + if (action) { + $("#action").val(action).selectpicker("refresh"); + populatePurpose(); + } + } + } + + // CKEDITOR.instances["editor1"] is registered as soon as CKEDITOR.replace() + // is called, well before the editor is actually ready to accept content + // (that's the "ready" status). Checking mere existence isn't enough here. + var editor = CKEDITOR.instances["editor1"]; + if (editor && editor.status === "ready") { + runPrefill(); + } else if (editor) { + editor.on("instanceReady", runPrefill); + } else { + CKEDITOR.on("instanceReady", function(evt) { + if (evt.editor.name === "editor1") { + runPrefill(); + } + }); + } +} + function populateFormType() { // populates Form Type only when Recipient is selected $("#formType").prop("disabled", false); From 338844bffdc29287b3bdd3f09d2cbd545514d60e Mon Sep 17 00:00:00 2001 From: lolongaj Date: Mon, 3 Aug 2026 17:18:13 -0400 Subject: [PATCH 06/11] manageDepartments.html: handle functionality of the Annual Position review btn AND annualPositionReview.html handles the confirmation --- app/templates/admin/manageDepartments.html | 4 +++- app/templates/snips/annualPositionReview.html | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/templates/admin/manageDepartments.html b/app/templates/admin/manageDepartments.html index d8cb2f5ca..2742508f4 100755 --- a/app/templates/admin/manageDepartments.html +++ b/app/templates/admin/manageDepartments.html @@ -7,6 +7,8 @@ src="{{url_for('static', filename='js/manageDepartments.js') }}?u={{lastStaticUpdate}}"> + {% endblock %} {% block app_content %}
Click to Skip @@ -87,7 +89,7 @@

Manage Departments

Annual Allocation Review {% include "snips/annualPositionReview.html" %} -
diff --git a/app/templates/snips/annualPositionReview.html b/app/templates/snips/annualPositionReview.html index dac86a31e..18ba209aa 100644 --- a/app/templates/snips/annualPositionReview.html +++ b/app/templates/snips/annualPositionReview.html @@ -5,7 +5,7 @@ @@ -13,9 +13,9 @@