-
Notifications
You must be signed in to change notification settings - Fork 1
Annual Position Review Button #662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dep_portal_ad_ManageDepartments
Are you sure you want to change the base?
Changes from all commits
661ff17
bc46dae
643d58e
8bbf144
5b76836
3361ecb
f9091b0
338844b
9a31d84
6fb8246
667112d
73eaeb0
60a30f8
d088351
fd6e4c5
48d3a54
f6366b3
80aa951
b92ba12
f0f122e
829421e
9279efc
9ee697d
e9dd270
e9c04bd
e21407d
7bcc558
0e5dc82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,14 +15,16 @@ | |
| 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 | ||
| from app.logic.allocationManager import allocationExists | ||
| from app.logic.academicYearManager import getCurrentAndNextAY | ||
|
|
||
|
|
||
|
|
||
| @admin.route('/admin/manageDepartments/', methods=['GET']) | ||
| def manageDepartments(): | ||
| @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 | ||
| and their allocations. | ||
|
|
@@ -38,8 +40,16 @@ def manageDepartments(): | |
| elif currentUser.supervisor: | ||
| return render_template('errors/403.html'), 403 | ||
|
|
||
| currentAY, nextAY = getCurrentAndNextAY() | ||
| chosenAY = Term.get(Term.termCode == currentAY.termCode) | ||
|
|
||
| # The condition below may be deleted if the routing to the Manage Departments page is changed. | ||
| if academicYear == None: | ||
| academicYear = g.currentAY[0] * 100 | ||
| else: | ||
| academicYear = int(academicYear) | ||
|
|
||
|
|
||
| currentAY, nextAY = generateAdjacentYears(academicYear) | ||
| chosenAY = Term.get(Term.termCode == academicYear) | ||
|
|
||
| breakHoursByDepartment = {row["department"]: str(row["totalHours"] or 0) for row in getUsedBreakHours(chosenAY)} | ||
|
|
||
|
|
@@ -59,6 +69,7 @@ def manageDepartments(): | |
| allSupervisors = allSupervisors, | ||
| currentAY = currentAY, | ||
| nextAY = nextAY, | ||
| chosenAY = chosenAY, | ||
| academicYear = chosenAY.termName, | ||
| breakHoursByDepartment = breakHoursByDepartment, | ||
| allocationStatus = allocationStatus | ||
|
|
@@ -85,6 +96,36 @@ def complianceStatusCheck(): | |
|
|
||
|
|
||
|
|
||
| @admin.route('/admin/manageDepartments/annualPositionReview', methods=['POST']) | ||
| def annualPositionReviewRequest(): | ||
| """ | ||
| Sends an Annual Position Review request email to every active department's | ||
|
GalinaP7 marked this conversation as resolved.
|
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of jsonify({"success":false}) we usually use render_template('errors/403.html') |
||
|
|
||
| rsp = request.get_json(silent=True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this rsp is receiving the chosen academic year from javascript from the front-end we no longer allow the users to chose a academic year.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to select a academic year we are doing position review for the positions in each department means this likely is a interval of either 3 years. we are not allowing the labor department to select which year its more of in intervals the positions are being reviewed. |
||
|
|
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not handler the variable would most likely be email something like positionReviewEmail. |
||
| result = handler.sendAnnualPositionReviewRequests(currentUser) | ||
| if result["failedDepartments"]: | ||
| result["message"] = "Requests sent to {} of {} departments. Failed departments: {}.".format( | ||
| result["sentCount"], result["departmentCount"], ", ".join(result["failedDepartments"]) | ||
| ) | ||
| return jsonify({"Success": False, **result}) | ||
| 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): | ||
| """ | ||
|
|
@@ -116,12 +157,12 @@ def allocationReview(org=None, account=None): | |
|
|
||
|
|
||
| # checking if the allocation has already been approved | ||
| if allocationExists(nextAY.termCode, dept, isFinal=True): | ||
| 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 | ||
|
|
||
| # 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/') | ||
|
|
@@ -133,7 +174,7 @@ def allocationReview(org=None, account=None): | |
|
|
||
|
|
||
| return render_template('admin/allocationReview.html', | ||
| department = dept, | ||
| department = dept, | ||
| nextAY = nextAY, | ||
| currentAlloc = currentAlloc, | ||
| requestedAlloc = requestedAlloc | ||
|
|
@@ -143,7 +184,7 @@ def allocationReview(org=None, account=None): | |
|
|
||
| @admin.route('/admin/allocationReview/approve', methods=['POST']) | ||
| def approveAllocationReview(): | ||
|
|
||
| # Retrieving the current and following academic years | ||
| currentAY, nextAY = getCurrentAndNextAY() | ||
|
|
||
|
|
@@ -154,8 +195,8 @@ def approveAllocationReview(): | |
| requester = request.form.get("requester", type=int, default=None) | ||
|
|
||
| # saving the newly approved allocation | ||
| newApprovedAlloc = Allocation.create(termCode = nextAY.termCode, | ||
| department = requester, | ||
| newApprovedAlloc = Allocation.create(termCode = nextAY.termCode, | ||
| department = requester, | ||
| isFinal = True, | ||
| approvedBy = approverID, | ||
| approvedOn = date.today(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.positionHistory import PositionHistory | ||
| from app.logic.getSupervisors import getSupervisors | ||
|
|
||
|
|
||
| class emailHandler(): | ||
| def __init__(self, formHistoryKey): | ||
| def __init__(self, formHistoryKey=None, academicYearTermCode=None): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. academicYearTermCode is general like how formHistoryKey is not related to email but it is a distinction of what the emailhandler do we should turn the parameter naming of academicYearTermCode to something that resembles its purpose. This academicYearTermCode is going to use in two instance the allocation review email and position review email. So the parameter naming can be termCodeUnderReview, targetTermCode and more |
||
| 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']: | ||
|
|
@@ -110,7 +124,67 @@ 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_or_none(EmailTemplate.purpose == "Annual Position Review Request") | ||
| if template is None: | ||
| raise ValueError("The 'Annual Position Review Request' email template is missing.") | ||
|
|
||
| departments = Department.select().where(Department.isActive == True) | ||
|
|
||
| sentCount = 0 | ||
| failedDepartments = [] | ||
| for department in departments: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this logic is a function on its own emailHandler methods are only here to handle different email logic.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. existingReview = PositionHistory.get_or_none( |
||
| # 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 = PositionHistory.get_or_none( | ||
| PositionHistory.academicYear == self.term, | ||
| PositionHistory.department == department, | ||
| PositionHistory.positionCode.is_null(True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this logic feels iffy. the idea that we are considering existingreview to be based off from positioncode being null isn't a good logic here. I would encourage to see the composite key in positionhistory and see whether a position should be reviewed based on how long it has not been reviewed and what status it is. i believe status wise we should be reviewing the active status. |
||
| ) | ||
| if existingReview: | ||
| existingReview.requestedOn = datetime.now() | ||
| existingReview.requestedBy = requestingUser | ||
| existingReview.save() | ||
| else: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think reviewing means creating a positionhistory |
||
| PositionHistory.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) | ||
|
|
||
| try: | ||
| message = Message(subject, recipients=list(recipients)) | ||
| message.html = body | ||
| self.send(message) | ||
| except Exception as error: | ||
| failedDepartments.append(department.DEPT_NAME) | ||
| print("Failed to send Annual Position Review request for department {}: {}".format(department.DEPT_NAME, error)) | ||
| continue | ||
|
|
||
| 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(), | ||
| "failedDepartments": failedDepartments | ||
| } | ||
|
|
||
| # 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. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
academicYear = g.currentAY[0] * 100 if academicYear is None else int(academicYear)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if else can be consolidated in a conditional expression