diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md new file mode 100644 index 000000000..234bfa5ae --- /dev/null +++ b/.github/workflows/pull_request_template.md @@ -0,0 +1,14 @@ +## Issue Description + +Fixes # + + +## Changes + + + + +## Testing + + + diff --git a/app/__init__.py b/app/__init__.py index 75e9f6ca8..206a5fc7b 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,7 +15,8 @@ app.config['use_shibboleth'] = (app.config['ENV'] == 'production') app.config['use_tracy'] = (app.config['ENV'] in ('production','staging')) -app.config['use_banner'] = (app.config['ENV'] in ('production','staging')) +if 'use_banner' not in app.config.keys(): + app.config['use_banner'] = (app.config['ENV'] in ('production','staging')) # Record and output queries if requested from flask import session diff --git a/app/controllers/admin_routes/adminManagement.py b/app/controllers/admin_routes/adminManagement.py index bc70a5add..68590cd70 100644 --- a/app/controllers/admin_routes/adminManagement.py +++ b/app/controllers/admin_routes/adminManagement.py @@ -1,5 +1,4 @@ from app.controllers.admin_routes import * -from app.models.user import User, DoesNotExist from app.models.user import * from app.controllers.admin_routes import admin from flask import request @@ -8,7 +7,10 @@ from app.models.supervisor import Supervisor from app.models.student import Student from app.logic.tracy import Tracy -from app.logic.userInsertFunctions import createUser, createSupervisorFromTracy, createStudentFromTracy +from app.logic.userInsertFunctions import createStudentFromTracy, createSupervisorFromTracy, createUser +from app.logic.adminManagement import searchForAdmin, getUser +from app.logic.utils import adminFlashMessage + @admin.route('/admin/adminManagement', methods=['GET']) # @login_required @@ -23,7 +25,9 @@ def admin_Management(): elif currentUser.supervisor: return render_template('errors/403.html'), 403 - users = User.select() + users = (User.select(User,Supervisor,Student) + .join(Supervisor,join_type=JOIN.LEFT_OUTER).switch() + .join(Student,join_type=JOIN.LEFT_OUTER)) return render_template( 'admin/adminManagement.html', title=('Admin Management'), users = users @@ -37,47 +41,7 @@ def adminSearch(): """ try: rsp = eval(request.data.decode("utf-8")) - userInput = rsp[1] - adminType = rsp[0] - userList = [] - if adminType == "addlaborAdmin": - tracyStudents = Tracy().getStudentsFromUserInput(userInput) - students = [] - for student in tracyStudents: - try: - existingUser = User.get(User.student == student.ID) - if existingUser.isLaborAdmin: - pass - else: - students.append(student) - except DoesNotExist as e: - students.append(student) - for student in students: - username = student.STU_EMAIL.split('@', 1) - userList.append({'username': username[0], - 'firstName': student.FIRST_NAME, - 'lastName': student.LAST_NAME, - 'type': 'Student' - }) - tracySupervisors = Tracy().getSupervisorsFromUserInput(userInput) - supervisors = [] - for supervisor in tracySupervisors: - try: - existingUser = User.get(User.supervisor == supervisor.ID) - if ((existingUser.isLaborAdmin and adminType == "addlaborAdmin") - or (existingUser.isSaasAdmin and adminType == "addSaasAdmin") - or (existingUser.isFinancialAidAdmin and adminType == "addFinAidAdmin")): - pass - else: - supervisors.append(supervisor) - except DoesNotExist as e: - supervisors.append(supervisor) - for sup in supervisors: - username = sup.EMAIL.split('@', 1) - userList.append({'username': username[0], - 'firstName': sup.FIRST_NAME, - 'lastName': sup.LAST_NAME, - 'type': 'Supervisor'}) + userList = searchForAdmin(rsp) return jsonify(userList) except Exception as e: print('ERROR Loading Non Labor Admins:', e, type(e)) @@ -85,69 +49,38 @@ def adminSearch(): @admin.route("/adminManagement/userInsert", methods=['POST']) def manageLaborAdmin(): - if request.form.get("addAdmin"): - newAdmin = getUser('addAdmin') - addAdmin(newAdmin, 'labor') - flashMessage(newAdmin, 'added', 'Labor') - - elif request.form.get("removeAdmin"): - oldAdmin = getUser('removeAdmin') - removeAdmin(oldAdmin, 'labor') - flashMessage(oldAdmin, 'removed', 'Labor') - - elif request.form.get("addFinancialAidAdmin"): - newAdmin = getUser('addFinancialAidAdmin') - addAdmin(newAdmin, 'finAid') - flashMessage(newAdmin, 'added', 'Financial Aid') - - elif request.form.get("removeFinancialAidAdmin"): - oldAdmin = getUser('removeFinancialAidAdmin') - removeAdmin(oldAdmin, 'finAid') - flashMessage(oldAdmin, 'removed', 'Financial Aid') + actionMap = { + "addLaborAdmin": {"selectPickerID": "addAdmin", "type": "Labor", "action": "add", "pretty": "Labor"}, + "removeLaborAdmin": {"selectPickerID": "removeAdmin", "type": "Labor", "action": "remove", "pretty": "Labor"}, + "addFinAidAdmin": {"selectPickerID": "addFinancialAidAdmin", "type": "FinancialAid", "action": "add", "pretty": "Financial Aid"}, + "removeFinAidAdmin": {"selectPickerID": "removeFinancialAidAdmin", "type": "FinancialAid", "action": "remove", "pretty": "Financial Aid"}, + "addSaasAdmin": {"selectPickerID": "addSAASAdmin", "type": "Saas", "action": "add", "pretty": "SAAS"}, + "removeSaasAdmin": {"selectPickerID": "removeSAASAdmin", "type": "Saas", "action": "remove", "pretty": "SAAS"}, + } - elif request.form.get("addSAASAdmin"): - newAdmin = getUser('addSAASAdmin') - addAdmin(newAdmin, 'saas') - flashMessage(newAdmin, 'added', 'SAAS') - - elif request.form.get("removeSAASAdmin"): - oldAdmin = getUser('removeSAASAdmin') - removeAdmin(oldAdmin, 'saas') - flashMessage(oldAdmin, 'removed', 'SAAS') + key = request.form.get('action') + meta = actionMap[key] + user = getUser(actionMap[key]['selectPickerID']) + # pick addAdmin or removeAdmin dynamically + if meta['action'] == 'add': + addAdmin(user, meta['type']) + else: + removeAdmin(user, meta['type']) + + flashMessage(user, + 'added' if meta["action"] == "add" else 'removed', + meta["pretty"]) + return redirect(url_for('admin.admin_Management')) -def getUser(selectpickerID): - username = request.form.get(selectpickerID) - try: - user = User.get(User.username == username) - except DoesNotExist as e: - usertype = Tracy().checkStudentOrSupervisor(username) - supervisor = student = None - if usertype == "Student": - student = createStudentFromTracy(username) - else: - supervisor = createSupervisorFromTracy(username) - user = createUser(username, student=student, supervisor=supervisor) - return user - -def addAdmin(newAdmin, adminType): - if adminType == 'labor': - newAdmin.isLaborAdmin = True - if adminType == 'finAid': - newAdmin.isFinancialAidAdmin = True - if adminType == 'saas': - newAdmin.isSaasAdmin = True - newAdmin.save() +def addAdmin(user, adminType): + setattr(user, f"is{adminType}Admin", True) + user.save() -def removeAdmin(oldAdmin, adminType): - if adminType == 'labor': - oldAdmin.isLaborAdmin = False - if adminType == 'finAid': - oldAdmin.isFinancialAidAdmin = False - if adminType == 'saas': - oldAdmin.isSaasAdmin = False - oldAdmin.save() +def removeAdmin(user, adminType): + setattr(user, f"is{adminType}Admin", False) + user.save() def flashMessage(user, action, adminType): message = "{} has been {} as a {} Admin".format(user.fullName, action, adminType) diff --git a/app/controllers/admin_routes/allPendingForms.py b/app/controllers/admin_routes/allPendingForms.py index 564610e67..ef3499db2 100644 --- a/app/controllers/admin_routes/allPendingForms.py +++ b/app/controllers/admin_routes/allPendingForms.py @@ -3,6 +3,7 @@ import json from peewee import JOIN +from app.models import mainDB from app.login_manager import * from app.controllers.admin_routes import admin from app.controllers.errors_routes.handlers import * @@ -13,20 +14,19 @@ from app.models.overloadForm import OverloadForm from app.models.notes import Notes from app.logic.emailHandler import emailHandler -from app.logic.utils import makeThirdPartyLink -from app.models.formHistory import * +from app.logic.utils import makeThirdPartyLink, calculateExpirationDate +from app.models.formHistory import FormHistory from app.models.term import Term from app.logic.banner import Banner from app.logic.tracy import Tracy -from app.logic.userInsertFunctions import calculateExpirationDate from app.models.Tracy.stuposn import STUPOSN from app.models.supervisor import Supervisor from app.models.student import Student from app.models.historyType import HistoryType from app.models.department import Department -from app.controllers.main_routes.download import CSVMaker -from app.models import mainDB -from app.logic.download import saveFormSearchResult, retrieveFormSearchResult +from app.models.status import Status +from app.logic.allPendingForms import saveStatus, laborAdminOverloadApproval, financialAidSAASOverloadApproval, modal_approval_and_denial_data, checkAdjustment +from app.logic.download import CSVMaker, saveFormSearchResult, retrieveFormSearchResult @admin.route('/admin/pendingForms/', methods=['GET']) def allPendingForms(formType): @@ -187,37 +187,6 @@ def allPendingForms(formType): print("Error Loading all Pending Forms:", e) return render_template('errors/500.html'), 500 -def checkAdjustment(allForms): - """ - Retrieve supervisor and position information for adjusted forms using the new values - stored in adjusted table and update allForms - """ - if allForms.adjustedForm: - - if allForms.adjustedForm.fieldAdjusted == "supervisor": - # use the supervisor id in the field adjusted to find supervisor in User table. - newSupervisorID = allForms.adjustedForm.newValue - newSupervisor = Supervisor.get(Supervisor.ID == newSupervisorID) - if not newSupervisor: - newSupervisor = createSupervisorFromTracy(bnumber=newSupervisorID) - - # we are temporarily storing the supervisor name in new value, - # because we want to show the supervisor name in the hmtl template. - allForms.adjustedForm.newValue = newSupervisor.FIRST_NAME +" "+ newSupervisor.LAST_NAME - allForms.adjustedForm.oldValue = {"email":newSupervisor.EMAIL, "ID":newSupervisor.ID} - - if allForms.adjustedForm.fieldAdjusted == "position": - newPositionCode = allForms.adjustedForm.newValue - newPosition = Tracy().getPositionFromCode(newPositionCode) - # temporarily storing the position code and wls in new value, and position name in old value - # because we want to show these information in the hmtl template. - allForms.adjustedForm.newValue = newPosition.POSN_CODE +" (" + newPosition.WLS+")" - allForms.adjustedForm.oldValue = newPosition.POSN_TITLE - - if allForms.adjustedForm.fieldAdjusted == "department": - newDepartment = Department.get(Department.ORG==allForms.adjustedForm.newValue) - allForms.adjustedForm.newValue = newDepartment.DEPT_NAME - allForms.adjustedForm.oldValue = newDepartment.ORG + "-" + newDepartment.ACCOUNT @admin.route('/admin/pendingForms/download', methods=['POST']) def downloadAllPendingForms(): @@ -348,165 +317,10 @@ def finalUpdateStatus(raw_status): print("Unknown status: ", raw_status) return jsonify({"success": False}) + flash("Forms have been successfully updated.", "success") form_ids = eval(request.data.decode("utf-8")) return saveStatus(new_status, form_ids, currentUser) -def saveStatus(new_status, formHistoryIds, currentUser): - try: - if new_status == 'Denied by Admin': - # Index 1 will always hold the reject reason in the list, so we can - # set a variable equal to the index value and then slice off the list - # item before the iteration - denyReason = formHistoryIds[1] - formHistoryIds = formHistoryIds[:1] - - for formHistoryID in formHistoryIds: - formHistory = FormHistory.get(FormHistory.formHistoryID == formHistoryID) - formHistory.status = Status.get(Status.statusName == new_status) - - formHistory.reviewedDate = date.today() - formHistory.reviewedBy = currentUser - - formType = formHistory.historyType_id - - # Add a note if we've skipped to Pending - if new_status == 'Pending': - note = "Skipped Student Approval" - Notes.create(formID=formHistory.formID, - createdBy=currentUser, - date=date.today(), - notesContents=note, - noteType = "Labor Note") - - - # Add to BANNER - save_status = True # default true so that we will still save in other cases - - if new_status == 'Approved' and formType == "Labor Status Form" and formHistory.formID.POSN_CODE != "S12345": # don't update banner for Adjustment forms or for CS dummy position - if formHistory.formID.POSN_CODE == "SNOLAB": - formHistory.formID.weeklyHours = 10 - conn = Banner() - save_status = conn.insert(formHistory) - - # if we are able to save - if save_status: - - if new_status == 'Denied by Admin': - formHistory.rejectReason = denyReason - formHistory.save() - - # Send necessary emails from the status change - email = emailHandler(formHistory.formHistoryID) - if new_status == "Denied by Admin" and formType == "Labor Status Form": - email.laborStatusFormRejected() - if new_status == "Approved" and formType == "Labor Status Form": - email.laborStatusFormApproved() - if new_status == "Approved" and formType == "Labor Adjustment Form": - # This function is triggered whenever an adjustment form is approved. - # The following function overrides the original data in lsf with the new data from adjustment form. - LSF = LaborStatusForm.get_by_id(formHistory.formID) - overrideOriginalStatusFormOnAdjustmentFormApproval(formHistory, LSF) - - else: - print("Unable to update form status for formHistoryID {}.".format(id)) - return jsonify({"success": False}), 500 - - except Exception as e: - print("Error preparing form for status update:", e) - return jsonify({"success": False}), 500 - - return jsonify({"success": True}) - - - - -def overrideOriginalStatusFormOnAdjustmentFormApproval(form, LSF): - """ - This function checks whether an Adjustment Form is approved. If yes, it overrides the information - in the original Labor Status Form with the new information coming from approved Adjustment Form. - - The only fields that will ever be changed in an adjustment form are: supervisor, department, position, and hours. - """ - currentUser = require_login() - if not currentUser: # Not logged in - return render_template('errors/403.html'), 403 - if form.adjustedForm.fieldAdjusted == "supervisor": - d, created = Supervisor.get_or_create(ID = form.adjustedForm.newValue) - if not created: - LSF.supervisor = d.ID - LSF.save() - if created: - tracyUser = Tracy().getSupervisorFromID(form.adjustedForm.newValue) - tracyEmail = tracyUser.EMAIL - tracyUsername = tracyEmail.find('@') - createSupervisorFromTracy(tracyUsername) - - if form.adjustedForm.fieldAdjusted == "position": - LSF.POSN_CODE = form.adjustedForm.newValue - position = Tracy().getPositionFromCode(form.adjustedForm.newValue) - LSF.POSN_TITLE = position.POSN_TITLE - LSF.WLS = position.WLS - LSF.save() - - if form.adjustedForm.fieldAdjusted == "department": - department = Department.get(Department.ORG==form.adjustedForm.newValue) - LSF.department = department.departmentID - LSF.save() - - if form.adjustedForm.fieldAdjusted == "contractHours": - LSF.contractHours = int(form.adjustedForm.newValue) - LSF.save() - - if form.adjustedForm.fieldAdjusted == "weeklyHours": - LSF.weeklyHours = int(form.adjustedForm.newValue) - LSF.save() - - -#method extracts data from the data base to papulate pending form approvale modal -def modal_approval_and_denial_data(approval_ids): - ''' This method grabs the data that populated the on approve modal for lsf''' - - id_list = [] - for formHistoryID in approval_ids: - formHistory = FormHistory.get(FormHistory.formHistoryID == int(formHistoryID)) - fhistory_id = LaborStatusForm.select().join(FormHistory).where(FormHistory.formHistoryID == int(formHistoryID)).get() - student_details = LaborStatusForm.get(LaborStatusForm.laborStatusFormID == fhistory_id) - student_firstname, student_lastname = student_details.studentSupervisee.FIRST_NAME, student_details.studentSupervisee.LAST_NAME - student_name = str(student_firstname) + " " + str(student_lastname) - student_pos = student_details.POSN_TITLE - supervisor_firstname, supervisor_lastname = student_details.supervisor.FIRST_NAME, student_details.supervisor.LAST_NAME - supervisor_name = str(supervisor_firstname) + " " + str(supervisor_lastname) - student_hours = student_details.weeklyHours - student_hours_ch = student_details.contractHours - student_dept = student_details.department.DEPT_NAME - - if formHistory.adjustedForm: - if formHistory.adjustedForm.fieldAdjusted == "position": - position = Tracy().getPositionFromCode(formHistory.adjustedForm.newValue) - student_pos = position.POSN_TITLE - if formHistory.adjustedForm.fieldAdjusted == "supervisor": - supervisor = Supervisor.get(Supervisor.ID == formHistory.adjustedForm.newValue) - supervisor_firstname, supervisor_lastname = supervisor.FIRST_NAME, supervisor.LAST_NAME - supervisor_name = str(supervisor_firstname) +" "+ str(supervisor_lastname) - if formHistory.adjustedForm.fieldAdjusted == "weeklyHours": - student_hours = formHistory.adjustedForm.newValue - if formHistory.adjustedForm.fieldAdjusted == "contractHours": - student_hours_ch = formHistory.adjustedForm.newValue - if formHistory.adjustedForm.fieldAdjusted == "department": - department = Department.get(Department.ORG==formHistory.adjustedForm.newValue) - student_dept = department.DEPT_NAME - - tempList = [] - tempList.append(student_name) - tempList.append(student_dept) - tempList.append(student_pos) - tempList.append(str(student_hours)) - tempList.append(str(student_hours_ch)) - tempList.append(supervisor_name) - id_list.append(tempList) - return(id_list) - - @admin.route('/admin/getNotes/', methods=['GET']) def getNotes(formid): ''' @@ -659,103 +473,6 @@ def getReleaseModalData(formHistoryID): print("Error Populating Release Modal:", e) return render_template('errors/500.html'), 500 -def financialAidSAASOverloadApproval(historyForm, rsp, status, currentUser, currentDate): - selectedOverload = OverloadForm.get(OverloadForm.overloadFormID == historyForm.overloadForm.overloadFormID) - if 'denialReason' in rsp.keys(): - newNoteEntry = Notes.create(formID=historyForm.formID.laborStatusFormID, - createdBy=currentUser, - date=currentDate, - notesContents=rsp["denialReason"], - noteType = "Labor Note") - newNoteEntry.save() - ## Updating the overloadform TableS - if currentUser.isFinancialAidAdmin: - selectedOverload.financialAidApproved = status.statusName - selectedOverload.financialAidApprover = currentUser - selectedOverload.financialAidInitials = rsp['initials'] - selectedOverload.financialAidReviewDate = currentDate - - elif currentUser.isSaasAdmin: - selectedOverload.SAASApproved = status.statusName - selectedOverload.SAASApprover = currentUser - selectedOverload.SAASInitials = rsp['initials'] - selectedOverload.SAASReviewDate = currentDate - selectedOverload.save() - return jsonify({"Success": True}) - -def laborAdminOverloadApproval(rsp, historyForm, status, currentUser, currentDate, email): - if rsp['formType'] == 'Overload': - overloadForm = OverloadForm.get(OverloadForm.overloadFormID == historyForm.overloadForm.overloadFormID) - overloadForm.laborApproved = status.statusName - overloadForm.laborApprover = currentUser - overloadForm.laborReviewDate = currentDate - overloadForm.save() - try: - pendingForm = FormHistory.select().where((FormHistory.formID == historyForm.formID) & (FormHistory.status == "Pending") & (FormHistory.historyType != "Labor Overload Form")).get() - if historyForm.adjustedForm and rsp['status'] == "Approved": - LSF = LaborStatusForm.get(LaborStatusForm.laborStatusFormID == historyForm.formID) - if historyForm.adjustedForm.fieldAdjusted == "weeklyHours": - LSF.weeklyHours = pendingForm.adjustedForm.newValue - LSF.save() - if pendingForm.historyType.historyTypeName == "Labor Status Form" or (pendingForm.historyType.historyTypeName == "Labor Adjustment Form" and pendingForm.adjustedForm.fieldAdjusted == "weeklyHours"): - if status.statusName == "Approved Reluctantly": - pendingForm.status = "Approved" - else: - pendingForm.status = status.statusName - pendingForm.reviewedBy = currentUser - pendingForm.reviewedDate = currentDate - if 'denialReason' in rsp.keys(): - pendingForm.rejectReason = rsp['denialReason'] - Notes.create(formID = pendingForm.formID.laborStatusFormID, - createdBy = currentUser, - date = currentDate, - notesContents = rsp['denialReason'], - noteType = "Labor Note") - pendingForm.save() - - if pendingForm.historyType.historyTypeName == "Labor Status Form": - email = emailHandler(pendingForm.formHistoryID) - if rsp['status'] in ['Approved', 'Approved Reluctantly']: - email.laborStatusFormApproved() - elif rsp['status'] == 'Denied by Admin': - email.laborStatusFormRejected() - except DoesNotExist: - pass - except Exception as e: - print(e) - if 'denialReason' in rsp.keys(): - # We only update the reject reason if one was given on the UI - historyForm.rejectReason = rsp['denialReason'] - historyForm.save() - Notes.create(formID = historyForm.formID.laborStatusFormID, - createdBy = currentUser, - date = currentDate, - notesContents = rsp['denialReason'], - noteType = "Labor Note") - if 'adminNotes' in rsp.keys(): - # We only add admin notes if there was a note made on the UI - Notes.create(formID = historyForm.formID.laborStatusFormID, - createdBy = currentUser, - date = currentDate, - notesContents = rsp['adminNotes'], - noteType = "Labor Note") - historyForm.status = status.statusName - historyForm.reviewedBy = currentUser - historyForm.reviewedDate = currentDate - historyForm.save() - if rsp['formType'] == 'Overload': - if rsp['status'] in ['Approved', 'Approved Reluctantly']: - email.LaborOverLoadFormApproved() - elif rsp['status'] == 'Denied by Admin': - email.LaborOverLoadFormRejected() - elif rsp['formType'] == 'Release': - if rsp['status'] == 'Approved': - email.laborReleaseFormApproved() - elif rsp['status'] == 'Denied by Admin': - email.laborReleaseFormRejected() - return jsonify({"Success": True}) - - @admin.route('/admin/modalFormUpdate', methods=['POST']) def modalFormUpdate(): """ @@ -780,7 +497,6 @@ def modalFormUpdate(): conn = Banner() save_form_status = conn.insert(historyForm) - # if we are able to save if save_form_status: # This try is to handle Overload Forms diff --git a/app/controllers/admin_routes/emailTemplateController.py b/app/controllers/admin_routes/emailTemplateController.py index 89b9e06f9..f612c723f 100644 --- a/app/controllers/admin_routes/emailTemplateController.py +++ b/app/controllers/admin_routes/emailTemplateController.py @@ -32,7 +32,7 @@ def getEmailArray(): "audience": template.audience, "formType": template.formType, "action": template.action - } for template in EmailTemplate.select()]) + } for template in templates]) @admin.route('/admin/emailTemplates/getPurpose/', methods=['GET']) diff --git a/app/controllers/admin_routes/financialAidOverload.py b/app/controllers/admin_routes/financialAidOverload.py index f9f04fbc6..6fd1f2b8e 100644 --- a/app/controllers/admin_routes/financialAidOverload.py +++ b/app/controllers/admin_routes/financialAidOverload.py @@ -33,7 +33,7 @@ def financialAidOverload(formHistoryID): # IF YES: populate the "Current Position" Fields with the information from that labor status form. allLaborStatusForms = (LaborStatusForm.select() .join(FormHistory) - .where(FormHistory.status_id.in_(["Approved", "Approved Reluctantly", "Pending"]), + .where(FormHistory.status_id.in_(["Approved", "Pending"]), FormHistory.historyType_id == "Labor Status Form", LaborStatusForm.studentSupervisee == lsfForm.studentSupervisee.ID, LaborStatusForm.termCode == lsfForm.termCode)) diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 3f6094aab..54b32d3c5 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -19,7 +19,7 @@ def injectGlobalData(): from app.controllers.main_routes import alterLSF from app.controllers.main_routes import studentOverloadApp from app.controllers.main_routes import contributors -from app.controllers.main_routes import download +from app.logic import download from app.controllers.main_routes import laborReleaseForm from app.controllers.main_routes import contributors from app.controllers.main_routes import studentLaborEvaluation diff --git a/app/controllers/main_routes/alterLSF.py b/app/controllers/main_routes/alterLSF.py index f1a97b47f..c6edd850a 100644 --- a/app/controllers/main_routes/alterLSF.py +++ b/app/controllers/main_routes/alterLSF.py @@ -1,22 +1,19 @@ +from datetime import date, datetime +from flask import json, jsonify, request, flash from app.controllers.main_routes import * from app.controllers.main_routes.main_routes import * from app.controllers.main_routes.laborHistory import * from app.models.formHistory import FormHistory from app.models.user import User from app.models.supervisor import Supervisor -from app.models.department import Department -from app.models.adjustedForm import AdjustedForm from app.logic.userInsertFunctions import createSupervisorFromTracy from app.logic.emailHandler import * from app.login_manager import require_login -from app.logic.tracy import Tracy -from app.logic.utils import makeThirdPartyLink +from app.logic.tracy import Tracy, InvalidQueryException from app.models.notes import Notes from app.models.supervisor import Supervisor from app.login_manager import require_login -from datetime import date, datetime -from flask import json, jsonify, request, flash -import base64 +from app.logic.alterLSF import modifyLSF, adjustLSF @main_bp.route("/alterLSF/", methods=["GET"]) @@ -177,130 +174,3 @@ def submitAlteredLSF(laborStatusKey): return jsonify({"Success": False}), 500 -def modifyLSF(fieldsChanged, fieldName, lsf, currentUser, host=None): - if fieldName == "supervisorNotes": - noteEntry = Notes.create(formID = lsf.laborStatusFormID, - createdBy = currentUser, - date = datetime.now().strftime("%Y-%m-%d"), - notesContents = fieldsChanged[fieldName]["newValue"], - noteType = "Supervisor Note") - noteEntry.save() - lsf.supervisorNotes = noteEntry.notesContents - lsf.save() - if fieldName == "supervisor": - supervisor = createSupervisorFromTracy(bnumber=fieldsChanged[fieldName]["newValue"]) - lsf.supervisor = supervisor.ID - lsf.save() - - if fieldName == "department": - department = Department.get(Department.ORG==fieldsChanged[fieldName]['newValue']) - lsf.department = department.departmentID - lsf.save() - - if fieldName == "position": - position = Tracy().getPositionFromCode(fieldsChanged[fieldName]["newValue"]) - lsf.POSN_CODE = position.POSN_CODE - lsf.POSN_TITLE = position.POSN_TITLE - lsf.WLS = position.WLS - lsf.save() - - if fieldName == "weeklyHours": - newWeeklyHours = int(fieldsChanged[fieldName]['newValue']) - createOverloadForm(newWeeklyHours, lsf, currentUser, host=host) - lsf.weeklyHours = newWeeklyHours - lsf.save() - - if fieldName == "contractHours": - lsf.contractHours = int(fieldsChanged[fieldName]["newValue"]) - lsf.save() - - if fieldName == "startDate": - lsf.startDate = datetime.strptime(fieldsChanged[fieldName]["newValue"], "%m/%d/%Y").strftime('%Y-%m-%d') - lsf.save() - - if fieldName == "endDate": - lsf.endDate = datetime.strptime(fieldsChanged[fieldName]["newValue"], "%m/%d/%Y").strftime('%Y-%m-%d') - lsf.save() - - -def adjustLSF(fieldsChanged, fieldName, lsf, currentUser, host=None): - if fieldName == "supervisorNotes": - newNoteEntry = Notes.create(formID = lsf.laborStatusFormID, - createdBy = currentUser, - date = datetime.now().strftime("%Y-%m-%d"), - notesContents = fieldsChanged[fieldName]["newValue"], - noteType = "Supervisor Note") - newNoteEntry.save() - return None - else: - adjustedforms = AdjustedForm.create(fieldAdjusted = fieldName, - oldValue = fieldsChanged[fieldName]["oldValue"], - newValue = fieldsChanged[fieldName]["newValue"], - effectiveDate = datetime.strptime(fieldsChanged[fieldName]["date"], "%m/%d/%Y").strftime("%Y-%m-%d")) - historyType = HistoryType.get(HistoryType.historyTypeName == "Labor Adjustment Form") - status = Status.get(Status.statusName == "Pending") - adjustedFormHistory = FormHistory.create(formID = lsf.laborStatusFormID, - historyType = historyType.historyTypeName, - adjustedForm = adjustedforms.adjustedFormID, - createdBy = currentUser, - createdDate = date.today(), - status = status.statusName) - - if fieldName == "weeklyHours": - newWeeklyHours = int(fieldsChanged[fieldName]['newValue']) - createOverloadForm(newWeeklyHours, lsf, currentUser, adjustedforms.adjustedFormID, adjustedFormHistory,host=host) - - return adjustedFormHistory.formHistoryID - -def createOverloadForm(newWeeklyHours, lsf, currentUser, adjustedForm=None, formHistories=None, host=None): - allTermForms = LaborStatusForm.select() \ - .join_from(LaborStatusForm, Student) \ - .join_from(LaborStatusForm, FormHistory) \ - .where((LaborStatusForm.termCode == lsf.termCode) & - (LaborStatusForm.studentSupervisee.ID == lsf.studentSupervisee.ID) & - ~(FormHistory.status % "Denied%") & - (FormHistory.historyType == "Labor Status Form")) - - - previousTotalHours = 0 - if allTermForms: - for statusForm in allTermForms: - previousTotalHours += statusForm.weeklyHours - changeInHours = newWeeklyHours - lsf.weeklyHours - newTotalHours = previousTotalHours + changeInHours - - if previousTotalHours <= 15 and newTotalHours > 15: # If we weren't overloading and now we are - newLaborOverloadForm = OverloadForm.create(studentOverloadReason = "None") - newFormHistory = FormHistory.create(formID = lsf.laborStatusFormID, - historyType = "Labor Overload Form", - createdBy = currentUser, - adjustedForm = adjustedForm, - overloadForm = newLaborOverloadForm.overloadFormID, - createdDate = date.today(), - status = "Pre-Student Approval") - try: - if formHistories: - formHistories.status = "Pre-Student Approval" - formHistories.save() - - else: - modifiedFormHistory = FormHistory.select() \ - .join_from(FormHistory, HistoryType) \ - .where(FormHistory.formID == lsf.laborStatusFormID, FormHistory.historyType.historyTypeName == "Labor Status Form") \ - .get() - modifiedFormHistory.status = "Pre-Student Approval" - modifiedFormHistory.save() - - link = makeThirdPartyLink("student", host, newFormHistory.formHistoryID) - overloadEmail = emailHandler(newFormHistory.formHistoryID) - overloadEmail.LaborOverLoadFormSubmitted(link) - - except Exception as e: - print("An error occured while attempting to send overload form emails: ", e) - - # This will delete an overload form after the hours are changed - elif previousTotalHours > 15 and newTotalHours <= 15: # If we were overloading and now we aren't - print(f"Trying to get formhistory with formID '{lsf.laborStatusFormID}' and history type: 'Labor Overload Form'") - deleteOverloadForm = FormHistory.get((FormHistory.formID == lsf.laborStatusFormID) & (FormHistory.historyType == "Labor Overload Form")) - deleteOverloadForm = OverloadForm.get(OverloadForm.overloadFormID == deleteOverloadForm.overloadForm_id) - deleteOverloadForm.delete_instance() # This line also deletes the Form History since it's set to cascade up in the model file diff --git a/app/controllers/main_routes/download.py b/app/controllers/main_routes/download.py deleted file mode 100755 index 368084973..000000000 --- a/app/controllers/main_routes/download.py +++ /dev/null @@ -1,208 +0,0 @@ -from peewee import ModelSelect -from app.models.formHistory import * -import csv -from app.controllers.main_routes.main_routes import * -from app.models.studentLaborEvaluation import StudentLaborEvaluation - -class CSVMaker: - ''' - Create the CSV for the download bottons - ''' - def __init__(self, downloadName, requestedLSFs: ModelSelect, includeEvals = False, additionalSpreadsheetFields: list[str] = []): - self.relativePath = f'static/files/{downloadName}.csv' - self.completePath = 'app/' + self.relativePath - self.additionalSpreadsheetFields = (self._validateAdditionalSpreadsheetFields(additionalSpreadsheetFields)) - self.formHistories = requestedLSFs - self.includeEvals = includeEvals - self.makeCSV() - - @staticmethod - def _validateAdditionalSpreadsheetFields(additionalFields): - for additionalField in additionalFields: - if additionalField not in {'overloads', 'finalEvaluations', 'midYearEvaluations', 'allEvaluations'}: - raise ValueError(f'Invalid spreadsheet fields: {additionalField}') - return additionalFields - - def makeCSV(self): - ''' - Creates the CSV file - ''' - with open(self.completePath, 'w', encoding="utf-8", errors="backslashreplace") as csvfile: - self.filewriter = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) - - ## Create heading on csv ## - headers = ([ 'Term', - 'Form Status', - 'B#', - 'Student Name', - 'Student Email', - 'Position', - 'Labor Position Code', - 'Labor Position Title', - 'WLS', - 'Weekly Hours', - 'Total Contract Hours', - 'Start Date', - 'End Date', - 'Department', - 'Supervisor', - 'Supervisor Email', - 'Supervisor Notes' - ]) - if self.includeEvals: - headers.extend(['SLE Type', - 'SLE Attendance', - 'SLE Attendance Comments', - 'SLE Accountability', - 'SLE Accountability Comments', - 'SLE Teamwork', - 'SLE Teamwork Comments', - 'SLE Initiative', - 'SLE Initiative Comments', - 'SLE Respect', - 'SLE Respect Comments', - 'SLE Learning', - 'SLE Learning Comments', - 'SLE Job Specific', - 'SLE Job Specific Comments', - 'SLE Overall Score' - ]) - - if 'overloads' in self.additionalSpreadsheetFields: - headers.extend(['Student Overload Reason', - 'Financial Aid Status', - 'Financial Aid Approver', - 'Financial Aid Review Date', - 'SAAS Status', - 'SAAS Approver', - 'SAAS Review Date', - 'Labor Status', - 'Labor Approver', - 'Labor Review Date']) - - self.filewriter.writerow(headers) - - for form in self.formHistories: - row = self.addPrimaryData(form) - if self.includeEvals: - evalRows = self.addEvaluationData(form.formHistoryID) - if len(evalRows) == 0: - self.filewriter.writerow(row) - elif len(evalRows) == 1: - row.extend(evalRows[0]) - self.filewriter.writerow(row) - else: - row.extend(evalRows[0]) - self.filewriter.writerow(row) - for evaluation in evalRows[1:]: - row = [""] * 17 - row.extend(evaluation) - self.filewriter.writerow(row) - - elif 'overloads' in self.additionalSpreadsheetFields: - row = self.addOverloadData(form, row) - self.filewriter.writerow(row) - else: - self.filewriter.writerow(row) - - - def addPrimaryData(self, form): - ''' - Adds data included on every CSV - ''' - - row = [ form.formID.termCode.termName, - form.status.statusName, - form.formID.studentSupervisee.ID, - u' '.join((form.formID.studentSupervisee.FIRST_NAME, form.formID.studentSupervisee.LAST_NAME)), - form.formID.studentSupervisee.STU_EMAIL, - form.formID.jobType, - form.formID.POSN_CODE, - form.formID.POSN_TITLE, - form.formID.WLS, - form.formID.weeklyHours, - form.formID.contractHours, - form.formID.startDate, - form.formID.endDate, - form.formID.department.DEPT_NAME, - u' '.join((form.formID.supervisor.FIRST_NAME, form.formID.supervisor.LAST_NAME)), - form.formID.supervisor.EMAIL, - form.formID.supervisorNotes - ] - return row - - def addOverloadData(self, form, rowData): - - faApprover = form.overloadForm.financialAidApprover.fullName if form.overloadForm.financialAidApprover else "" - saasApprover = form.overloadForm.SAASApprover.fullName if form.overloadForm.SAASApprover else "" - laborApprover = form.overloadForm.laborApprover.fullName if form.overloadForm.laborApprover else "" - - rowData.extend([ - form.overloadForm.studentOverloadReason, - form.overloadForm.financialAidApproved_id, - faApprover, - form.overloadForm.financialAidReviewDate, - form.overloadForm.SAASApproved_id, - saasApprover, - form.overloadForm.SAASReviewDate, - form.overloadForm.laborApproved_id, - laborApprover, - form.overloadForm.laborReviewDate, - ]) - - return rowData - - - def addEvaluationData(self, formID): - ''' - Adds data for SLE - ''' - multipleRows = [] - if "finalEvaluations" in self.additionalSpreadsheetFields: - finalEvaluation = StudentLaborEvaluation.get_or_none(StudentLaborEvaluation.formHistoryID == formID, StudentLaborEvaluation.is_midyear_evaluation == 0, StudentLaborEvaluation.is_submitted == True) - if finalEvaluation: - multipleRows.append(self.insertEvaluationData(finalEvaluation, "Final")) - elif "midYearEvaluations" in self.additionalSpreadsheetFields: - midyearEvaluation = StudentLaborEvaluation.get_or_none(StudentLaborEvaluation.formHistoryID == formID, StudentLaborEvaluation.is_midyear_evaluation == 1, StudentLaborEvaluation.is_submitted == True) - if midyearEvaluation: - multipleRows.append(self.insertEvaluationData(midyearEvaluation, "Midyear")) - elif self.includeEvals == True: - anyEvaluation = StudentLaborEvaluation.select().where(StudentLaborEvaluation.formHistoryID == formID, StudentLaborEvaluation.is_submitted == True) - if anyEvaluation: - for evaluation in anyEvaluation: - multipleRows.append(self.insertEvaluationData(evaluation, "Midyear" if evaluation.is_midyear_evaluation else "Final")) - else: - return [] - - return multipleRows - - def insertEvaluationData(self, evaluation, evalType): - ''' - Helper function for self.addEvaluationData(); Adds individual row's SLE data - ''' - tableRow = [] - tableRow.extend([ evalType, - evaluation.attendance_score, - evaluation.attendance_comment, - evaluation.accountability_score, - evaluation.accountability_comment, - evaluation.teamwork_score, - evaluation.teamwork_comment, - evaluation.initiative_score, - evaluation.initiative_comment, - evaluation.respect_score, - evaluation.respect_comment, - evaluation.learning_score, - evaluation.learning_comment, - evaluation.jobSpecific_score, - evaluation.jobSpecific_comment - ]) - tableRow.append(evaluation.attendance_score + - evaluation.accountability_score + - evaluation.teamwork_score + - evaluation.initiative_score + - evaluation.respect_score + - evaluation.learning_score + - evaluation.jobSpecific_score - ) - return tableRow diff --git a/app/controllers/main_routes/laborHistory.py b/app/controllers/main_routes/laborHistory.py index 15ae8e629..c80cdf9a0 100755 --- a/app/controllers/main_routes/laborHistory.py +++ b/app/controllers/main_routes/laborHistory.py @@ -15,7 +15,7 @@ from app.models.student import Student from app.controllers.errors_routes.handlers import * from app.login_manager import require_login -from app.controllers.main_routes.download import CSVMaker +from app.logic.download import CSVMaker from app.logic.buttonStatus import ButtonStatus from app.logic.tracy import Tracy from app.models.supervisor import Supervisor @@ -57,11 +57,34 @@ def laborhistory(id): if len(authorizedForms) == 0: return render_template('errors/403.html'), 403 - authorizedForms = Term.order_by_term(list(authorizedForms.objects()), reverse=True) downloadId = saveFormSearchResult("Labor History", authorizedForms, "studentHistory") laborStatusFormList = ','.join([str(form.formID.laborStatusFormID) for form in studentForms]) + # modify status display for overload and release forms + formIds = [form.formID for form in authorizedForms] + + relatedForms = (FormHistory.select().where( + (FormHistory.formID.in_(formIds)) & + ((FormHistory.releaseForm.is_null(False)) | (FormHistory.overloadForm.is_null(False)) | (FormHistory.adjustedForm.is_null(False))) + )) + + formMap = {form.formID.laborStatusFormID: form for form in authorizedForms} + + # initialize displayStatus with each form's base status + for form in authorizedForms: + form.displayStatus = str(form.status) + # iterate once over relatedForms and update each form displayStatus + for related in relatedForms: + form = formMap.get(related.formID.laborStatusFormID) + + if related.overloadForm: + form.displayStatus = "Overload " + str(related.status) + if related.adjustedForm: + form.displayStatus = "Adjustment " + str(related.status) + if related.releaseForm: + form.displayStatus = "Release Pending" if str(related.status) == "Pending" else "Released" + return render_template('main/formHistory.html', title=('Labor History'), student = student, @@ -134,7 +157,7 @@ def populateModal(statusKey): if form.adjustedForm.fieldAdjusted == "position": # if position field has been changed in adjust form then retriev position name. newPosition = Tracy().getPositionFromCode(newValue) try: - oldPosition = form.formID.Tracy().getPositionFromCode(oldValue) + oldPosition = Tracy().getPositionFromCode(oldValue) except: oldPosition = types.SimpleNamespace(POSN_TITLE="Unknown - " + oldValue, WLS="?") @@ -148,20 +171,24 @@ def populateModal(statusKey): oldDepartment = Department.get(Department.ORG == oldValue) form.adjustedForm.newValue = newDepartment.DEPT_NAME form.adjustedForm.oldValue = oldDepartment.DEPT_NAME - # Converts the field adjusted value out of camelcase into a more readable format to be displayed on the front end + + # Convert the field adjusted value out of camelcase into a more readable format form.adjustedForm.fieldAdjusted = re.sub(r"(\w)([A-Z])", r"\1 \2", form.adjustedForm.fieldAdjusted).title() # Pending release or adjustment forms need the historyType known if (form.releaseForm != None or form.adjustedForm != None) and form.status.statusName == "Pending": pendingformType = form.historyType.historyTypeName + approveLink = f"{request.host_url}studentResponse/confirm?token={statusForm.confirmationToken}" + resp = make_response(render_template('snips/studentHistoryModal.html', forms = forms, currentUser = currentUser, statusForm = statusForm, currentDate = currentDate, pendingformType = pendingformType, - buttonState = buttonState + buttonState = buttonState, + approveLink = approveLink, )) return (resp) except Exception as e: diff --git a/app/controllers/main_routes/laborReleaseForm.py b/app/controllers/main_routes/laborReleaseForm.py index 3b2942666..bb15838f9 100755 --- a/app/controllers/main_routes/laborReleaseForm.py +++ b/app/controllers/main_routes/laborReleaseForm.py @@ -24,7 +24,6 @@ def laborReleaseForm(laborStatusKey): if not currentUser.isLaborAdmin: # Not an admin if currentUser.student and not currentUser.supervisor: return redirect('/laborHistory/' + currentUser.student.ID) - forms = LaborStatusForm.select().distinct().where(LaborStatusForm.laborStatusFormID == laborStatusKey) laborAdmins = (User.select(User, Supervisor).join(Supervisor) .where(User.isLaborAdmin == True) @@ -45,18 +44,16 @@ def laborReleaseForm(laborStatusKey): # will be able to submit a labor release form. This section will create the new # labor release form, and a new form in the form history table. laborStatusForiegnKey = LaborStatusForm.get(LaborStatusForm.laborStatusFormID == laborStatusKey) - formHistoryID = FormHistory.get(FormHistory.formID == laborStatusKey) #need formHistoryID for emailHandler datepickerDate = request.form.get("date") releaseDate = datetime.strptime(datepickerDate, "%m/%d/%Y").strftime("%Y-%m-%d") releaseReason = request.form.get("notes") releaseCondition = request.form.get("condition") releaseContactUsername = request.form.get("contactPerson") releaseContactPerson = User.get(User.username == releaseContactUsername) - - createLaborReleaseForm(currentUser, laborStatusForiegnKey, releaseDate, releaseCondition, releaseReason, releaseContactPerson) - - email = emailHandler(formHistoryID.formHistoryID) - email.laborReleaseFormSubmitted() + releaseContactFullName = releaseContactPerson.fullName + newFormHistory = createLaborReleaseForm(currentUser, laborStatusForiegnKey, releaseDate, releaseCondition, releaseReason, releaseContactPerson) + email = emailHandler(newFormHistory.formHistoryID) + email.laborReleaseFormSubmitted(releaseContactUsername, releaseContactFullName) # Once all the forms are created, the user gets redirected to the # home page and gets a flash message telling them the forms were # submiteds @@ -97,3 +94,5 @@ def createLaborReleaseForm(currentUser, laborStatusForiegnKey, releaseDate, rele status = status.statusName, rejectReason = None ) + return newFormHistory + diff --git a/app/controllers/main_routes/laborStatusForm.py b/app/controllers/main_routes/laborStatusForm.py index a12a214cd..dc794952d 100755 --- a/app/controllers/main_routes/laborStatusForm.py +++ b/app/controllers/main_routes/laborStatusForm.py @@ -20,7 +20,9 @@ from app.models.supervisor import Supervisor from app.logic.tracy import Tracy from app.controllers.main_routes.laborReleaseForm import createLaborReleaseForm -from app.controllers.admin_routes.allPendingForms import saveStatus +from app.logic.allPendingForms import saveStatus +from app.logic.statusFormFunctions import * + @main_bp.route('/laborstatusform', methods=['GET']) @main_bp.route('/laborstatusform/', methods=['GET']) @@ -153,7 +155,7 @@ def checkTotalHours(termCode, student, hours): .where(((FormHistory.formID.termCode == termCode) | (FormHistory.formID.termCode == ayTermCode)), FormHistory.formID.studentSupervisee == student, FormHistory.historyType == "Labor Status Form", - ((FormHistory.status == "Approved") | (FormHistory.status == "Approved Reluctantly") | (FormHistory.status == "Pending")) + ((FormHistory.status == "Approved") | (FormHistory.status == "Pending")) ) term = Term.get(Term.termCode == termCode) totalHours = 0 diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index cbbe1dd0d..b3af06c9e 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -1,35 +1,31 @@ -import operator -from flask import render_template, request, json, jsonify, redirect, url_for, send_file, g, make_response +from flask import render_template, request, json, redirect, url_for, send_file, g, flash, jsonify +from peewee import JOIN from functools import reduce -from peewee import fn, Case -import uuid -from datetime import datetime, date -from app.models.term import Term +import operator from app.models.department import Department from app.models.supervisor import Supervisor from app.models.supervisorDepartment import SupervisorDepartment from app.models.student import Student from app.models.laborStatusForm import LaborStatusForm from app.models.formHistory import FormHistory -from app.models.user import User -from app.models.studentLaborEvaluation import StudentLaborEvaluation +from app.models.term import Term from app.controllers.admin_routes.allPendingForms import checkAdjustment from app.controllers.main_routes import main_bp -from app.controllers.main_routes.download import CSVMaker -from app.logic.search import getDepartmentsForSupervisor +from app.logic.download import CSVMaker, saveFormSearchResult, retrieveFormSearchResult +from app.logic.search import getDepartmentsForSupervisor, searchPerson, searchSupervisorPortal from app.login_manager import require_login, logout -from app.logic.download import saveFormSearchResult, retrieveFormSearchResult - +from app.logic.getTableData import getDatatableData +from app.logic.banner import Banner @main_bp.route('/logout', methods=['GET']) -def logout(): +def triggerLogout(): return redirect(logout()) @main_bp.route('/', methods=['GET', 'POST']) def supervisorPortal(): ''' When the request is GET the function populates the General Search interface dropdown menus with their corresponding values. - If the request is POST it also populates the datatable with data based on user input. + If the request is POST it also populates the datatable with data based on S input. ''' currentUser = require_login() if not currentUser or not currentUser.supervisor: @@ -38,286 +34,19 @@ def supervisorPortal(): return render_template('errors/403.html'), 403 - terms = LaborStatusForm.select(LaborStatusForm.termCode).distinct().order_by(LaborStatusForm.termCode.desc()) - allSupervisors = Supervisor.select() - supervisorFirstName = fn.COALESCE(Supervisor.preferred_name, Supervisor.legal_name) - studentFirstName = fn.COALESCE(Student.preferred_name, Student.legal_name) - department = None + if request.method == 'POST': + return getDatatableData(request) + if currentUser.isLaborAdmin or currentUser.isFinancialAidAdmin or currentUser.isSaasAdmin: departments = list(Department.select().order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) - supervisors = (Supervisor.select(Supervisor, supervisorFirstName) - .order_by(Supervisor.isActive.desc(), supervisorFirstName.contains("Unknown"), supervisorFirstName, Supervisor.LAST_NAME)) - students = (Student.select(Student, studentFirstName) - .order_by(studentFirstName.contains("Unknown"), studentFirstName, Student.LAST_NAME)) - else: departments = list(getDepartmentsForSupervisor(currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) - deptNames = [department.DEPT_NAME for department in departments] - - supervisorPrimaryDepartment = Department.select().join(SupervisorDepartment) # count up all forms for a supervisor in department and get the max - - supervisors = (Supervisor.select(Supervisor, supervisorFirstName) - .join_from(Supervisor, LaborStatusForm) - .join_from(LaborStatusForm, Department) - .where(Department.DEPT_NAME.in_(deptNames)) - .distinct() - .order_by(Supervisor.isActive.desc(), supervisorFirstName.contains("Unknown"), supervisorFirstName, Supervisor.LAST_NAME)) - - students = (Student.select(Student, studentFirstName) - .join_from(Student, LaborStatusForm) - .join_from(LaborStatusForm, Department) - .where(Department.DEPT_NAME.in_(deptNames)) - .order_by(studentFirstName.contains("Unknown"), studentFirstName, Student.LAST_NAME) - .distinct()) - if request.method == 'POST': - return getDatatableData(request) return render_template('main/supervisorPortal.html', - terms = terms, - supervisors = supervisors, - allSupervisors = allSupervisors, - students = students, departments = departments, - department = department, currentUser = currentUser ) -def getDatatableData(request): - ''' - This function runs a query based on selected options in the front-end and retrieves the appropriate forms. - Then, it puts all the retrieved data in appropriate form to be send to the ajax call in the supervisorPortal.js file. - ''' - # 'draw', 'start', 'length', 'order[0][column]', 'order[0][dir]' are built-in parameters, i.e., - # they are implicitly passed as part of the AJAX request when using datatable server-side processing - - sleJoin = "" - currentUser = require_login() - draw = int(request.form.get('draw', 0)) - rowNumber = int(request.form.get('start', 0)) - rowsPerPage = int(request.form.get('length', 25)) - queryFilterData = request.form.get('data') - queryFilterDict = json.loads(queryFilterData) - sortBy = queryFilterDict.get('sortBy', "term") - if sortBy == "": - sortBy = "term" - order = queryFilterDict.get('order', "ASC") - - termCode = queryFilterDict.get('termCode', "") - if termCode == "currentTerm": - termCode = g.openTerm - elif termCode == "activeTerms": - termCode = list(Term.select(Term.termCode).where(Term.termEnd >= datetime.now())) - departmentId = queryFilterDict.get('departmentID', "") - supervisorId = queryFilterDict.get('supervisorID', "") - if supervisorId == "currentUser": - supervisorId = g.currentUser.supervisor - studentId = queryFilterDict.get('studentID', "") - formStatusList = queryFilterDict.get('formStatus', "") # form status radios - formTypeList = queryFilterDict.get('formType', "") # form type radios - evaluationStatus = queryFilterDict.get('evaluations', "") # evaluation radios - - fieldValueMap = {Term.termCode: termCode, - Department.departmentID: departmentId, - Student.ID: studentId, - Supervisor.ID: supervisorId, - FormHistory.status: formStatusList, - FormHistory.historyType: formTypeList, - StudentLaborEvaluation.ID: evaluationStatus} - clauses = [] - # WHERE clause conditions are dynamically generated using model fields and selectpicker values - for field, value in fieldValueMap.items(): - if value != "" and value: - if type(value) is list: - clauses.append(field.in_(value)) - elif field is StudentLaborEvaluation.ID: - sleJoin=value[0] - else: - clauses.append(field == value) - # This expression creates SQL AND operator between the conditions added to 'clauses' list - formSearchResults = (FormHistory.select() - .join(LaborStatusForm, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) - .join(Department, on=(LaborStatusForm.department == Department.departmentID)) - .join(Supervisor, on=(LaborStatusForm.supervisor == Supervisor.ID)) - .join(Student, on=(LaborStatusForm.studentSupervisee == Student.ID)) - .join(Term, on=(LaborStatusForm.termCode == Term.termCode)) - .join(User, on=(FormHistory.createdBy == User.userID))) - if clauses: - formSearchResults = formSearchResults.where(reduce(operator.and_, clauses)) - if not currentUser.isLaborAdmin: - supervisorDepartments = [d.departmentID for d in getDepartmentsForSupervisor(currentUser)] - formSearchResults = formSearchResults.where(FormHistory.formID.department.in_(supervisorDepartments)) - recordsTotal = len(formSearchResults) - - # this checks and finds the first value that is not null of preferred_name, legal_name and last_name. - # including last_name is necessary because there are like 4 cases where someone has no first name or last name, instead their full name is - # stored in last_name - supervisorFirstNameCase = fn.COALESCE(fn.NULLIF(Supervisor.preferred_name, ''), fn.NULLIF(Supervisor.legal_name, ''), Supervisor.LAST_NAME) - studentFirstNameCase = fn.COALESCE(fn.NULLIF(Student.preferred_name, ''), fn.NULLIF(Student.legal_name, ''), Student.LAST_NAME) - - # this maps all of the values we expect to receive from the sorting dropdowns in the frontend - # to actual peewee objects we can sort by later - # the casing is weird because the columns that don't have any fields are are not capitalized - sortValueColumnMap = { - "term": Term.termCode, - "department": Department.DEPT_NAME, - "supervisorFirstName": supervisorFirstNameCase, - "supervisorLastName": Supervisor.LAST_NAME, - "studentFirstName": studentFirstNameCase, - "studentLastName": Student.LAST_NAME, - "positionWLS": LaborStatusForm.WLS, - "positionTitle": LaborStatusForm.POSN_TITLE, - "positionType": LaborStatusForm.jobType, - "length": LaborStatusForm.startDate, - "createdBy": User.username, - "formStatus": FormHistory.status, - "formType": FormHistory.historyType, - } - - if order == "DESC": - filteredSearchResults = formSearchResults.order_by(fn.TRIM(sortValueColumnMap[sortBy]).desc()).limit(rowsPerPage).offset(rowNumber) - else: - filteredSearchResults = formSearchResults.order_by(fn.TRIM(sortValueColumnMap[sortBy]).asc()).limit(rowsPerPage).offset(rowNumber) - formattedData = getFormattedData(filteredSearchResults, queryFilterDict.get('view')) - - downloadId = saveFormSearchResult("Form Search", formSearchResults, "LSF Search") - formsDict = {"draw": draw, "recordsTotal": recordsTotal, "recordsFiltered": recordsTotal, "data": formattedData, "downloadId": downloadId} - - return make_response(jsonify(formsDict)) - -def getFormattedData(filteredSearchResults, view ='simple'): - ''' - Putting the data in the correct format to be used by the JS file. - Because this implementation is using server-side processing of datatables, - the HTML for the datatables are also formatted here. - ''' - if view == "simple": - formattedData = {} - todaysDate = date.today() - filteredSearchResults.order_by(FormHistory.formID.startDate.desc()) - isMostCurrent = False - for form in filteredSearchResults: - startDate = form.formID.startDate - endDate = form.formID.endDate - bNumber = form.formID.studentSupervisee.ID - if bNumber not in formattedData: - absentInFormatting = True - else: - absentInFormatting = False - isMostCurrent = (startDate > formattedData[bNumber][1]) or (startDate <= todaysDate <= endDate) - if absentInFormatting or isMostCurrent: - - # html fields - firstName, lastName = form.formID.studentSupervisee.FIRST_NAME, form.formID.studentSupervisee.LAST_NAME - term = form.formID.termCode.termName - positionTitle = form.formID.POSN_TITLE - jobType = form.formID.jobType - departmentName = form.formID.department.DEPT_NAME - statusFormId = form.formID.laborStatusFormID - - html = f""" - - {firstName} {lastName} ({bNumber}) - - {form.status} -
- - {term} - {positionTitle} ({jobType}) - {departmentName} - - """ - - formattedData[bNumber] = (html, startDate, endDate) - - formattedDataList = [[value] for value, _, _ in formattedData.values()] - - return formattedDataList - - supervisorHTML = '{} ' - studentHTML = '
{}
{}
' - departmentHTML = ' {}' - positionHTML = ' {}' - formTypeStatus = ' {}' - formattedData = [] - for form in filteredSearchResults: - # The order in which you append the items to 'record' matters and it should match the order of columns on the table! - record = [] - # Term - record.append(form.formID.termCode.termName) - # Student - record.append(studentHTML.format( - form.formID.studentSupervisee.ID, - f'{form.formID.studentSupervisee.preferred_name if form.formID.studentSupervisee.preferred_name else form.formID.studentSupervisee.legal_name} {form.formID.studentSupervisee.LAST_NAME}', - form.formID.studentSupervisee.ID, - form.formID.studentSupervisee.STU_EMAIL)) - # Supervisor - supervisorField = supervisorHTML.format( - form.formID.supervisor.ID, - f'{form.formID.supervisor.preferred_name if form.formID.supervisor.preferred_name else form.formID.supervisor.legal_name } {form.formID.supervisor.LAST_NAME}', - form.formID.supervisor.EMAIL) - record.append(supervisorField) - - # Department - record.append(departmentHTML.format( - form.formID.department.ORG, - form.formID.department.ACCOUNT, - form.formID.department.DEPT_NAME)) - - # Position - positionField = positionHTML.format( - form.formID.jobType, - f'{form.formID.jobType} ({form.formID.WLS})') - # Hours - hoursField = form.formID.weeklyHours if form.formID.weeklyHours else form.formID.contractHours - # Adjustment Form Specific Data - checkAdjustment(form) - if (form.adjustedForm): - if form.adjustedForm.fieldAdjusted == "supervisor": - newSupervisor = supervisorHTML.format( - form.adjustedForm.oldValue['ID'], - form.adjustedForm.newValue, - form.adjustedForm.oldValue['email']) - supervisorField = f'{supervisorField}
{newSupervisor}' - - if form.adjustedForm.fieldAdjusted == "position": - newPosition = positionHTML.format( - form.adjustedForm.oldValue, - form.adjustedForm.newValue) - positionField = f'{positionField}
{newPosition}' - - if form.adjustedForm.fieldAdjusted == "weeklyHours" or form.adjustedForm.fieldAdjusted == "contractHours": - newHours = form.adjustedForm.newValue - hoursField = f'{hoursField}
{newHours}' - - - - - record.append(f'{form.formID.POSN_TITLE}
{positionField}') - record.append(hoursField) - # Contract Dates - record.append("
".join([form.formID.startDate.strftime('%m/%d/%y'), - form.formID.endDate.strftime('%m/%d/%y')])) - # Created By - record.append(supervisorHTML.format( - form.createdBy.supervisor.ID if form.createdBy.supervisor else form.createdBy.student.ID, - form.createdBy.username, - form.createdBy.email, - form.createdDate.strftime('%m/%d/%y'))) - # Form Type - formTypeNameMapping = { - "Labor Status Form": "Original", - "Labor Adjustment Form": "Adjusted", - "Labor Overload Form": "Overload", - "Labor Release Form": "Release"} - originalFormTypeName = form.historyType.historyTypeName - mappedFormTypeName = formTypeNameMapping[originalFormTypeName] - # formType(Status) - formTypeStatusField = record.append(formTypeStatus.format(f'{mappedFormTypeName} ({form.status.statusName})')) - - # Evaluation status - # TODO: Skipping adding to the table. Requires database work to get SLE out from form (formHistory, to be precise) - - formattedData.append(record) - return formattedData - @main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form @@ -354,3 +83,34 @@ def downloadSupervisorPortalResults(): includeEvals=False ) return send_file(excel.relativePath, as_attachment=True, attachment_filename=excel.relativePath.split('/').pop()) + +@main_bp.route('/supervisorPortal/liveSearch', methods=['GET']) +def SupervisorPortalSearch(): + """ + Returns a list of users that match a given string + """ + searchType = request.args.get("searchType") + userInput = request.args.get("userInput") + + if not searchType or not userInput: + return jsonify({}), 400 + currentUser = require_login() + userList = searchSupervisorPortal(currentUser, searchType, userInput) + return jsonify(userList) + +@main_bp.route('/lsf//submitToBanner', methods=['GET']) +def submitToBanner(formHistoryId): + if not (g.currentUser.isLaborAdmin or g.currentUser.isLaborDepartmentStudent): + return render_template('errors/403.html'), 403 + + try: + conn = Banner() + save_form_status = conn.insert(formHistoryId) + except Exception as e: + save_form_status = False + print(f"Error saving form history ({formHistoryId}) to Banner.") + + if save_form_status: + return "Form successfully submitted to Banner.", 200 + else: + return "Submitting to Banner failed.", 500 \ No newline at end of file diff --git a/app/controllers/main_routes/studentOverloadApp.py b/app/controllers/main_routes/studentOverloadApp.py index 941a248ae..683d95e54 100755 --- a/app/controllers/main_routes/studentOverloadApp.py +++ b/app/controllers/main_routes/studentOverloadApp.py @@ -1,6 +1,6 @@ from datetime import date, datetime from playhouse.shortcuts import model_to_dict -from flask import json, jsonify, request, redirect, url_for, abort, flash +from flask import json, jsonify, request, redirect, url_for, abort, flash, g from app.controllers.main_routes import * from app.logic.emailHandler import* @@ -62,7 +62,7 @@ def studentOverloadApp(formHistoryId): studentPrimaryHistory = (FormHistory.select().where( FormHistory.formID == primaryForm, FormHistory.historyType == "Labor Status Form", - FormHistory.status.in_(["Approved","Approved Reluctantly","Pending","Pre-Student Approval"]) )) + FormHistory.status.in_(["Approved","Pending","Pre-Student Approval"]) )) formIDPrimary.append(studentPrimaryHistory) formIDSecondary = [] @@ -70,7 +70,7 @@ def studentOverloadApp(formHistoryId): studentSecondaryHistory = (FormHistory.select().where( FormHistory.formID == secondaryForm, FormHistory.historyType == "Labor Status Form", - FormHistory.status.in_(["Approved","Approved Reluctantly","Pending","Pre-Student Approval"]) )) + FormHistory.status.in_(["Approved","Pending","Pre-Student Approval"]) )) formIDSecondary.append(studentSecondaryHistory) totalCurrentHours = 0 @@ -159,10 +159,8 @@ def updateDatabase(overloadFormHistoryID): link = makeThirdPartyLink("Financial Aid", request.host, overloadFormHistory.formHistoryID) email.overloadVerification("Financial Aid", link) - flash("Overload Request Submitted", "success") - - return "" + return g.currentUser.student.ID except Exception as e: print("ERROR: " + str(e)) diff --git a/app/logic/adminManagement.py b/app/logic/adminManagement.py new file mode 100644 index 000000000..643fd7903 --- /dev/null +++ b/app/logic/adminManagement.py @@ -0,0 +1,84 @@ +from peewee import DoesNotExist +from app.models.user import User +from app.logic.tracy import Tracy +from flask import request, flash +from app.logic.userInsertFunctions import createStudentFromTracy, createSupervisorFromTracy, createUser + + +def searchForAdmin(rsp): + userInput = rsp[1] + adminType = rsp[0] + userList = [] + if adminType == "addlaborAdmin": + tracyStudents = Tracy().getStudentsFromUserInput(userInput) + students = [] + for student in tracyStudents: + try: + existingUser = User.get(User.student == student.ID) + if existingUser.isLaborAdmin: + pass + else: + students.append(student) + except DoesNotExist as e: + students.append(student) + for student in students: + username = student.STU_EMAIL.split('@', 1) + userList.append({'username': username[0], + 'firstName': student.FIRST_NAME, + 'lastName': student.LAST_NAME, + 'type': 'Student' + }) + tracySupervisors = Tracy().getSupervisorsFromUserInput(userInput) + supervisors = [] + for supervisor in tracySupervisors: + try: + existingUser = User.get(User.supervisor == supervisor.ID) + if ((existingUser.isLaborAdmin and adminType == "addlaborAdmin") + or (existingUser.isSaasAdmin and adminType == "addSaasAdmin") + or (existingUser.isFinancialAidAdmin and adminType == "addFinAidAdmin")): + pass + else: + supervisors.append(supervisor) + except DoesNotExist as e: + supervisors.append(supervisor) + for sup in supervisors: + username = sup.EMAIL.split('@', 1) + userList.append({'username': username[0], + 'firstName': sup.FIRST_NAME, + 'lastName': sup.LAST_NAME, + 'type': 'Supervisor'}) + return userList + +def getUser(selectpickerID): + username = request.form.get(selectpickerID) + try: + user = User.get(User.username == username) + except DoesNotExist as e: + usertype = Tracy().checkStudentOrSupervisor(username) + supervisor = student = None + if usertype == "Student": + student = createStudentFromTracy(username) + else: + supervisor = createSupervisorFromTracy(username) + user = createUser(username, student=student, supervisor=supervisor) + return user + + + +def addAdmin(newAdmin, adminType): + if adminType == 'labor': + newAdmin.isLaborAdmin = True + if adminType == 'finAid': + newAdmin.isFinancialAidAdmin = True + if adminType == 'saas': + newAdmin.isSaasAdmin = True + newAdmin.save() + +def removeAdmin(oldAdmin, adminType): + if adminType == 'labor': + oldAdmin.isLaborAdmin = False + if adminType == 'finAid': + oldAdmin.isFinancialAidAdmin = False + if adminType == 'saas': + oldAdmin.isSaasAdmin = False + oldAdmin.save() diff --git a/app/logic/allPendingForms.py b/app/logic/allPendingForms.py new file mode 100644 index 000000000..60110d5ef --- /dev/null +++ b/app/logic/allPendingForms.py @@ -0,0 +1,288 @@ +import json +from datetime import date +from flask import jsonify +from app.models.formHistory import FormHistory +from app.models.status import Status +from app.logic.banner import Banner +from app.logic.emailHandler import emailHandler +from app.login_manager import require_login +from app.models.laborStatusForm import LaborStatusForm +from app.models.supervisor import Supervisor +from app.models.department import Department +from app.logic.userInsertFunctions import createSupervisorFromTracy +from app.logic.tracy import Tracy +from app.models.overloadForm import OverloadForm +from app.models.notes import Notes +from app.login_manager import DoesNotExist, render_template + + +def saveStatus(new_status, formHistoryIds, currentUser): + try: + if new_status == 'Denied by Admin': + # Index 1 will always hold the reject reason in the list, so we can + # set a variable equal to the index value and then slice off the list + # item before the iteration + denyReason = formHistoryIds[1] + formHistoryIds = formHistoryIds[:1] + + for formHistoryID in formHistoryIds: + formHistory = FormHistory.get(FormHistory.formHistoryID == formHistoryID) + formHistory.status = Status.get(Status.statusName == new_status) + + formHistory.reviewedDate = date.today() + formHistory.reviewedBy = currentUser + + formType = formHistory.historyType_id + + # Add a note if we've skipped to Pending + if new_status == 'Pending': + note = "Skipped Student Approval" + Notes.create(formID=formHistory.formID, + createdBy=currentUser, + date=date.today(), + notesContents=note, + noteType = "Labor Note") + + + # Add to BANNER + save_status = True # default true so that we will still save in other cases + + if new_status == 'Approved' and formType == "Labor Status Form" and formHistory.formID.POSN_CODE != "S12345": # don't update banner for Adjustment forms or for CS dummy position + if formHistory.formID.POSN_CODE == "SNOLAB": + formHistory.formID.weeklyHours = 10 + conn = Banner() + save_status = conn.insert(formHistory) + + # if we are able to save + if save_status: + + if new_status == 'Denied by Admin': + formHistory.rejectReason = denyReason + formHistory.save() + + # Send necessary emails from the status change + email = emailHandler(formHistory.formHistoryID) + if new_status == "Denied by Admin" and formType == "Labor Status Form": + email.laborStatusFormRejected() + if new_status == "Approved" and formType == "Labor Status Form": + email.laborStatusFormApproved() + if new_status == "Approved" and formType == "Labor Adjustment Form": + # This function is triggered whenever an adjustment form is approved. + # The following function overrides the original data in lsf with the new data from adjustment form. + LSF = LaborStatusForm.get_by_id(formHistory.formID) + overrideOriginalStatusFormOnAdjustmentFormApproval(formHistory, LSF) + + else: + print("Unable to update form status for formHistoryID {}.".format(id)) + return jsonify({"success": False}), 500 + + except Exception as e: + print("Error preparing form for status update:", e) + return jsonify({"success": False}), 500 + + return jsonify({"success": True}) + +def overrideOriginalStatusFormOnAdjustmentFormApproval(form, LSF): + """ + This function checks whether an Adjustment Form is approved. If yes, it overrides the information + in the original Labor Status Form with the new information coming from approved Adjustment Form. + + The only fields that will ever be changed in an adjustment form are: supervisor, department, position, and hours. + """ + currentUser = require_login() + if not currentUser: # Not logged in + return render_template('errors/403.html'), 403 + if form.adjustedForm.fieldAdjusted == "supervisor": + d, created = Supervisor.get_or_create(ID = form.adjustedForm.newValue) + if not created: + LSF.supervisor = d.ID + LSF.save() + if created: + tracyUser = Tracy().getSupervisorFromID(form.adjustedForm.newValue) + tracyEmail = tracyUser.EMAIL + tracyUsername = tracyEmail.find('@') + createSupervisorFromTracy(tracyUsername) + + if form.adjustedForm.fieldAdjusted == "position": + LSF.POSN_CODE = form.adjustedForm.newValue + position = Tracy().getPositionFromCode(form.adjustedForm.newValue) + LSF.POSN_TITLE = position.POSN_TITLE + LSF.WLS = position.WLS + LSF.save() + + if form.adjustedForm.fieldAdjusted == "department": + department = Department.get(Department.ORG==form.adjustedForm.newValue) + LSF.department = department.departmentID + LSF.save() + + if form.adjustedForm.fieldAdjusted == "contractHours": + LSF.contractHours = int(form.adjustedForm.newValue) + LSF.save() + + if form.adjustedForm.fieldAdjusted == "weeklyHours": + LSF.weeklyHours = int(form.adjustedForm.newValue) + LSF.save() + + +def laborAdminOverloadApproval(rsp, historyForm, status, currentUser, currentDate, email): + if rsp['formType'] == 'Overload': + overloadForm = OverloadForm.get(OverloadForm.overloadFormID == historyForm.overloadForm.overloadFormID) + overloadForm.laborApproved = status + overloadForm.laborApprover = currentUser + overloadForm.laborReviewDate = currentDate + overloadForm.save() + try: + pendingForm = FormHistory.select().where((FormHistory.formID == historyForm.formID) & (FormHistory.status == "Pending") & (FormHistory.historyType != "Labor Overload Form")).get() + if historyForm.adjustedForm and rsp['status'] == "Approved": + LSF = LaborStatusForm.get(LaborStatusForm.laborStatusFormID == historyForm.formID) + if historyForm.adjustedForm.fieldAdjusted == "weeklyHours": + LSF.weeklyHours = pendingForm.adjustedForm.newValue + LSF.save() + if pendingForm.historyType.historyTypeName == "Labor Status Form" or (pendingForm.historyType.historyTypeName == "Labor Adjustment Form" and pendingForm.adjustedForm.fieldAdjusted == "weeklyHours"): + pendingForm.status = status + pendingForm.reviewedBy = currentUser + pendingForm.reviewedDate = currentDate + if 'denialReason' in rsp.keys(): + pendingForm.rejectReason = rsp['denialReason'] + Notes.create(formID = pendingForm.formID.laborStatusFormID, + createdBy = currentUser, + date = currentDate, + notesContents = rsp['denialReason'], + noteType = "Labor Note") + pendingForm.save() + + if pendingForm.historyType.historyTypeName == "Labor Status Form": + email = emailHandler(pendingForm.formHistoryID) + if rsp['status'] == 'Approved': + email.laborStatusFormApproved() + elif rsp['status'] == 'Denied by Admin': + email.laborStatusFormRejected() + except DoesNotExist: + pass + except Exception as e: + print(e) + if 'denialReason' in rsp.keys(): + # We only update the reject reason if one was given on the UI + historyForm.rejectReason = rsp['denialReason'] + historyForm.save() + Notes.create(formID = historyForm.formID.laborStatusFormID, + createdBy = currentUser, + date = currentDate, + notesContents = rsp['denialReason'], + noteType = "Labor Note") + if 'adminNotes' in rsp.keys(): + # We only add admin notes if there was a note made on the UI + Notes.create(formID = historyForm.formID.laborStatusFormID, + createdBy = currentUser, + date = currentDate, + notesContents = rsp['adminNotes'], + noteType = "Labor Note") + historyForm.status = status.statusName + historyForm.reviewedBy = currentUser + historyForm.reviewedDate = currentDate + historyForm.save() + if rsp['formType'] == 'Overload': + if rsp['status'] == 'Approved': + email.LaborOverLoadFormApproved() + elif rsp['status'] == 'Denied by Admin': + email.LaborOverLoadFormRejected() + elif rsp['formType'] == 'Release': + if rsp['status'] == 'Approved': + email.laborReleaseFormApproved() + elif rsp['status'] == 'Denied by Admin': + email.laborReleaseFormRejected() + return jsonify({"Success": True}) + + +# extract data from the database to populate pending form approval modal +def modal_approval_and_denial_data(formHistoryIdList): + ''' This method grabs the data that populated the on approve modal for lsf''' + + details_list = [] + for fhID in formHistoryIdList: + formHistory = FormHistory.get(FormHistory.formHistoryID == fhID) + lsf = formHistory.formID + + studentName = f"{lsf.studentSupervisee.FIRST_NAME} {lsf.studentSupervisee.LAST_NAME}" + position = lsf.POSN_TITLE + supervisorName = f"{lsf.supervisor.FIRST_NAME} {lsf.supervisor.LAST_NAME}" + weeklyHours = lsf.weeklyHours + contractHours = lsf.contractHours + deptName = lsf.department.DEPT_NAME + + if formHistory.adjustedForm: + match formHistory.adjustedForm.fieldAdjusted: + case "position": + position = Tracy().getPositionFromCode(formHistory.adjustedForm.newValue) + position = position.POSN_TITLE + case "supervisor": + supervisor = Supervisor.get(Supervisor.ID == formHistory.adjustedForm.newValue) + supervisorName = f"{supervisor.FIRST_NAME} {supervisor.LAST_NAME}" + case "weeklyHours": + weeklyHours = formHistory.adjustedForm.newValue + case "contractHours": + contractHours = formHistory.adjustedForm.newValue + case "department": + deptName = Department.get(Department.ORG==formHistory.adjustedForm.newValue).DEPT_NAME + + details_list.append([studentName, deptName, position, str(weeklyHours),str(contractHours), supervisorName]) + + return details_list + + +def financialAidSAASOverloadApproval(historyForm, rsp, status, currentUser, currentDate): + selectedOverload = OverloadForm.get(OverloadForm.overloadFormID == historyForm.overloadForm.overloadFormID) + if 'denialReason' in rsp.keys(): + newNoteEntry = Notes.create(formID=historyForm.formID.laborStatusFormID, + createdBy=currentUser, + date=currentDate, + notesContents=rsp["denialReason"], + noteType = "Labor Note") + newNoteEntry.save() + ## Updating the overloadform TableS + if currentUser.isFinancialAidAdmin: + selectedOverload.financialAidApproved = status.statusName + selectedOverload.financialAidApprover = currentUser + selectedOverload.financialAidInitials = rsp['initials'] + selectedOverload.financialAidReviewDate = currentDate + + elif currentUser.isSaasAdmin: + selectedOverload.SAASApproved = status.statusName + selectedOverload.SAASApprover = currentUser + selectedOverload.SAASInitials = rsp['initials'] + selectedOverload.SAASReviewDate = currentDate + selectedOverload.save() + return jsonify({"Success": True}) + + +def checkAdjustment(allForms): + """ + Retrieve `supervisor` and position information for adjusted forms using the new values + stored in adjusted table and update allForms + """ + if allForms.adjustedForm: + + if allForms.adjustedForm.fieldAdjusted == "supervisor": + # use the supervisor id in the field adjusted to find supervisor in User table. + newSupervisorID = allForms.adjustedForm.newValue + newSupervisor = Supervisor.get(Supervisor.ID == newSupervisorID) + if not newSupervisor: + newSupervisor = createSupervisorFromTracy(bnumber=newSupervisorID) + + # we are temporarily storing the supervisor name in new value, + # because we want to show the supervisor name in the hmtl template. + allForms.adjustedForm.newValue = newSupervisor.FIRST_NAME +" "+ newSupervisor.LAST_NAME + allForms.adjustedForm.oldValue = {"email":newSupervisor.EMAIL, "ID":newSupervisor.ID} + + if allForms.adjustedForm.fieldAdjusted == "position": + newPositionCode = allForms.adjustedForm.newValue + newPosition = Tracy().getPositionFromCode(newPositionCode) + # temporarily storing the position code and wls in new value, and position name in old value + # because we want to show these information in the hmtl template. + allForms.adjustedForm.newValue = newPosition.POSN_CODE +" (" + newPosition.WLS+")" + allForms.adjustedForm.oldValue = newPosition.POSN_TITLE + + if allForms.adjustedForm.fieldAdjusted == "department": + newDepartment = Department.get(Department.ORG==allForms.adjustedForm.newValue) + allForms.adjustedForm.newValue = newDepartment.DEPT_NAME + allForms.adjustedForm.oldValue = newDepartment.ORG + "-" + newDepartment.ACCOUNT diff --git a/app/logic/alterLSF.py b/app/logic/alterLSF.py new file mode 100644 index 000000000..d77071624 --- /dev/null +++ b/app/logic/alterLSF.py @@ -0,0 +1,87 @@ +from datetime import date, datetime +from app.models.notes import Notes +from app.models.department import Department +from app.models.adjustedForm import AdjustedForm +from app.controllers.main_routes.laborHistory import * +from app.logic.emailHandler import * +from app.logic.utils import makeThirdPartyLink +from app.logic.userInsertFunctions import createSupervisorFromTracy +from app.logic.tracy import Tracy +from app.logic.statusFormFunctions import createOverloadForm + + +def modifyLSF(fieldsChanged, fieldName, lsf, currentUser, host=None): + if fieldName == "supervisorNotes": + noteEntry = Notes.create(formID = lsf.laborStatusFormID, + createdBy = currentUser, + date = datetime.now().strftime("%Y-%m-%d"), + notesContents = fieldsChanged[fieldName]["newValue"], + noteType = "Supervisor Note") + noteEntry.save() + lsf.supervisorNotes = noteEntry.notesContents + lsf.save() + if fieldName == "supervisor": + supervisor = createSupervisorFromTracy(bnumber=fieldsChanged[fieldName]["newValue"]) + lsf.supervisor = supervisor.ID + lsf.save() + + if fieldName == "department": + department = Department.get(Department.ORG==fieldsChanged[fieldName]['newValue']) + lsf.department = department.departmentID + lsf.save() + + if fieldName == "position": + position = Tracy().getPositionFromCode(fieldsChanged[fieldName]["newValue"]) + lsf.POSN_CODE = position.POSN_CODE + lsf.POSN_TITLE = position.POSN_TITLE + lsf.WLS = position.WLS + lsf.save() + + if fieldName == "weeklyHours": + newWeeklyHours = int(fieldsChanged[fieldName]['newValue']) + createOverloadForm(newWeeklyHours, lsf, currentUser, host=host) + lsf.weeklyHours = newWeeklyHours + lsf.save() + + if fieldName == "contractHours": + lsf.contractHours = int(fieldsChanged[fieldName]["newValue"]) + lsf.save() + + if fieldName == "startDate": + lsf.startDate = datetime.strptime(fieldsChanged[fieldName]["newValue"], "%m/%d/%Y").strftime('%Y-%m-%d') + lsf.save() + + if fieldName == "endDate": + lsf.endDate = datetime.strptime(fieldsChanged[fieldName]["newValue"], "%m/%d/%Y").strftime('%Y-%m-%d') + lsf.save() + + +def adjustLSF(fieldsChanged, fieldName, lsf, currentUser, host=None): + if fieldName == "supervisorNotes": + newNoteEntry = Notes.create(formID = lsf.laborStatusFormID, + createdBy = currentUser, + date = datetime.now().strftime("%Y-%m-%d"), + notesContents = fieldsChanged[fieldName]["newValue"], + noteType = "Supervisor Note") + newNoteEntry.save() + return None + else: + adjustedforms = AdjustedForm.create(fieldAdjusted = fieldName, + oldValue = fieldsChanged[fieldName]["oldValue"], + newValue = fieldsChanged[fieldName]["newValue"], + effectiveDate = datetime.strptime(fieldsChanged[fieldName]["date"], "%m/%d/%Y").strftime("%Y-%m-%d")) + historyType = HistoryType.get(HistoryType.historyTypeName == "Labor Adjustment Form") + status = Status.get(Status.statusName == "Pending") + adjustedFormHistory = FormHistory.create(formID = lsf.laborStatusFormID, + historyType = historyType.historyTypeName, + adjustedForm = adjustedforms.adjustedFormID, + createdBy = currentUser, + createdDate = date.today(), + status = status.statusName) + + if fieldName == "weeklyHours": + newWeeklyHours = int(fieldsChanged[fieldName]['newValue']) + createOverloadForm(newWeeklyHours, lsf, currentUser, adjustedforms.adjustedFormID, adjustedFormHistory,host=host) + + return adjustedFormHistory.formHistoryID + diff --git a/app/logic/buttonStatus.py b/app/logic/buttonStatus.py index de1be6ed2..a596c7ab3 100644 --- a/app/logic/buttonStatus.py +++ b/app/logic/buttonStatus.py @@ -1,7 +1,6 @@ from enum import Enum from datetime import date from app.logic.search import getDepartmentsForSupervisor -from app.models.studentLaborEvaluation import StudentLaborEvaluation from app.models.formHistory import FormHistory class ButtonStatus: @@ -22,6 +21,9 @@ def __init__(self): self.correction = False self.evaluate = False self.evaluation_exists = False + self.approve = False + self.studentApprove = False + self.resubmit = False self.num_buttons = 0 def get_history_form_from_lsf(self, historyForm): @@ -36,31 +38,6 @@ def get_history_form_from_lsf(self, historyForm): ''' return FormHistory.get(FormHistory.formID == historyForm.formID, (FormHistory.status == "Approved") | (FormHistory.status == "Pending"), FormHistory.historyType == "Labor Status Form") - def set_evaluation_button(self, historyForm, currentUser): - ogHistoryForm = self.get_history_form_from_lsf(historyForm) - evaluations = StudentLaborEvaluation.select().where(StudentLaborEvaluation.formHistoryID == ogHistoryForm, StudentLaborEvaluation.is_submitted == True) - if currentUser.student: - for evaluation in evaluations: - - if evaluation.is_midyear_evaluation and not historyForm.formID.termCode.isFinalEvaluationOpen: - self.evaluation_exists = True - elif not evaluation.is_midyear_evaluation: #i.e., it's a final evaluation - self.evaluation_exists = True - else: - currentUserDepartments = [department.DEPT_NAME for department in getDepartmentsForSupervisor(currentUser)] - if ogHistoryForm.formID.supervisor.DEPT_NAME in currentUserDepartments or currentUser.isLaborAdmin: - if historyForm.formID.termCode.isFinalEvaluationOpen or historyForm.formID.termCode.isMidyearEvaluationOpen: - self.evaluate = True - for evaluation in evaluations: - if evaluation.is_midyear_evaluation and not historyForm.formID.termCode.isFinalEvaluationOpen: - # If a midyear evaluation has been completed - self.evaluation_exists = True - self.evaluate = False - elif not evaluation.is_midyear_evaluation: - # If a final labor evaluation has been completed - self.evaluation_exists = True - self.evaluate = False - def set_button_states(self, historyForm, currentUser): ############################################################ # Student Options @@ -73,6 +50,8 @@ def set_button_states(self, historyForm, currentUser): self.adjust = False self.correction = False self.evaluate = False + self.studentApprove = True + self.resubmit = False self.num_buttons = 1 ############################################################ @@ -82,17 +61,17 @@ def set_button_states(self, historyForm, currentUser): #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Release #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - if historyForm.releaseForm != None: + if historyForm.releaseForm: if historyForm.status.statusName == "Approved": - # Approved release forms can be rehired self.rehire = True + self.resubmit = True self.num_buttons += 2 elif "Denied" in historyForm.status.statusName: self.rehire = True self.release = True self.adjust = True - self.num_buttons += 4 + self.num_buttons += 3 elif historyForm.status.statusName == "Pending": # Pending release forms get no buttons @@ -101,17 +80,13 @@ def set_button_states(self, historyForm, currentUser): #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Adjustment #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - elif historyForm.adjustedForm != None: + elif historyForm.adjustedForm: if historyForm.status.statusName in ["Approved","Denied by Student","Denied by Admin"]: self.rehire = True self.release = True self.adjust = True - - self.num_buttons += 4 - - elif historyForm.status.statusName == "Pending": - # Pending adjustment forms get no buttons - self.num_buttons += 1 + self.num_buttons += 3 + # Pending adjustment forms get no buttons #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Standard or Overload @@ -128,8 +103,7 @@ def set_button_states(self, historyForm, currentUser): self.rehire = True self.num_buttons += 1 - elif historyForm.status.statusName in ["Approved","Approved Reluctantly"]: - self.num_buttons += 1 + elif historyForm.status.statusName == "Approved": if self.currentDate <= historyForm.formID.endDate: # An approved LSF before the end of the term if self.currentDate > historyForm.formID.termCode.adjustmentCutOff and not currentUser.isLaborAdmin: @@ -142,7 +116,8 @@ def set_button_states(self, historyForm, currentUser): self.release = True self.adjust = True self.rehire = True - self.num_buttons += 3 + self.resubmit = True + self.num_buttons += 4 else: self.rehire = True self.num_buttons += 1 diff --git a/app/logic/departmentPositions.py b/app/logic/departmentPositions.py new file mode 100644 index 000000000..cd6ca53b6 --- /dev/null +++ b/app/logic/departmentPositions.py @@ -0,0 +1,43 @@ +from app.logic.tracy import Tracy, InvalidQueryException +from app.models.department import Department +from app.logic.userInsertFunctions import updateDepartmentRecord +from app.models.Tracy.stuposn import STUPOSN + + +def updatePositionRecords(): + remoteDepartments = Tracy().getDepartments() # Create local copies of new departments in Tracy + departmentsPulledFromTracy = 0 + for dept in remoteDepartments: + d = Department.get_or_none(ACCOUNT=dept.ACCOUNT, ORG=dept.ORG) + if d: + d.DEPT_NAME = dept.DEPT_NAME + d.save() + else: + Department.create(DEPT_NAME=dept.DEPT_NAME, ACCOUNT=dept.ACCOUNT, ORG=dept.ORG) + departmentsPulledFromTracy += 1 + + departmentsInDB = list(Department.select()) + departmentsUpdated = 0 + departmentsNotFound = 0 + departmentsFailed = 0 + for department in departmentsInDB: + try: + updateDepartmentRecord(department) + departmentsUpdated += 1 + except InvalidQueryException as e: + departmentsNotFound += 1 + except Exception as e: + departmentsFailed += 1 + + return departmentsPulledFromTracy, departmentsUpdated, departmentsNotFound, departmentsFailed + +def updateDepartmentRecord(department): + tracyDepartment = STUPOSN.query.filter((STUPOSN.ORG == department.ORG) & (STUPOSN.ACCOUNT == department.ACCOUNT)).first() + + department.isActive = bool(tracyDepartment) + if tracyDepartment is None: + raise InvalidQueryException("Department ({department.ORG}, {department.ACCOUNT}) not found") + + + department.DEPT_NAME = tracyDepartment.DEPT_NAME + department.save() diff --git a/app/logic/download.py b/app/logic/download.py index 9e293e322..b9532446d 100644 --- a/app/logic/download.py +++ b/app/logic/download.py @@ -1,5 +1,12 @@ +import csv import json + from flask import g +from peewee import ModelSelect + +from app.models.formHistory import * +from app.controllers.main_routes.main_routes import * +from app.models.studentLaborEvaluation import StudentLaborEvaluation from app.models.formSearchResult import FormSearchResult def saveFormSearchResult(displayName, formList, formType): @@ -20,3 +27,208 @@ def retrieveFormSearchResult(formSearchResultId): return result return None + +class CSVMaker: + ''' + Create the CSV for the download bottons + ''' + def __init__(self, downloadName, requestedLSFs: ModelSelect, includeEvals = False, additionalSpreadsheetFields: list[str] = []): + self.relativePath = f'static/files/{downloadName}.csv' + self.completePath = 'app/' + self.relativePath + self.additionalSpreadsheetFields = (self._validateAdditionalSpreadsheetFields(additionalSpreadsheetFields)) + self.formHistories = requestedLSFs + self.includeEvals = includeEvals + self.makeCSV() + + @staticmethod + def _validateAdditionalSpreadsheetFields(additionalFields): + for additionalField in additionalFields: + if additionalField not in {'overloads', 'finalEvaluations', 'midYearEvaluations', 'allEvaluations'}: + raise ValueError(f'Invalid spreadsheet fields: {additionalField}') + return additionalFields + + def makeCSV(self): + ''' + Creates the CSV file + ''' + with open(self.completePath, 'w', encoding="utf-8", errors="backslashreplace") as csvfile: + self.filewriter = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + + ## Create heading on csv ## + headers = ([ 'Term', + 'Form Type', + 'Form Status', + 'B#', + 'Student Name', + 'Student Email', + 'Position', + 'Labor Position Code', + 'Labor Position Title', + 'WLS', + 'Weekly Hours', + 'Total Contract Hours', + 'Start Date', + 'End Date', + 'Department', + 'Supervisor', + 'Supervisor Email', + 'Supervisor Notes' + ]) + if self.includeEvals: + headers.extend(['SLE Type', + 'SLE Attendance', + 'SLE Attendance Comments', + 'SLE Accountability', + 'SLE Accountability Comments', + 'SLE Teamwork', + 'SLE Teamwork Comments', + 'SLE Initiative', + 'SLE Initiative Comments', + 'SLE Respect', + 'SLE Respect Comments', + 'SLE Learning', + 'SLE Learning Comments', + 'SLE Job Specific', + 'SLE Job Specific Comments', + 'SLE Overall Score' + ]) + + if 'overloads' in self.additionalSpreadsheetFields: + headers.extend(['Student Overload Reason', + 'Financial Aid Status', + 'Financial Aid Approver', + 'Financial Aid Review Date', + 'SAAS Status', + 'SAAS Approver', + 'SAAS Review Date', + 'Labor Status', + 'Labor Approver', + 'Labor Review Date']) + + self.filewriter.writerow(headers) + + for form in self.formHistories: + row = self.addPrimaryData(form) + if self.includeEvals: + evalRows = self.addEvaluationData(form.formHistoryID) + if len(evalRows) == 0: + self.filewriter.writerow(row) + elif len(evalRows) == 1: + row.extend(evalRows[0]) + self.filewriter.writerow(row) + else: + row.extend(evalRows[0]) + self.filewriter.writerow(row) + for evaluation in evalRows[1:]: + row = [""] * 17 + row.extend(evaluation) + self.filewriter.writerow(row) + + elif 'overloads' in self.additionalSpreadsheetFields: + row = self.addOverloadData(form, row) + self.filewriter.writerow(row) + else: + self.filewriter.writerow(row) + + + def addPrimaryData(self, form): + ''' + Adds data included on every CSV + ''' + + row = [ form.formID.termCode.termName, + form.historyType_id, + form.status_id, + form.formID.studentSupervisee.ID, + u' '.join((form.formID.studentSupervisee.FIRST_NAME, form.formID.studentSupervisee.LAST_NAME)), + form.formID.studentSupervisee.STU_EMAIL, + form.formID.jobType, + form.formID.POSN_CODE, + form.formID.POSN_TITLE, + form.formID.WLS, + form.formID.weeklyHours, + form.formID.contractHours, + form.formID.startDate, + form.formID.endDate, + form.formID.department.DEPT_NAME, + u' '.join((form.formID.supervisor.FIRST_NAME, form.formID.supervisor.LAST_NAME)), + form.formID.supervisor.EMAIL, + form.formID.supervisorNotes + ] + return row + + def addOverloadData(self, form, rowData): + + faApprover = form.overloadForm.financialAidApprover.fullName if form.overloadForm.financialAidApprover else "" + saasApprover = form.overloadForm.SAASApprover.fullName if form.overloadForm.SAASApprover else "" + laborApprover = form.overloadForm.laborApprover.fullName if form.overloadForm.laborApprover else "" + + rowData.extend([ + form.overloadForm.studentOverloadReason, + form.overloadForm.financialAidApproved_id, + faApprover, + form.overloadForm.financialAidReviewDate, + form.overloadForm.SAASApproved_id, + saasApprover, + form.overloadForm.SAASReviewDate, + form.overloadForm.laborApproved_id, + laborApprover, + form.overloadForm.laborReviewDate, + ]) + + return rowData + + + def addEvaluationData(self, formID): + ''' + Adds data for SLE + ''' + multipleRows = [] + if "finalEvaluations" in self.additionalSpreadsheetFields: + finalEvaluation = StudentLaborEvaluation.get_or_none(StudentLaborEvaluation.formHistoryID == formID, StudentLaborEvaluation.is_midyear_evaluation == 0, StudentLaborEvaluation.is_submitted == True) + if finalEvaluation: + multipleRows.append(self.insertEvaluationData(finalEvaluation, "Final")) + elif "midYearEvaluations" in self.additionalSpreadsheetFields: + midyearEvaluation = StudentLaborEvaluation.get_or_none(StudentLaborEvaluation.formHistoryID == formID, StudentLaborEvaluation.is_midyear_evaluation == 1, StudentLaborEvaluation.is_submitted == True) + if midyearEvaluation: + multipleRows.append(self.insertEvaluationData(midyearEvaluation, "Midyear")) + elif self.includeEvals == True: + anyEvaluation = StudentLaborEvaluation.select().where(StudentLaborEvaluation.formHistoryID == formID, StudentLaborEvaluation.is_submitted == True) + if anyEvaluation: + for evaluation in anyEvaluation: + multipleRows.append(self.insertEvaluationData(evaluation, "Midyear" if evaluation.is_midyear_evaluation else "Final")) + else: + return [] + + return multipleRows + + def insertEvaluationData(self, evaluation, evalType): + ''' + Helper function for self.addEvaluationData(); Adds individual row's SLE data + ''' + tableRow = [] + tableRow.extend([ evalType, + evaluation.attendance_score, + evaluation.attendance_comment, + evaluation.accountability_score, + evaluation.accountability_comment, + evaluation.teamwork_score, + evaluation.teamwork_comment, + evaluation.initiative_score, + evaluation.initiative_comment, + evaluation.respect_score, + evaluation.respect_comment, + evaluation.learning_score, + evaluation.learning_comment, + evaluation.jobSpecific_score, + evaluation.jobSpecific_comment + ]) + tableRow.append(evaluation.attendance_score + + evaluation.accountability_score + + evaluation.teamwork_score + + evaluation.initiative_score + + evaluation.respect_score + + evaluation.learning_score + + evaluation.jobSpecific_score + ) + return tableRow diff --git a/app/logic/emailHandler.py b/app/logic/emailHandler.py index 8b5f06f78..1de8ef492 100644 --- a/app/logic/emailHandler.py +++ b/app/logic/emailHandler.py @@ -17,6 +17,7 @@ import os from datetime import datetime, date + class emailHandler(): def __init__(self, formHistoryKey): self.mail = Mail(app) @@ -31,7 +32,7 @@ def __init__(self, formHistoryKey): 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: @@ -101,7 +102,6 @@ def send(self, message: Message): message.recipients = [app.config['MAIL_OVERRIDE_ALL']] message.reply_to = app.config["REPLY_TO_ADDRESS"] - # print("Debugging emailHandler.py: ", app.config) self.mail.send(message) elif app.config['ENV'] == 'testing': @@ -137,7 +137,45 @@ def laborStatusFormSubmitted(self): self.checkRecipient("Labor Status Form Submitted For Student", "Primary Position Labor Status Form Submitted") - + def laborStatusFormExpired(self): + """ + Sends email to labor supervisor and student when LSF is expired. + """ + + lsfID = self.laborStatusForm.laborStatusFormID + expired = self.laborStatusForm.isExpired + if not expired: + return + + supervisorTemplate = EmailTemplate.get_or_none( + EmailTemplate.purpose == "Email when Labor Status Form is expired to Supervisor" + ) + studentTemplate = EmailTemplate.get_or_none( + EmailTemplate.purpose == "Email when Labor Status Form is expired to Student" + ) + if not supervisorTemplate or not studentTemplate: + return + + try: + alreadySent = (EmailTracker + .select() + .where( + (EmailTracker.formID == lsfID) & + ((EmailTracker.subject == supervisorTemplate.subject) | (EmailTracker.subject == studentTemplate.subject)) & + (EmailTracker.date == date.today()) + ) + .exists()) + if alreadySent: + return + + except Exception as e: + print(f"Check failed for LSF{lsfID}: {e}. Proceeding to send.") + + self.checkRecipient( + studentEmailPurpose=studentTemplate.purpose, + emailPurpose=supervisorTemplate.purpose, + secondaryEmailPurpose=None + ) def laborStatusFormApproved(self): if self.laborStatusForm.jobType == 'Secondary': @@ -171,9 +209,13 @@ def laborStatusFormAdjusted(self, newSupervisor=False): self.checkRecipient(False, "Labor Status Form Adjusted For Supervisor") - def laborReleaseFormSubmitted(self): + def laborReleaseFormSubmitted(self, adminUserName=None, adminName=None): + self.adminName = adminName + self.adminEmail = adminUserName + "@berea.edu" + emailTemplate = EmailTemplate.get(EmailTemplate.purpose == "Labor Release Form Admin Notification") self.checkRecipient("Labor Release Form Submitted For Student", "Labor Release Form Submitted For Supervisor") + self.sendEmail(emailTemplate, "admin") def laborReleaseFormApproved(self): self.checkRecipient("Labor Release Form Approved For Student", @@ -332,6 +374,10 @@ def sendEmail(self, template, sendTo): message = Message(template.subject, recipients=[self.supervisorEmail]) recipient = 'Primary Supervisor' + elif sendTo == "admin": + message = Message(template.subject, + recipients=[self.adminEmail]) + recipient = 'Admin' message.html = formTemplate newEmailTracker = EmailTracker.create( @@ -354,6 +400,7 @@ def replaceText(self, form): form = form.replace("@@Department@@", self.laborStatusForm.department.DEPT_NAME) form = form.replace("@@WLS@@", self.laborStatusForm.WLS) form = form.replace("@@Term@@", self.term.termName) + form = form.replace("@@Admin@@", self.adminName) if self.formHistory.rejectReason: form = form.replace("@@RejectReason@@", self.formHistory.rejectReason) @@ -370,6 +417,7 @@ def replaceText(self, form): elif self.primaryForm: # 'Primary Supervisor' is the primary supervisor of the student who's laborStatusForm is passed in the initializer form = form.replace("@@PrimarySupervisor@@", self.primaryForm.formID.supervisor.FIRST_NAME + " " + self.primaryForm.formID.supervisor.LAST_NAME) + if self.formHistory.adjustedForm: form = form.replace("@@NewAdjustmentField@@", self.newAdjustmentField) form = form.replace("@@CurrentAdjustmentField@@", self.oldAdjustmentField) diff --git a/app/logic/getTableData.py b/app/logic/getTableData.py new file mode 100644 index 000000000..9af53e9b0 --- /dev/null +++ b/app/logic/getTableData.py @@ -0,0 +1,262 @@ +import operator +from datetime import date +from functools import reduce + +from flask import json, jsonify, g, make_response +from peewee import fn, SQL + +from app.controllers.admin_routes.allPendingForms import checkAdjustment +from app.logic.search import getDepartmentsForSupervisor +from app.logic.download import saveFormSearchResult +from app.models.term import Term +from app.models.department import Department +from app.models.supervisor import Supervisor +from app.models.student import Student +from app.models.laborStatusForm import LaborStatusForm +from app.models.formHistory import FormHistory +from app.models.user import User +from app.models.studentLaborEvaluation import StudentLaborEvaluation + +def getDatatableData(request): + ''' + This function runs a query based on selected options in the front-end and retrieves the appropriate forms. + Then, it puts all the retrieved data in appropriate form to be send to the ajax call in the supervisorPortal.js file. + ''' + # 'draw', 'start', 'length', 'order[0][column]', 'order[0][dir]' are built-in parameters, i.e., + # they are implicitly passed as part of the AJAX request when using datatable server-side processing + + sleJoin = "" + draw = int(request.form.get('draw', 0)) + rowNumber = int(request.form.get('start', 0)) + rowsPerPage = int(request.form.get('length', 25)) + queryFilterData = request.form.get('data') + queryFilterDict = json.loads(queryFilterData) + sortBy = queryFilterDict.get('sortBy', "term") + if sortBy == "": + sortBy = "term" + order = queryFilterDict.get('order', "ASC") + + termCode = queryFilterDict.get('termCode', "") + if termCode == "currentTerm": + termCode = g.openTerm + elif termCode == "activeTerms": + termCode = list(Term.select(Term.termCode).where(Term.termEnd >= date.today())) + departmentId = queryFilterDict.get('departmentID', "") + supervisorId = queryFilterDict.get('supervisorID', "") + if supervisorId == "currentUser": + supervisorId = g.currentUser.supervisor + studentId = queryFilterDict.get('studentID', "") + formStatusList = queryFilterDict.get('formStatus', "") # form status radios + formTypeList = queryFilterDict.get('formType', "") # form type radios + evaluationStatus = queryFilterDict.get('evaluations', "") # evaluation radios + + fieldValueMap = {Term.termCode: termCode, + Department.departmentID: departmentId, + Student.ID: studentId, + Supervisor.ID: supervisorId, + FormHistory.status: formStatusList, + FormHistory.historyType: formTypeList, + StudentLaborEvaluation.ID: evaluationStatus} + clauses = [] + # WHERE clause conditions are dynamically generated using model fields and selectpicker values + for field, value in fieldValueMap.items(): + if value != "" and value: + if type(value) is list: + if field == FormHistory.historyType: + if "Labor Status Form" not in value: + clauses.append(field.in_(value)) # if "original" is selected, we include all forms, so no need to filter by historyType + else: + clauses.append(field.in_(value)) + elif field is StudentLaborEvaluation.ID: + sleJoin=value[0] + else: + clauses.append(field == value) + # This expression creates SQL AND operator between the conditions added to 'clauses' list + formSearchResults = (FormHistory.select() + .join(LaborStatusForm, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) + .join(Department, on=(LaborStatusForm.department == Department.departmentID)) + .join(Supervisor, on=(LaborStatusForm.supervisor == Supervisor.ID)) + .join(Student, on=(LaborStatusForm.studentSupervisee == Student.ID)) + .join(Term, on=(LaborStatusForm.termCode == Term.termCode)) + .join(User, on=(FormHistory.createdBy == User.userID))) + if clauses: + formSearchResults = formSearchResults.where(reduce(operator.and_, clauses)) + if not g.currentUser.isLaborAdmin: + supervisorDepartments = getDepartmentsForSupervisor(g.currentUser) + formSearchResults = formSearchResults.where(FormHistory.formID.department.in_(supervisorDepartments)) + recordsTotal = formSearchResults.count() + # this checks and finds the first value that is not null of preferred_name, legal_name and last_name. + # including last_name is necessary because there are like 4 cases where someone has no first name or last name, instead their full name is + # stored in last_name + supervisorFirstNameCase = fn.COALESCE(fn.NULLIF(Supervisor.preferred_name, ''), fn.NULLIF(Supervisor.legal_name, ''), Supervisor.LAST_NAME) + studentFirstNameCase = fn.COALESCE(fn.NULLIF(Student.preferred_name, ''), fn.NULLIF(Student.legal_name, ''), Student.LAST_NAME) + + # this maps all of the values we expect to receive from the sorting dropdowns in the frontend + # to actual peewee objects we can sort by later + # the casing is weird because the columns that don't have any fields are are not capitalized + sortValueColumnMap = { + "term": Term.termCode, + "department": Department.DEPT_NAME, + "supervisorFirstName": supervisorFirstNameCase, + "supervisorLastName": Supervisor.LAST_NAME, + "studentFirstName": studentFirstNameCase, + "studentLastName": Student.LAST_NAME, + "positionWLS": LaborStatusForm.WLS, + "positionTitle": LaborStatusForm.POSN_TITLE, + "positionType": LaborStatusForm.jobType, + "length": LaborStatusForm.startDate, + "createdBy": User.username, + "formStatus": FormHistory.status, + "formType": FormHistory.historyType, + } + + if order == "DESC": + filteredSearchResults = formSearchResults.order_by(fn.TRIM(sortValueColumnMap[sortBy]).desc()).limit(rowsPerPage).offset(rowNumber) + else: + filteredSearchResults = formSearchResults.order_by(fn.TRIM(sortValueColumnMap[sortBy]).asc()).limit(rowsPerPage).offset(rowNumber) + formattedData = getFormattedData(filteredSearchResults, queryFilterDict.get('view')) + + downloadId = saveFormSearchResult("Form Search", formSearchResults, "LSF Search") + formsDict = {"draw": draw, "recordsTotal": recordsTotal, "recordsFiltered": recordsTotal, "data": formattedData, "downloadId": downloadId} + + return make_response(jsonify(formsDict)) + +def getFormattedData(filteredSearchResults, view ='simple'): + ''' + Putting the data in the correct format to be used by the JS file. + Because this implementation is using server-side processing of datatables, + the HTML for the datatables are also formatted here. + ''' + if view == "simple": + formattedData = {} + filteredSearchResults.order_by(FormHistory.formID.startDate.desc()) + isMostCurrent = False + for form in filteredSearchResults: + createdDate = form.createdDate + bNumber = form.formID.studentSupervisee.ID + if bNumber not in formattedData: + absentInFormatting = True + else: + absentInFormatting = False + isMostCurrent = (createdDate > formattedData[bNumber][1]) + if absentInFormatting or isMostCurrent: + + # html fields + firstName, lastName = form.formID.studentSupervisee.FIRST_NAME, form.formID.studentSupervisee.LAST_NAME + term = form.formID.termCode.termName + positionTitle = form.formID.POSN_TITLE + jobType = form.formID.jobType + departmentName = form.formID.department.DEPT_NAME + statusFormId = form.formID.laborStatusFormID + + # determine display status for each student + formStatus = str(form.status) + displayStatus = formStatus + + if form.adjustedForm is not None: + displayStatus = "Adjustment " + formStatus + if form.overloadForm is not None: + displayStatus = "Overload " + formStatus + if form.releaseForm is not None: + displayStatus = "Release Pending" if formStatus == "Pending" else "Released" + + html = f""" + + {firstName} {lastName} ({bNumber}) + + {displayStatus} +
+ + {term} - {positionTitle} ({jobType}) - {departmentName} + + """ + + formattedData[bNumber] = (html, createdDate) + + formattedDataList = [[value] for value, _S in formattedData.values()] + + return formattedDataList + + # now in advanced view + + supervisorHTML = '{} ' + studentHTML = '
{}
{}
' + departmentHTML = ' {}' + positionHTML = ' {}' + formTypeStatus = ' {}' + formattedData = [] + for form in filteredSearchResults: + # The order in which you append the items to 'record' matters and it should match the order of columns on the table! + record = [] + # Term + record.append(form.formID.termCode.termName) + # Student + record.append(studentHTML.format( + form.formID.studentSupervisee.ID, + f'{form.formID.studentSupervisee.preferred_name if form.formID.studentSupervisee.preferred_name else form.formID.studentSupervisee.legal_name} {form.formID.studentSupervisee.LAST_NAME}', + form.formID.studentSupervisee.ID, + form.formID.studentSupervisee.STU_EMAIL)) + # Supervisor + supervisorField = supervisorHTML.format( + form.formID.supervisor.ID, + f'{form.formID.supervisor.preferred_name if form.formID.supervisor.preferred_name else form.formID.supervisor.legal_name } {form.formID.supervisor.LAST_NAME}', + form.formID.supervisor.EMAIL) + record.append(supervisorField) + + # Department + record.append(departmentHTML.format( + form.formID.department.ORG, + form.formID.department.ACCOUNT, + form.formID.department.DEPT_NAME)) + + # Position + positionField = positionHTML.format( + form.formID.jobType, + f'{form.formID.jobType} ({form.formID.WLS})') + # Hours + hoursField = form.formID.weeklyHours if form.formID.weeklyHours else form.formID.contractHours + # Adjustment Form Specific Data + checkAdjustment(form) + if (form.adjustedForm): + if form.adjustedForm.fieldAdjusted == "supervisor": + newSupervisor = supervisorHTML.format( + form.adjustedForm.oldValue['ID'], + form.adjustedForm.newValue, + form.adjustedForm.oldValue['email']) + supervisorField = f'{supervisorField}
{newSupervisor}' + + if form.adjustedForm.fieldAdjusted == "position": + newPosition = positionHTML.format( + form.adjustedForm.oldValue, + form.adjustedForm.newValue) + positionField = f'{positionField}
{newPosition}' + + if form.adjustedForm.fieldAdjusted == "weeklyHours" or form.adjustedForm.fieldAdjusted == "contractHours": + newHours = form.adjustedForm.newValue + hoursField = f'{hoursField}
{newHours}' + + record.append(f'{form.formID.POSN_TITLE}
{positionField}') + record.append(hoursField) + # Contract Dates + record.append("
".join([form.formID.startDate.strftime('%m/%d/%y'), + form.formID.endDate.strftime('%m/%d/%y')])) + # Created By + record.append(supervisorHTML.format( + form.createdBy.supervisor.ID if form.createdBy.supervisor else form.createdBy.student.ID, + form.createdBy.username, + form.createdBy.email, + form.createdDate.strftime('%m/%d/%y'))) + # Form Type + formTypeNameMapping = { + "Labor Status Form": "Original", + "Labor Adjustment Form": "Adjusted", + "Labor Overload Form": "Overload", + "Labor Release Form": "Release"} + originalFormTypeName = form.historyType.historyTypeName + mappedFormTypeName = formTypeNameMapping[originalFormTypeName] + # formType(Status) + formTypeStatusField = record.append(formTypeStatus.format(f'{mappedFormTypeName} ({form.status.statusName})')) + + formattedData.append(record) + + return formattedData diff --git a/app/logic/search.py b/app/logic/search.py index acef1b17a..864e2952c 100644 --- a/app/logic/search.py +++ b/app/logic/search.py @@ -4,6 +4,11 @@ from app.models.laborStatusForm import LaborStatusForm from app.models.department import Department from app.models.formHistory import FormHistory +from app.models.term import Term + +from functools import reduce +import operator +from peewee import JOIN def limitSearchByUserDepartment(students, currentUser): """ @@ -62,4 +67,71 @@ def getDepartmentsForSupervisor(currentUser): def getSupervisorsForDepartment(departmentId): departmentSupervisors = Supervisor.select().join(SupervisorDepartment).where(SupervisorDepartment.department == departmentId).order_by(Supervisor.LAST_NAME).execute() - return departmentSupervisors \ No newline at end of file + return departmentSupervisors + +def searchSupervisorPortal(currentUser, searchType, userInput): + if currentUser.isLaborAdmin or currentUser.isFinancialAidAdmin or currentUser.isSaasAdmin: + allowed_departments = None # unrestricted + else: + allowed_departments = [dept.DEPT_NAME for dept in getDepartmentsForSupervisor(currentUser)] + + if searchType == "termSelect": + terms = Term.select().where(Term.termName.contains(userInput)).order_by(Term.termCode.desc()) + return [{'termCode': term.termCode, 'termName': term.termName} for term in terms] + + elif searchType == "departmentSelect": + query = Department.select().where(Department.DEPT_NAME.contains(userInput)) + if allowed_departments is not None: + query = query.where(Department.DEPT_NAME.in_(allowed_departments)) + query = query.order_by(Department.isActive.desc(), Department.DEPT_NAME.asc()).limit(10) + return [ + {'DEPT_NAME': dept.DEPT_NAME, 'id': dept.departmentID, 'isActive': dept.isActive} + for dept in query + ] + + elif searchType == "supervisorSelect": + supervisor_query = searchPerson(Supervisor, userInput, allowed_departments) + supervisor_query = supervisor_query.order_by(Supervisor.isActive.desc()).limit(10) + return [ + {'id': sup.ID, 'FIRST_NAME': sup.FIRST_NAME, "LAST_NAME": sup.LAST_NAME, "isActive": sup.isActive} + for sup in supervisor_query + ] + + elif searchType == "studentSelect": + student_query = searchPerson(Student, userInput, allowed_departments) + student_query = student_query.order_by(Student.LAST_NAME.asc()).limit(10) + return [ + {'id': stu.ID, 'FIRST_NAME': stu.FIRST_NAME, 'LAST_NAME': stu.LAST_NAME} + for stu in student_query + ] + + return [] + +def searchPerson(model, userInput, allowed_departments=None): + """ + Returns a Peewee SelectObject filtered so that all words in userInput + must appear in at least one of the model's fields. + """ + words = userInput.strip().split() + + word_conditions = [] + for word in words: + word_conditions.append( + (model.preferred_name.contains(word)) | + (model.legal_name.contains(word)) | + (model.LAST_NAME.contains(word)) | + (model.ID.contains(word)) + ) + + query = model.select().where(reduce(operator.and_, word_conditions)) + + if allowed_departments is not None: + query = ( + query + .join_from(model, LaborStatusForm, JOIN.LEFT_OUTER) + .join_from(LaborStatusForm, Department, JOIN.LEFT_OUTER) + .where(Department.DEPT_NAME.in_(allowed_departments)) + .distinct() + ) + + return query \ No newline at end of file diff --git a/app/logic/statusFormFunctions.py b/app/logic/statusFormFunctions.py new file mode 100644 index 000000000..4694bad63 --- /dev/null +++ b/app/logic/statusFormFunctions.py @@ -0,0 +1,288 @@ +from app.models.overloadForm import * +from datetime import datetime, date, timedelta, time +from app.models.laborStatusForm import * +from app.logic.userInsertFunctions import InvalidUserException +from app.models.formHistory import* +from app.logic.emailHandler import emailHandler +from app.logic.utils import makeThirdPartyLink, calculateExpirationDate +from app.logic.tracy import Tracy, InvalidQueryException +from flask import request, json, jsonify +from functools import reduce +import operator + + +def createLaborStatusForm(student, primarySupervisor, department, term, rspFunctional): + """ + Creates a labor status form with the appropriate data passed from userInsert() in laborStatusForm.py + studentID: student's primary ID in the database AKA their B# + primarySupervisor: primary supervisor of the student + department: department the position is a part of + term: term when the position will happen + rspFunctional: a dictionary containing all the data submitted in the LSF page + returns the laborStatusForm object just created for later use in laborStatusForm.py + """ + # Changes the dates into the appropriate format for the table + startDate = datetime.strptime(rspFunctional['stuStartDate'], "%m/%d/%Y").strftime('%Y-%m-%d') + endDate = datetime.strptime(rspFunctional['stuEndDate'], "%m/%d/%Y").strftime('%Y-%m-%d') + # Creates the labor Status form + lsf = LaborStatusForm.create(termCode_id = term, + studentSupervisee_id = student.ID, + supervisor_id = primarySupervisor, + department_id = department, + jobType = rspFunctional["stuJobType"], + WLS = rspFunctional["stuWLS"], + POSN_TITLE = rspFunctional["stuPosition"], + POSN_CODE = rspFunctional["stuPositionCode"], + contractHours = rspFunctional.get("stuContractHours", None), + weeklyHours = rspFunctional.get("stuWeeklyHours", None), + startDate = startDate, + endDate = endDate, + supervisorNotes = rspFunctional["stuNotes"], + laborDepartmentNotes = rspFunctional["stuLaborNotes"], + studentName = student.legal_name + " " + student.LAST_NAME, + studentExpirationDate = calculateExpirationDate() + ) + + return lsf + + +def createOverloadFormAndFormHistory(rspFunctional, lsf, creatorID, host=None): + """ + Creates a 'Labor Status Form' and then if the request needs an overload we create + a 'Labor Overload Form'. Emails are sent based on whether the form is an 'Overload Form' + rspFunctional: a dictionary containing all the data submitted in the LSF page + lsf: stores the new instance of a labor status form + creatorID: id of the user submitting the labor status form + status: status of the labor status form (e.g. Pending, etc.) + """ + # We create a 'Labor Status Form' first, then we check to see if a 'Labor Overload Form' + # needs to be created + isOverload = rspFunctional.get("isItOverloadForm") == "True" + if isOverload: + newLaborOverloadForm = OverloadForm.create( studentOverloadReason = None, + financialAidApproved = None, + financialAidApprover = None, + financialAidReviewDate = None, + SAASApproved = None, + SAASApprover = None, + SAASReviewDate = None, + laborApproved = None, + laborApprover = None, + laborReviewDate = None) + formOverload = FormHistory.create( formID = lsf.laborStatusFormID, + historyType = "Labor Overload Form", + overloadForm = newLaborOverloadForm.overloadFormID, + createdBy = creatorID, + createdDate = date.today(), + status = "Pre-Student Approval") + email = emailHandler(formOverload.formHistoryID) + link = makeThirdPartyLink("student", host, formOverload.formHistoryID) + email.LaborOverLoadFormSubmitted(link) + + formHistory = FormHistory.create( formID = lsf.laborStatusFormID, + historyType = "Labor Status Form", + overloadForm = None, + createdBy = creatorID, + createdDate = date.today(), + status = "Pre-Student Approval") + + if not formHistory.formID.termCode.isBreak and not isOverload: + email = emailHandler(formHistory.formHistoryID) + email.laborStatusFormSubmitted() + + return formHistory + + + +def checkForSecondLSFBreak(termCode, student): + """ + Checks if a student has more than one labor status form submitted for them during a break term, and sends emails accordingly. + """ + positions = (LaborStatusForm.select() + .join(FormHistory) + .where( LaborStatusForm.termCode == termCode, + LaborStatusForm.studentSupervisee == student, + ~(FormHistory.status_id % "Denied%")) + .distinct()) + isMoreLSFDict = {} + storeLSFFormsID = [] + previousSupervisorNames = [] + if len(list(positions)) >= 1: # If student has one or more than one lsf + isMoreLSFDict["showModal"] = True # show modal when the student has one or more than one lsf + + for item in positions: + previousSupervisorNames.append(item.supervisor.FIRST_NAME + " " + item.supervisor.LAST_NAME) + isMoreLSFDict["studentName"] = item.studentSupervisee.FIRST_NAME + " " + item.studentSupervisee.LAST_NAME + isMoreLSFDict['previousSupervisorNames'] = previousSupervisorNames + + if len(list(positions)) == 1: # if there is only one labor status form then send email to the supervisor and student + laborStatusFormID = positions[0].laborStatusFormID + formHistoryID = FormHistory.get(FormHistory.formID == laborStatusFormID) + isMoreLSFDict["formHistoryID"] = formHistoryID.formHistoryID + + else: # if there are more lsfs then send email to student, supervisor and all previous supervisors + for item in positions: # add all the previous lsf ID's + storeLSFFormsID.append(item.laborStatusFormID) # store all of the previous labor status forms for break + laborStatusFormID = storeLSFFormsID.pop() #save all the previous lsf ID's except the one currently created. Pop removes the one created right now. + formHistoryID = FormHistory.get(FormHistory.formID == laborStatusFormID) + isMoreLSFDict['formHistoryID'] = formHistoryID.formHistoryID + isMoreLSFDict["lsfFormID"] = storeLSFFormsID + else: + isMoreLSFDict["showModal"] = False # Do not show the modal when there's not previous lsf + return json.dumps(isMoreLSFDict) + +def checkForPrimaryPosition(termCode, student, currentUser): + """ Checks if a student has a primary supervisor (which means they have primary position) in the selected term. """ + rsp = (request.data).decode("utf-8") # This turns byte data into a string + rspFunctional = json.loads(rsp) + term = Term.get(Term.termCode == termCode) + + termYear = termCode[:-2] + shortCode = termCode[-2:] + clauses = [] + if shortCode == '00': + fallTermCode = termYear + '11' + springTermCode = termYear + '12' + clauses.extend([FormHistory.formID.termCode == fallTermCode, + FormHistory.formID.termCode == springTermCode, + FormHistory.formID.termCode == termCode]) + else: + ayTermCode = termYear + '00' + clauses.extend([FormHistory.formID.termCode == ayTermCode, + FormHistory.formID.termCode == termCode]) + expression = reduce(operator.or_, clauses) # This expression creates SQL OR operator between the conditions added to 'clauses' list + + try: + lastPrimaryPosition = FormHistory.select()\ + .join_from(FormHistory, LaborStatusForm)\ + .join_from(FormHistory, HistoryType)\ + .where((expression) & + (FormHistory.formID.studentSupervisee == student) & + (FormHistory.historyType.historyTypeName == "Labor Status Form") & + (FormHistory.formID.jobType == "Primary"))\ + .order_by(FormHistory.formHistoryID.desc())\ + .get() + except DoesNotExist: + lastPrimaryPosition = None + + if not lastPrimaryPosition: + approvedRelease = None + else: + try: + approvedRelease = FormHistory.select()\ + .where(FormHistory.formID == lastPrimaryPosition.formID, + FormHistory.historyType == "Labor Release Form", + FormHistory.status == "Approved")\ + .order_by(FormHistory.formHistoryID.desc())\ + .get() + except DoesNotExist: + approvedRelease = None + + finalStatus = {} + if not term.isBreak: + if lastPrimaryPosition and not approvedRelease: + if rspFunctional == "Primary": + if "Denied" in lastPrimaryPosition.status.statusName: # handle two denied statuses + finalStatus["status"] = "hire" + else: + finalStatus["status"] = "noHire" + finalStatus["term"] = lastPrimaryPosition.formID.termCode.termName + finalStatus["primarySupervisor"] = lastPrimaryPosition.formID.supervisor.FIRST_NAME + " " +lastPrimaryPosition.formID.supervisor.LAST_NAME + finalStatus["department"] = lastPrimaryPosition.formID.department.DEPT_NAME + " (" + lastPrimaryPosition.formID.department.ORG + "-" + lastPrimaryPosition.formID.department.ACCOUNT+ ")" + finalStatus["position"] = lastPrimaryPosition.formID.POSN_CODE +" - "+lastPrimaryPosition.formID.POSN_TITLE + " (" + lastPrimaryPosition.formID.WLS + ")" + finalStatus["hours"] = lastPrimaryPosition.formID.jobType + " (" + str(lastPrimaryPosition.formID.weeklyHours) + ")" + finalStatus["isLaborAdmin"] = currentUser.isLaborAdmin + finalStatus["approvedForm"] = (lastPrimaryPosition.status_id == "Approved") + else: + if lastPrimaryPosition.status_id in ["Approved", "Pending"]: + lastPrimaryPositionTermCode = str(lastPrimaryPosition.formID.termCode.termCode)[-2:] + # if selected term is AY and student has an approved/pending LSF in spring or fall + if shortCode == '00' and lastPrimaryPositionTermCode in ['11', '12']: + finalStatus["status"] = "noHireForSecondary" + else: + finalStatus["status"] = "hire" + else: + finalStatus["status"] = "noHireForSecondary" + + elif lastPrimaryPosition and approvedRelease: + if rspFunctional == "Primary": + finalStatus["status"] = "hire" + else: + finalStatus["status"] = "noHireForSecondary" + else: + if rspFunctional == "Primary": + finalStatus["status"] = "hire" + else: + finalStatus["status"] = "noHireForSecondary" + else: + finalStatus["status"] = "hire" + return json.dumps(finalStatus) + + +def emailDuringBreak(secondLSFBreak, term): + """ + Sending emails during break period + """ + if term.isBreak: + isOneLSF = json.loads(secondLSFBreak) + formHistory = FormHistory.get(FormHistory.formHistoryID == isOneLSF['formHistoryID']) + email = emailHandler(formHistory.formHistoryID) + email.laborStatusFormSubmitted() + if(len(isOneLSF["previousSupervisorNames"]) > 1): #Student has more than one lsf. Send email to both supervisors and student + email.notifyAdditionalLaborStatusFormSubmittedForBreak() + + +def createOverloadForm(newWeeklyHours, lsf, currentUser, adjustedForm=None, formHistories=None, host=None): + allTermForms = LaborStatusForm.select() \ + .join_from(LaborStatusForm, Student) \ + .join_from(LaborStatusForm, FormHistory) \ + .where((LaborStatusForm.termCode == lsf.termCode) & + (LaborStatusForm.studentSupervisee.ID == lsf.studentSupervisee.ID) & + ~(FormHistory.status % "Denied%") & + (FormHistory.historyType == "Labor Status Form")) + + previousTotalHours = 0 + if allTermForms: + for statusForm in allTermForms: + previousTotalHours += statusForm.weeklyHours + changeInHours = newWeeklyHours - lsf.weeklyHours + newTotalHours = previousTotalHours + changeInHours + + if previousTotalHours <= 15 and newTotalHours > 15: # If we weren't overloading and now we are + newLaborOverloadForm = OverloadForm.create(studentOverloadReason = "None") + newFormHistory = FormHistory.create(formID = lsf.laborStatusFormID, + historyType = "Labor Overload Form", + createdBy = currentUser, + adjustedForm = adjustedForm, + overloadForm = newLaborOverloadForm.overloadFormID, + createdDate = date.today(), + status = "Pre-Student Approval") + try: + if formHistories: + formHistories.status = "Pre-Student Approval" + formHistories.save() + + else: + modifiedFormHistory = FormHistory.select() \ + .join_from(FormHistory, HistoryType) \ + .where(FormHistory.formID == lsf.laborStatusFormID, FormHistory.historyType.historyTypeName == "Labor Status Form") \ + .get() + modifiedFormHistory.status = "Pre-Student Approval" + modifiedFormHistory.save() + + link = makeThirdPartyLink("student", host, newFormHistory.formHistoryID) + overloadEmail = emailHandler(newFormHistory.formHistoryID) + overloadEmail.LaborOverLoadFormSubmitted(link) + + except Exception as e: + print("An error occured while attempting to send overload form emails: ", e) + + # This will delete an overload form after the hours are changed + elif previousTotalHours > 15 and newTotalHours <= 15: # If we were overloading and now we aren't + print(f"Trying to get formhistory with formID '{lsf.laborStatusFormID}' and history type: 'Labor Overload Form'") + # XXX this breaks if the overload was attached to a different form. ie, this form is the + # primary, but a secondary is what triggered the overload process + deleteOverloadForm = FormHistory.get((FormHistory.formID == lsf.laborStatusFormID) & (FormHistory.historyType == "Labor Overload Form")) + deleteOverloadForm = OverloadForm.get(OverloadForm.overloadFormID == deleteOverloadForm.overloadForm_id) + deleteOverloadForm.delete_instance() # This line also deletes the Form History since it's set to cascade up in the model file +# end createOverloadForm diff --git a/app/logic/tracy.py b/app/logic/tracy.py index e2b122c23..cd0ecf50a 100644 --- a/app/logic/tracy.py +++ b/app/logic/tracy.py @@ -4,8 +4,15 @@ from app.models.Tracy.stuposn import STUPOSN from app.models.Tracy.studata import STUDATA from app.models.Tracy.stustaff import STUSTAFF +from app.models.supervisor import Supervisor +from app.models.student import Student from app import app + + +class InvalidUserException(Exception): + pass + class InvalidQueryException(Exception): pass @@ -148,3 +155,4 @@ def checkStudentOrSupervisor(self, username: str): student = self.getStudentFromEmail(email) if student: return "Student" + diff --git a/app/logic/userInsertFunctions.py b/app/logic/userInsertFunctions.py index 46bbb55fb..5e77212d3 100644 --- a/app/logic/userInsertFunctions.py +++ b/app/logic/userInsertFunctions.py @@ -1,11 +1,5 @@ from datetime import datetime, date, timedelta, time from functools import reduce -import operator - -from flask import json, jsonify -from flask import request -from flask import Flask, redirect, url_for, flash -from flask_login import login_required from peewee import DoesNotExist from app.models.user import * @@ -18,38 +12,8 @@ from app.models.student import Student from app.models.supervisor import Supervisor from app.models.department import * -from app.models.Tracy.stuposn import STUPOSN -from app.logic.emailHandler import emailHandler -from app.logic.tracy import Tracy, InvalidQueryException -from app.logic.utils import makeThirdPartyLink - -class InvalidUserException(Exception): - pass - -def createUser(username, student=None, supervisor=None): - """ - Retrieves or creates a user in the User table and updates Supervisor and/or Student as requested. - - Raises InvalidUserException if this does not succeed. - """ - - if not student and not supervisor: - raise InvalidUserException("A User should be connected to Student or Supervisor") +from app.logic.tracy import Tracy, InvalidQueryException, InvalidUserException - try: - user = User.get_or_create(username=username)[0] - - except Exception as e: - raise InvalidUserException("Adding {} to user table failed".format(username), e) - - if student: - user.student = student.ID # Not sure why assigning the object doesn't work... - if supervisor: - user.supervisor = supervisor.ID - - user.save() - - return user def updatePersonRecords(): """ @@ -140,101 +104,51 @@ def updateSupervisorRecord(supervisor): supervisor.isActive = True supervisor.save() -def updatePositionRecords(): - remoteDepartments = Tracy().getDepartments() # Create local copies of new departments in Tracy - departmentsPulledFromTracy = 0 - for dept in remoteDepartments: - d = Department.get_or_none(ACCOUNT=dept.ACCOUNT, ORG=dept.ORG) - if d: - d.DEPT_NAME = dept.DEPT_NAME - d.save() - else: - Department.create(DEPT_NAME=dept.DEPT_NAME, ACCOUNT=dept.ACCOUNT, ORG=dept.ORG) - departmentsPulledFromTracy += 1 - - departmentsInDB = list(Department.select()) - departmentsUpdated = 0 - departmentsNotFound = 0 - departmentsFailed = 0 - for department in departmentsInDB: - try: - updateDepartmentRecord(department) - departmentsUpdated += 1 - except InvalidQueryException as e: - departmentsNotFound += 1 - except Exception as e: - departmentsFailed += 1 - - return departmentsPulledFromTracy, departmentsUpdated, departmentsNotFound, departmentsFailed -def updateDepartmentRecord(department): - tracyDepartment = STUPOSN.query.filter((STUPOSN.ORG == department.ORG) & (STUPOSN.ACCOUNT == department.ACCOUNT)).first() - - department.isActive = bool(tracyDepartment) - if tracyDepartment is None: - raise InvalidQueryException("Department ({department.ORG}, {department.ACCOUNT}) not found") - - - department.DEPT_NAME = tracyDepartment.DEPT_NAME - department.save() - - -def createSupervisorFromTracy(username=None, bnumber=None): +def createUser(username, student=None, supervisor=None): """ - Attempts to add a supervisor from the Tracy database to the application, based on the provided username or bnumber. + Retrieves or creates a user in the User table and updates Supervisor and/or Student as requested. - Raises InvalidUserException if this does not succeed. + Raises InvalidUserException if this does not succeed. """ - if bnumber: - try: - tracyUser = Tracy().getSupervisorFromID(bnumber) - except InvalidQueryException as e: - raise InvalidUserException("{} not found in Tracy database".format(bnumber)) - else: # Executes if no ID is provided - email = "{}@berea.edu".format(username) - try: - tracyUser = Tracy().getSupervisorFromEmail(email) - except InvalidQueryException as e: - raise InvalidUserException("{} not found in Tracy database".format(email)) + if not student and not supervisor: + raise InvalidUserException("A User should be connected to Student or Supervisor") try: - return Supervisor.get(Supervisor.ID == tracyUser.ID.strip()) - except DoesNotExist: - return Supervisor.create(PIDM = tracyUser.PIDM, - legal_name = tracyUser.FIRST_NAME, - LAST_NAME = tracyUser.LAST_NAME, - ID = tracyUser.ID.strip(), - EMAIL = tracyUser.EMAIL, - CPO = tracyUser.CPO, - ORG = tracyUser.ORG, - DEPT_NAME = tracyUser.DEPT_NAME) + user = User.get_or_create(username=username)[0] + except Exception as e: - print(e) - raise InvalidUserException("Error: Could not get or create {0} {1}".format(tracyUser.FIRST_NAME, tracyUser.LAST_NAME)) + raise InvalidUserException("Adding {} to user table failed".format(username), e) + + if student: + user.student = student.ID # Not sure why assigning the object doesn't work... + if supervisor: + user.supervisor = supervisor.ID + + user.save() + + return user def getOrCreateStudentRecord(username=None, bnumber=None): """ Attempts to add a student from the Tracy database to the application, based on the provided username or bnumber. - Raises InvalidUserException if this does not succeed. """ if not username and not bnumber: raise ValueError("No arguments provided to getOrCreateStudentRecord()") - try: if bnumber: student = Student.get(Student.ID == bnumber) else: student = Student.get(Student.STU_EMAIL == "{}@berea.edu".format(username)) - except DoesNotExist: student = createStudentFromTracy(username,bnumber) - return student + def createStudentFromTracy(username=None, bnumber=None): """ Attempts to add a student from the Tracy database to the application, based on the provided username or bnumber. @@ -278,224 +192,37 @@ def createStudentFromTracy(username=None, bnumber=None): else: raise InvalidUserException("Error: Could not get or create {0} {1}".format(tracyStudent.FIRST_NAME, tracyStudent.LAST_NAME)) -def calculateExpirationDate(): - return datetime.combine(datetime.now() + timedelta(app.config["student_confirmation_days"]),time(23, 59, 59)) -def createLaborStatusForm(student, primarySupervisor, department, term, rspFunctional): - """ - Creates a labor status form with the appropriate data passed from userInsert() in laborStatusForm.py - studentID: student's primary ID in the database AKA their B# - primarySupervisor: primary supervisor of the student - department: department the position is a part of - term: term when the position will happen - rspFunctional: a dictionary containing all the data submitted in the LSF page - returns the laborStatusForm object just created for later use in laborStatusForm.py - """ - # Changes the dates into the appropriate format for the table - startDate = datetime.strptime(rspFunctional['stuStartDate'], "%m/%d/%Y").strftime('%Y-%m-%d') - endDate = datetime.strptime(rspFunctional['stuEndDate'], "%m/%d/%Y").strftime('%Y-%m-%d') - # Creates the labor Status form - lsf = LaborStatusForm.create(termCode_id = term, - studentSupervisee_id = student.ID, - supervisor_id = primarySupervisor, - department_id = department, - jobType = rspFunctional["stuJobType"], - WLS = rspFunctional["stuWLS"], - POSN_TITLE = rspFunctional["stuPosition"], - POSN_CODE = rspFunctional["stuPositionCode"], - contractHours = rspFunctional.get("stuContractHours", None), - weeklyHours = rspFunctional.get("stuWeeklyHours", None), - startDate = startDate, - endDate = endDate, - supervisorNotes = rspFunctional["stuNotes"], - laborDepartmentNotes = rspFunctional["stuLaborNotes"], - studentName = student.legal_name + " " + student.LAST_NAME, - studentExpirationDate = calculateExpirationDate() - ) - - return lsf - - -def createOverloadFormAndFormHistory(rspFunctional, lsf, creatorID, host=None): - """ - Creates a 'Labor Status Form' and then if the request needs an overload we create - a 'Labor Overload Form'. Emails are sent based on whether the form is an 'Overload Form' - rspFunctional: a dictionary containing all the data submitted in the LSF page - lsf: stores the new instance of a labor status form - creatorID: id of the user submitting the labor status form - status: status of the labor status form (e.g. Pending, etc.) - """ - # We create a 'Labor Status Form' first, then we check to see if a 'Labor Overload Form' - # needs to be created - isOverload = rspFunctional.get("isItOverloadForm") == "True" - if isOverload: - newLaborOverloadForm = OverloadForm.create( studentOverloadReason = None, - financialAidApproved = None, - financialAidApprover = None, - financialAidReviewDate = None, - SAASApproved = None, - SAASApprover = None, - SAASReviewDate = None, - laborApproved = None, - laborApprover = None, - laborReviewDate = None) - formOverload = FormHistory.create( formID = lsf.laborStatusFormID, - historyType = "Labor Overload Form", - overloadForm = newLaborOverloadForm.overloadFormID, - createdBy = creatorID, - createdDate = date.today(), - status = "Pre-Student Approval") - email = emailHandler(formOverload.formHistoryID) - link = makeThirdPartyLink("student", host, formOverload.formHistoryID) - email.LaborOverLoadFormSubmitted(link) - - formHistory = FormHistory.create( formID = lsf.laborStatusFormID, - historyType = "Labor Status Form", - overloadForm = None, - createdBy = creatorID, - createdDate = date.today(), - status = "Pre-Student Approval") - - if not formHistory.formID.termCode.isBreak and not isOverload: - email = emailHandler(formHistory.formHistoryID) - email.laborStatusFormSubmitted() - - return formHistory - - -def emailDuringBreak(secondLSFBreak, term): - """ - Sending emails during break period - """ - if term.isBreak: - isOneLSF = json.loads(secondLSFBreak) - formHistory = FormHistory.get(FormHistory.formHistoryID == isOneLSF['formHistoryID']) - email = emailHandler(formHistory.formHistoryID) - email.laborStatusFormSubmitted() - if(len(isOneLSF["previousSupervisorNames"]) > 1): #Student has more than one lsf. Send email to both supervisors and student - email.notifyAdditionalLaborStatusFormSubmittedForBreak() - -def checkForSecondLSFBreak(termCode, student): +def createSupervisorFromTracy(username=None, bnumber=None): """ - Checks if a student has more than one labor status form submitted for them during a break term, and sends emails accordingly. + Attempts to add a supervisor from the Tracy database to the application, based on the provided username or bnumber. + + Raises InvalidUserException if this does not succeed. """ - positions = (LaborStatusForm.select() - .join(FormHistory) - .where( LaborStatusForm.termCode == termCode, - LaborStatusForm.studentSupervisee == student, - ~(FormHistory.status_id % "Denied%")) - .distinct()) - isMoreLSFDict = {} - storeLSFFormsID = [] - previousSupervisorNames = [] - if len(list(positions)) >= 1: # If student has one or more than one lsf - isMoreLSFDict["showModal"] = True # show modal when the student has one or more than one lsf - - for item in positions: - previousSupervisorNames.append(item.supervisor.FIRST_NAME + " " + item.supervisor.LAST_NAME) - isMoreLSFDict["studentName"] = item.studentSupervisee.FIRST_NAME + " " + item.studentSupervisee.LAST_NAME - isMoreLSFDict['previousSupervisorNames'] = previousSupervisorNames - - if len(list(positions)) == 1: # if there is only one labor status form then send email to the supervisor and student - laborStatusFormID = positions[0].laborStatusFormID - formHistoryID = FormHistory.get(FormHistory.formID == laborStatusFormID) - isMoreLSFDict["formHistoryID"] = formHistoryID.formHistoryID - - else: # if there are more lsfs then send email to student, supervisor and all previous supervisors - for item in positions: # add all the previous lsf ID's - storeLSFFormsID.append(item.laborStatusFormID) # store all of the previous labor status forms for break - laborStatusFormID = storeLSFFormsID.pop() #save all the previous lsf ID's except the one currently created. Pop removes the one created right now. - formHistoryID = FormHistory.get(FormHistory.formID == laborStatusFormID) - isMoreLSFDict['formHistoryID'] = formHistoryID.formHistoryID - isMoreLSFDict["lsfFormID"] = storeLSFFormsID - else: - isMoreLSFDict["showModal"] = False # Do not show the modal when there's not previous lsf - return json.dumps(isMoreLSFDict) - -def checkForPrimaryPosition(termCode, student, currentUser): - """ Checks if a student has a primary supervisor (which means they have primary position) in the selected term. """ - rsp = (request.data).decode("utf-8") # This turns byte data into a string - rspFunctional = json.loads(rsp) - term = Term.get(Term.termCode == termCode) - - termYear = termCode[:-2] - shortCode = termCode[-2:] - clauses = [] - if shortCode == '00': - fallTermCode = termYear + '11' - springTermCode = termYear + '12' - clauses.extend([FormHistory.formID.termCode == fallTermCode, - FormHistory.formID.termCode == springTermCode, - FormHistory.formID.termCode == termCode]) - else: - ayTermCode = termYear + '00' - clauses.extend([FormHistory.formID.termCode == ayTermCode, - FormHistory.formID.termCode == termCode]) - expression = reduce(operator.or_, clauses) # This expression creates SQL OR operator between the conditions added to 'clauses' list + if bnumber: + try: + tracyUser = Tracy().getSupervisorFromID(bnumber) + except InvalidQueryException as e: + raise InvalidUserException("{} not found in Tracy database".format(bnumber)) + + else: # Executes if no ID is provided + email = "{}@berea.edu".format(username) + try: + tracyUser = Tracy().getSupervisorFromEmail(email) + except InvalidQueryException as e: + raise InvalidUserException("{} not found in Tracy database".format(email)) try: - lastPrimaryPosition = FormHistory.select()\ - .join_from(FormHistory, LaborStatusForm)\ - .join_from(FormHistory, HistoryType)\ - .where((expression) & - (FormHistory.formID.studentSupervisee == student) & - (FormHistory.historyType.historyTypeName == "Labor Status Form") & - (FormHistory.formID.jobType == "Primary"))\ - .order_by(FormHistory.formHistoryID.desc())\ - .get() + return Supervisor.get(Supervisor.ID == tracyUser.ID.strip()) except DoesNotExist: - lastPrimaryPosition = None - - if not lastPrimaryPosition: - approvedRelease = None - else: - try: - approvedRelease = FormHistory.select()\ - .where(FormHistory.formID == lastPrimaryPosition.formID, - FormHistory.historyType == "Labor Release Form", - FormHistory.status == "Approved")\ - .order_by(FormHistory.formHistoryID.desc())\ - .get() - except DoesNotExist: - approvedRelease = None - - finalStatus = {} - if not term.isBreak: - if lastPrimaryPosition and not approvedRelease: - if rspFunctional == "Primary": - if "Denied" in lastPrimaryPosition.status.statusName: # handle two denied statuses - finalStatus["status"] = "hire" - else: - finalStatus["status"] = "noHire" - finalStatus["term"] = lastPrimaryPosition.formID.termCode.termName - finalStatus["primarySupervisor"] = lastPrimaryPosition.formID.supervisor.FIRST_NAME + " " +lastPrimaryPosition.formID.supervisor.LAST_NAME - finalStatus["department"] = lastPrimaryPosition.formID.department.DEPT_NAME + " (" + lastPrimaryPosition.formID.department.ORG + "-" + lastPrimaryPosition.formID.department.ACCOUNT+ ")" - finalStatus["position"] = lastPrimaryPosition.formID.POSN_CODE +" - "+lastPrimaryPosition.formID.POSN_TITLE + " (" + lastPrimaryPosition.formID.WLS + ")" - finalStatus["hours"] = lastPrimaryPosition.formID.jobType + " (" + str(lastPrimaryPosition.formID.weeklyHours) + ")" - finalStatus["isLaborAdmin"] = currentUser.isLaborAdmin - if lastPrimaryPosition.status.statusName == "Approved" or lastPrimaryPosition.status.statusName == "Approved Reluctantly": - finalStatus["approvedForm"] = True - else: # New form is secondary - if lastPrimaryPosition.status_id in ["Approved", "Approved Reluctantly", "Pending"]: - lastPrimaryPositionTermCode = str(lastPrimaryPosition.formID.termCode.termCode)[-2:] - # if selected term is AY and student has an approved/pending LSF in spring or fall - if shortCode == '00' and lastPrimaryPositionTermCode in ['11', '12']: - finalStatus["status"] = "noHireForSecondary" - else: - finalStatus["status"] = "hire" - else: - finalStatus["status"] = "noHireForSecondary" - - elif lastPrimaryPosition and approvedRelease: - if rspFunctional == "Primary": - finalStatus["status"] = "hire" - else: - finalStatus["status"] = "noHireForSecondary" - else: - if rspFunctional == "Primary": - finalStatus["status"] = "hire" - else: - finalStatus["status"] = "noHireForSecondary" - else: - finalStatus["status"] = "hire" - return json.dumps(finalStatus) + return Supervisor.create(PIDM = tracyUser.PIDM, + legal_name = tracyUser.FIRST_NAME, + LAST_NAME = tracyUser.LAST_NAME, + ID = tracyUser.ID.strip(), + EMAIL = tracyUser.EMAIL, + CPO = tracyUser.CPO, + ORG = tracyUser.ORG, + DEPT_NAME = tracyUser.DEPT_NAME) + except Exception as e: + print(e) + raise InvalidUserException("Error: Could not get or create {0} {1}".format(tracyUser.FIRST_NAME, tracyUser.LAST_NAME)) diff --git a/app/logic/utils.py b/app/logic/utils.py index 4e3c5a876..9bb693958 100644 --- a/app/logic/utils.py +++ b/app/logic/utils.py @@ -1,6 +1,8 @@ -from flask import session, request +from datetime import datetime, date, timedelta, time +from app.models.user import * +from flask import session, request, flash from urllib.parse import urlparse - +from datetime import datetime, timedelta def makeThirdPartyLink(recipient, host, formHistoryId): route = "" @@ -15,3 +17,16 @@ def makeThirdPartyLink(recipient, host, formHistoryId): def setReferrerPath(): session['referrerPath'] = urlparse(request.referrer).path or '' + + +def adminFlashMessage(user, action, adminType): + message = "{} has been {} as a {} Admin".format(user.fullName, action, adminType) + + if action == 'added': + flash(message, "success") + elif action == 'removed': + flash(message, "danger") + +# This function calculates the expiration date for a student confirmation and the total date based on the number of days set in the secret config from now at 11:59:59 PM +def calculateExpirationDate(): + return datetime.combine(datetime.now() + timedelta(app.config["student_confirmation_days"]),time(23, 59, 59)) diff --git a/app/login_manager.py b/app/login_manager.py index 017a70450..49f67016d 100755 --- a/app/login_manager.py +++ b/app/login_manager.py @@ -3,7 +3,8 @@ from app.controllers.errors_routes.handlers import * from app.models.user import User, DoesNotExist from app.models.term import Term -from app.logic.userInsertFunctions import createUser, createSupervisorFromTracy, createStudentFromTracy, InvalidUserException, updateUserFromTracy +from app.logic.userInsertFunctions import createSupervisorFromTracy, createStudentFromTracy, updateUserFromTracy, createUser +from app.logic.tracy import InvalidUserException def getUsernameFromEnv(env): envK = "eppn" @@ -24,7 +25,7 @@ def logout(): url ="/" if app.config['use_shibboleth']: - url = "/Shibboleth.sso/Logout" + url = "/Shibboleth.sso/Logout?return=https://login.berea.edu/idp/profile/Logout" return url def require_login(): diff --git a/app/models/laborReleaseForm.py b/app/models/laborReleaseForm.py index eb9a71cac..bbfef8127 100755 --- a/app/models/laborReleaseForm.py +++ b/app/models/laborReleaseForm.py @@ -6,7 +6,7 @@ class LaborReleaseForm (baseModel): conditionAtRelease = CharField(null=False) # Performance (satisfactory or unsatisfactory) releaseDate = DateField(null=False) # Can be tomorrow's date or future date, never past date reasonForRelease = CharField(null=False) - contactPerson = ForeignKeyField(User) + contactPerson = ForeignKeyField(User, null=True) diff --git a/app/models/laborStatusForm.py b/app/models/laborStatusForm.py index 0e5488caf..96125006c 100755 --- a/app/models/laborStatusForm.py +++ b/app/models/laborStatusForm.py @@ -5,7 +5,7 @@ from app.models.department import Department from app.models.supervisor import Supervisor from uuid import uuid4 - +from datetime import date, datetime # All caps fields are pulled from TRACY class LaborStatusForm (baseModel): studentName = CharField(null=True) @@ -27,8 +27,14 @@ class LaborStatusForm (baseModel): studentConfirmation = BooleanField(null=True) # Pending is None, Accepted is True, Denied is False confirmationToken = UUIDField(default=uuid4, null=True) studentExpirationDate = DateField(null=True) - studentResponseDate = DateTimeField(null=True) - + studentResponseDate = DateTimeField(null=True) def __str__(self): return str(self.__dict__) + + @property + def isExpired(self): + exp = self.studentExpirationDate + if not exp: + return False + return exp < date.today() \ No newline at end of file diff --git a/app/static/js/adminManagement.js b/app/static/js/adminManagement.js index 186747457..f4c13b1e7 100755 --- a/app/static/js/adminManagement.js +++ b/app/static/js/adminManagement.js @@ -43,56 +43,45 @@ function liveSearch(selectPickerID, e) { } }); } + else{$("#"+ selectPickerID).selectpicker("refresh");} // clear search list if <3 chars }; +let formToSubmit = null; // store form clicked function modal(button) { - if(button == "add" && $("#addlaborAdmin").val() != "") { - $("h2").html("Labor Administrators"); - $("p").html("Are you sure you want to add " + $("#addlaborAdmin option:selected").text() + " as a Labor Administrator?"); - document.getElementById("submitModal").setAttribute("name", "add"); - document.getElementById("submitModal").setAttribute("value", "add"); - $("#modal").modal("show"); - } - else if (button == "add1" && $("#addFinAidAdmin").val() != "") { - $("h2").html("Financial Aid Administrators"); - $("p").html("Are you sure you want to add " + $("#addFinAidAdmin option:selected").text() + " as a Financial Aid Administrator?"); - document.getElementById("submitModal").setAttribute("name", "addAid"); - document.getElementById("submitModal").setAttribute("value", "addAid"); - $("#modal").modal("show"); - } - else if (button == "add2" && $("#addSaasAdmin").val() != "") { - $("h2").html("SAAS Administrators"); - $("p").html("Are you sure you want to add " + $("#addSaasAdmin option:selected").text() + " as a SAAS Administrator?"); - document.getElementById("submitModal").setAttribute("name", "addSaas"); - document.getElementById("submitModal").setAttribute("value", "addSaas"); - $("#modal").modal("show"); - } - else if (button == "remove" && $("#removelaborAdmin").val() != "") { - $("h2").html("Labor Administrators"); - $("p").html("Are you sure you want to remove " + $("#removelaborAdmin option:selected").text() + " as a Labor Administrator?"); - document.getElementById("submitModal").setAttribute("name", "remove"); - document.getElementById("submitModal").setAttribute("value", "remove"); - $("#modal").modal("show"); - } - else if (button == "remove1" && $("#removeFinAidAdmin").val() != "") { - $("h2").html("Financial Aid Administrators"); - $("p").html("Are you sure you want to remove " + $("#removeFinAidAdmin option:selected").text() + " as a Financial Aid Administrator?"); - document.getElementById("submitModal").setAttribute("name", "removeAid"); - document.getElementById("submitModal").setAttribute("value", "removeAid"); - $("#modal").modal("show"); - } - else if (button == "remove2" && $("#removeSaasAdmin").val() != "") { - $("h2").html("SAAS Administrators"); - $("p").html("Are you sure you want to remove " + $("#removeSaasAdmin option:selected").text() + " as a SAAS Administrator?"); - document.getElementById("submitModal").setAttribute("name", "removeSaas"); - document.getElementById("submitModal").setAttribute("value", "removeSaas"); - $("#modal").modal("show"); + // map button IDs to {selectID, formID, header, actionText, key} + const formButtons = { + add: { select: "#addlaborAdmin", form: "#laborAdminForm", header: "Labor Administrators", action: "add", noun: "Labor Administrator", key: "addLaborAdmin" }, + add1: { select: "#addFinAidAdmin", form: "#finAidAdminForm", header: "Financial Aid Administrators", action: "add", noun: "Financial Aid Administrator", key: "addFinAidAdmin" }, + add2: { select: "#addSaasAdmin", form: "#SAASAdminForm", header: "SAAS Administrators", action: "add", noun: "SAAS Administrator", key: "addSaasAdmin" }, + remove:{ select: "#removelaborAdmin", form: "#laborAdminForm", header: "Labor Administrators", action: "remove", noun: "Labor Administrator", key: "removeLaborAdmin" }, + remove1:{ select: "#removeFinAidAdmin", form: "#finAidAdminForm", header: "Financial Aid Administrators", action: "remove", noun: "Financial Aid Administrator", key: "removeFinAidAdmin" }, + remove2:{ select: "#removeSaasAdmin", form: "#SAASAdminForm", header: "SAAS Administrators", action: "remove", noun: "SAAS Administrator", key: "removeSaasAdmin" } + }; + + const adminForm = formButtons[button]; + const select = $(adminForm.select); + + if (!select.val()) { + // no user selected + msgFlash("Please select a user", "warning"); + return; } - else { - category = "danger" - msg = "Please select a user."; - $("#flash_container").html('') - $("#flasher").delay(3000).fadeOut() + + $(adminForm.form).find("#formActionField").val(adminForm.key); // set hidden field value + formToSubmit = $(adminForm.form); + + const selectedText = select.find("option:selected").text(); + const message = `Are you sure you want to ${adminForm.action} ${selectedText} as a ${adminForm.noun}?`; + + // populate modal + $("#adminModalHeader").html(adminForm.header); + $("#adminModalText").html(message); + $("#modal").modal("show"); +} + +$("#submitModal").on("click", function () { + if (formToSubmit) { + formToSubmit.submit(); } -}; +}); \ No newline at end of file diff --git a/app/static/js/allPendingForms.js b/app/static/js/allPendingForms.js index f8068870c..fda836f08 100644 --- a/app/static/js/allPendingForms.js +++ b/app/static/js/allPendingForms.js @@ -146,16 +146,8 @@ function finalApproval() { //this method changes the status of the lsf from pend $("#approveModalButton").text("Approve"); $("#approvalModal").data("bs.modal").options.backdrop = true; $("#approvalModal").data("bs.modal").options.keyboard = true; - - - // Try and catch is used here to prevent General Search page from reloading the entire the page. - try { - runformSearchQuery(); - $('#approvalModal').modal('hide'); - } - catch(e){ - location.reload(true); - } + + location.reload(true); } } }); @@ -463,9 +455,6 @@ function submitOverload(formHistoryID, isLaborAdmin) { if ($('#approve').is(':checked')) { status = 'Approved'; } - if ($('#approveRel').is(':checked')) { - status = 'Approved Reluctantly' - } if ($('#overloadNotes').val() != '') { var adminNotes = $('#overloadNotes').val() overloadModalInfo['adminNotes'] = adminNotes; diff --git a/app/static/js/laborhistory.js b/app/static/js/laborhistory.js index 4a25de169..cf6868bb8 100755 --- a/app/static/js/laborhistory.js +++ b/app/static/js/laborhistory.js @@ -17,19 +17,12 @@ $('#positionTable tbody tr td').on('click',function(){ function loadFormHistoryModal(formHistory) { $("#modal").modal("show"); - $("#modal").find('.modal-content').load('/laborHistory/modal/' + formHistory); - setTimeout(function(){ $(".loader").fadeOut("slow"); }, 500); -} - -function redirection(laborStatusKey){ - /* - When any of the three buttons is clicked, this function will append the 'href' attribute with the - correct redirection link and LSF primary key to each button - */ - $("#alter").attr("href", "/alterLSF/" + laborStatusKey); // will go to the alterLSF controller - $("#rehire").attr("href", "/laborstatusform/" + laborStatusKey); // will go to the lsf controller - $("#release").attr("href", "/laborReleaseForm/" + laborStatusKey); // will go to labor release form controller - $("#sle").attr("href", "/sle/" + laborStatusKey); // will go to student labor evaluations controller + $(".loader").show(); + + // This function is called when the modal content is loaded + $("#modal").find('.modal-content').load('/laborHistory/modal/' + formHistory, function() { + $(".loader").fadeOut("slow"); // Hide the loader after content is loaded + }); } $("#modal").on('transitionend', function(){ //Had to search for css element visibility changes to make this work @@ -116,3 +109,16 @@ function withdrawform(formID){ } }); } +function resubmitform(formHistoryId){ + $.ajax({ + method: "GET", + url: '/lsf/' + formHistoryId + '/submitToBanner', + contentType: 'application/json', + success: function(response) { + msgFlash(response,"success"); + }, + error: function(xhr) { + msgFlash(xhr.responseText,"fail"); + } + }); +}; diff --git a/app/static/js/supervisorPortal.js b/app/static/js/supervisorPortal.js index e90d57c2b..a4810ebb4 100644 --- a/app/static/js/supervisorPortal.js +++ b/app/static/js/supervisorPortal.js @@ -1,4 +1,14 @@ $(document).ready(function () { + var supervisorOption = $('#supervisorSelect option[data-preloaded="current"]'); + + g_currentUserOption = { + value: supervisorOption.val(), + text: supervisorOption.text(), + 'data-content': supervisorOption.attr('data-content') + }; + + supervisorOption.remove(); + $('#formSearchButton').on('click', function () { runFormSearchQuery(); $('#sortOptions').show(); @@ -14,7 +24,7 @@ $(document).ready(function () { runFormSearchQuery(); $('#sortOptions').show(); }); - + $('#addUserToDept').on('click', function () { $("#addSupervisorToDeptModal").modal("show"); $('#addUser').prop('disabled', true) @@ -39,6 +49,7 @@ $(document).ready(function () { addSupervisorToDepartment(supervisorID, departmentID) }) + $('#departmentModalSelect').on('change', disableButtonHandler) $('#supervisorModalSelect').on('change', disableButtonHandler) @@ -52,6 +63,7 @@ $(document).ready(function () { collapsible: true }); }); + $(function () { $("#formSearchAccordion").accordion(); $("#formSearchAccordion .ui-accordion-header").css({ fontSize: 20 });// width of the box content area @@ -61,18 +73,17 @@ $(document).ready(function () { $("input:checkbox").removeAttr("checked"); runFormSearchQuery("mySupervisees"); }); + $('#superviseesPendingForms').on('click', function () { $("input:checkbox").removeAttr("checked"); runFormSearchQuery("pendingForms"); }); - $('#currentTerm').on('click', function () { - runFormSearchQuery("currentTerm"); - }); + $('#columnPicker').on('change', function () { let column = $('#columnPicker :selected').text() buttonVal = $("#switchViewButton").val() let fields = buttonVal == "advanced" ? advancedColumnFieldMap[column] : simpleColumnFieldMap[column]; - + // clear the options from the current field picker and replace // them with the ones from the columnFieldMap $('#fieldPicker').empty(); @@ -109,6 +120,7 @@ $(document).ready(function () { fetchSimpleView(cookieStr) switchViewButton('simple') } + loadSavedSearchOptions(cookieJSON) setFormSearchValues(cookieJSON) } else { @@ -121,86 +133,104 @@ $(document).ready(function () { $('#mySupervisees').trigger("click") } + // Live search handling for dropdowns + $("#termSelectParent .bs-searchbox input").on("keyup", debouncedSearchTerm); + $("#departmentSelectParent .bs-searchbox input").on("keyup", debouncedSearchDepartment); + $("#supervisorSelectParent .bs-searchbox input").on("keyup", debouncedSearchSupervisor); + $("#studentSelectParent .bs-searchbox input").on("keyup", debouncedSearchStudent); + }); +function debounce(func, delay) { + let timeout; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => { + func.apply(this, args); + }, delay); + }; +} + +const debouncedSearchTerm = debounce(function(e) { + liveSearch("termSelect", e); +}, 500); +const debouncedSearchDepartment = debounce(function(e) { + liveSearch("departmentSelect", e); +}, 500); +const debouncedSearchSupervisor = debounce(function(e) { + liveSearch("supervisorSelect", e); +}, 500); +const debouncedSearchStudent = debounce(function(e) { + liveSearch("studentSelect", e); +}, 500); + // this is a mapping which maps the column option to its field options. // many do not have multiple fields so the field is just the column itself (e.g. term) const advancedColumnFieldMap = { - 'Term': [['Term', 'term']], - 'Department': [['Department', 'department']], - 'Supervisor': [['First name', 'supervisorFirstName'], ['Last Name', 'supervisorLastName']], - 'Student': [['First name', 'studentFirstName'], ['Last Name', 'studentLastName']], - 'Position (WLS)': [['WLS', 'positionWLS'], ['Position Type', 'positionType'], ['Position Title', 'positionTitle']], - 'Length': [['Length', 'length']], - 'Created By': [['Created By', 'createdBy']], - 'Form Type (Status)': [['Form Type', 'formType'], ['Status', 'formStatus']] +'Term': [['Term', 'term']], +'Department': [['Department', 'department']], +'Supervisor': [['First name', 'supervisorFirstName'], ['Last Name', 'supervisorLastName']], +'Student': [['First name', 'studentFirstName'], ['Last Name', 'studentLastName']], +'Position (WLS)': [['WLS', 'positionWLS'], ['Position Type', 'positionType'], ['Position Title', 'positionTitle']], +'Length': [['Length', 'length']], +'Created By': [['Created By', 'createdBy']], +'Form Type (Status)': [['Form Type', 'formType'], ['Status', 'formStatus']] }; const simpleColumnFieldMap = { - 'Term': [['Term', 'term']], - 'Department': [['Department', 'department']], - 'Student': [['First name', 'studentFirstName'], ['Last Name', 'studentLastName']], - 'Position': [['Position Type', 'positionType'], ['Position Title', 'positionTitle']], - 'Form Status': [['Status', 'formStatus']] +'Term': [['Term', 'term']], +'Department': [['Department', 'department']], +'Student': [['First name', 'studentFirstName'], ['Last Name', 'studentLastName']], +'Position': [['Position Type', 'positionType'], ['Position Title', 'positionTitle']], +'Form Status': [['Status', 'formStatus']] }; function disableButtonHandler() { - if ($('#departmentModalSelect :selected').val() == "" || $('#supervisorModalSelect :selected').val() == "") { - $('#addUser').prop('disabled', true) - } - else { - $('#addUser').prop('disabled', false) - } +if ($('#departmentModalSelect :selected').val() == "" || $('#supervisorModalSelect :selected').val() == "") { +$('#addUser').prop('disabled', true) +} +else { +$('#addUser').prop('disabled', false) +} } function runFormSearchQuery(button) { - let currentView = $('#switchViewButton').val() - let termCode, departmentID, supervisorID, studentID; - let formStatusList = []; - let formTypeList = []; - var isDisabled = $('#fieldPicker').prop('disabled'); - let sortBy = $('#fieldPicker').val() - - - // if the fieldPicker is disabled that means we should take the value - // from the columnPicker instead - if (isDisabled) { - sortBy = $('#columnPicker').val() - } - let order = $('#orderPicker').val() - - switch (button) { - case "mySupervisees": - termCode = "currentTerm" - departmentID = "" - supervisorID = "currentUser" - studentID = "" - formStatusList = ["Approved", "Approved Reluctantly"] +let currentView = $('#switchViewButton').val() +let termCode, departmentID, supervisorID, studentID; +let formStatusList = []; +let formTypeList = []; +var isDisabled = $('#fieldPicker').prop('disabled'); +let sortBy = $('#fieldPicker').val() + + +// if the fieldPicker is disabled that means we should take the value +// from the columnPicker instead +if (isDisabled) { +sortBy = $('#columnPicker').val() +} +let order = $('#orderPicker').val() + +switch (button) { +case "mySupervisees": + termCode = "activeTerms" + departmentID = "" + supervisorID = "currentUser" + studentID = "" + formStatusList = ["Approved"] if (currentView == "simple") { // avoid duplicates in the table formTypeList = ["Labor Status Form"] } break; case "pendingForms": - termCode = "currentTerm" + termCode = "activeTerms" departmentID = "" supervisorID = "currentUser" studentID = "" formStatusList = ["Pending", "Pre-Student Approval"] break; - case "currentTerm": - termCode = "currentTerm" - departmentID = "" - supervisorID = "" - studentID = "" - formStatusList = [] - if (currentView == "simple") { // avoid duplicates in the table - formTypeList = ["Labor Status Form"] - } - break; - default: termCode = $("#termSelect").val(); departmentID = $("#departmentSelect").val(); @@ -213,14 +243,34 @@ function runFormSearchQuery(button) { queryDict = { 'view': currentView, 'termCode': termCode, + 'termOption': { + value: termCode, + text: $('#termSelect option:selected').text() + }, 'departmentID': departmentID, + 'departmentOption': { + value: departmentID, + text: $('#departmentSelect option:selected').text(), + 'data-content': $('#departmentSelect option:selected').attr('data-content') + }, 'supervisorID': supervisorID, + 'supervisorOption': { + value: supervisorID, + text: $('#supervisorSelect option:selected').text(), + 'data-content': $('#supervisorSelect option:selected').attr('data-content') + }, 'studentID': studentID, + 'studentOption': { + value: studentID, + text: $('#studentSelect option:selected').text(), + 'data-content': $('#studentSelect option:selected').attr('data-content') + }, 'formStatus': formStatusList, 'formType': formTypeList, 'sortBy': sortBy, 'order': order }; + setFormSearchValues(queryDict) data = JSON.stringify(queryDict) @@ -350,10 +400,36 @@ function updateDownloadButton(response){ } } +function loadSavedSearchOptions(cookies) { + const selectMap = { + 'termOption': 'termSelect', + 'supervisorOption': 'supervisorSelect', + 'studentOption': 'studentSelect', + 'departmentOption': 'departmentSelect' + }; + + const skipValues = ['', 'currentUser', 'activeTerms']; + + Object.keys(selectMap).forEach(optionKey => { + const selectId = selectMap[optionKey]; + const option = cookies[optionKey]; + + // Check if option exists and has a valid value + if (option && option.value && !skipValues.includes(option.value)) { + // Check if option doesn't already exist in the select + if ($(`#${selectId} option[value="${option.value}"]`).length === 0) { + $(`#${selectId}`).append($("