diff --git a/app/controllers/admin_routes/__init__.py b/app/controllers/admin_routes/__init__.py index ca116a498..161824bd7 100644 --- a/app/controllers/admin_routes/__init__.py +++ b/app/controllers/admin_routes/__init__.py @@ -20,3 +20,4 @@ def injectGlobalData(): from app.controllers.admin_routes import financialAidOverload from app.controllers.admin_routes import emailTemplateController from app.controllers.admin_routes import search +from app.controllers.admin_routes import viewPositionDescriptions diff --git a/app/controllers/admin_routes/viewPositionDescriptions.py b/app/controllers/admin_routes/viewPositionDescriptions.py new file mode 100644 index 000000000..80ba2c483 --- /dev/null +++ b/app/controllers/admin_routes/viewPositionDescriptions.py @@ -0,0 +1,31 @@ +from app.controllers.admin_routes import * +from app.models.user import User +from app.controllers.admin_routes import admin +from app.login_manager import require_login +from app.models.term import Term +from app.models.positionDescription import * +from app.models.positionDescriptionItem import * +from app.models.position import * +from datetime import datetime +from flask import json, jsonify +from flask import request, redirect +from peewee import IntegrityError + +@admin.route('/admin/viewPositionDescriptions', methods=['GET', 'POST']) +# @login_required + +def viewPositionDescriptions(): + currentUser = require_login() + if not currentUser: # Not logged in + return render_template('errors/403.html') + if not currentUser.isLaborAdmin: # Not an admin + if currentUser.student: # logged in as a student + return redirect('/laborHistory/' + currentUser.student.ID) + elif currentUser.supervisor: + return render_template('errors/403.html'), 403 + + pendingPositionDescriptions = PositionDescription.select().where(PositionDescription.status == "Pending") + return render_template( 'admin/viewPositionDescriptions.html', + title='Position Descriptions', + pendingPositionDescriptions = pendingPositionDescriptions + ) diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 1cc229b63..f9515081c 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -22,3 +22,5 @@ def injectGlobalData(): from app.controllers.main_routes import download from app.controllers.main_routes import laborReleaseForm from app.controllers.main_routes import contributors +from app.controllers.main_routes import positionDescription +from app.controllers.main_routes import positionDescriptionEdit diff --git a/app/controllers/main_routes/positionDescription.py b/app/controllers/main_routes/positionDescription.py new file mode 100644 index 000000000..4907afddd --- /dev/null +++ b/app/controllers/main_routes/positionDescription.py @@ -0,0 +1,100 @@ +from flask_login import login_required +from app.controllers.main_routes import * +from app.login_manager import require_login +from app.models.user import * +from app.models.formHistory import * +from app.models.positionDescription import * +from app.models.positionDescriptionItem import * +from app.models.position import * +from flask import json, jsonify +from flask import request +from datetime import datetime, date, timedelta +from flask import Flask, redirect, url_for, flash +from app import cfg +from app.logic.emailHandler import* +from app.logic.userInsertFunctions import* + +@main_bp.route('/positionDescriptions', methods=['GET']) +def PositionDescriptionView(): + """ Render Position Description Form""" + currentUser = require_login() + if not currentUser: # Not logged in + return render_template('errors/403.html'), 403 + if not currentUser.isLaborAdmin: + if currentUser.student and not currentUser.supervisor: + return redirect('/laborHistory/' + currentUser.student.ID) + if not currentUser.student and currentUser.supervisor: + # Checks all the forms where the current user has been the creator or the supervisor, and grabs all the departments associated with those forms. Will only grab each department once. + departments = FormHistory.select(FormHistory.formID.department.DEPT_NAME, FormHistory.formID.department.ACCOUNT, FormHistory.formID.department.ORG) \ + .join_from(FormHistory, LaborStatusForm) \ + .join_from(LaborStatusForm, Department) \ + .where((FormHistory.formID.supervisor == currentUser.supervisor.ID) | (FormHistory.createdBy == currentUser)) \ + .order_by(FormHistory.formID.department.DEPT_NAME.asc()) \ + .distinct() + + if currentUser.isLaborAdmin: + # Grabs every single department that currently has at least one labor status form in it + departments = FormHistory.select(FormHistory.formID.department.DEPT_NAME, FormHistory.formID.department.ACCOUNT, FormHistory.formID.department.ORG) \ + .join_from(FormHistory, LaborStatusForm) \ + .join_from(LaborStatusForm, Department) \ + .order_by(FormHistory.formID.department.DEPT_NAME.asc()) \ + .distinct() + + # Logged in + todayDate = date.today() + openTerms = Term.select().where(Term.termEnd > todayDate) + closedTerms = Term.select().where(Term.termEnd < todayDate) + + return render_template( 'main/positionDescription.html', + title=('Position Description'), + UserID = currentUser, + openTerms = openTerms, + closedTerms = closedTerms, + departments = departments) + +@main_bp.route("/positionDescriptions/getPositions//", methods=['GET']) +def getDepartmentPositions(departmentOrg, departmentAcct): + """ Get all of the positions that are in the selected department """ + positions = Tracy().getPositionsFromDepartment(departmentOrg,departmentAcct) + positionDict = {} + for position in positions: + positionDict[position.POSN_CODE] = {"position": position.POSN_TITLE, "WLS":position.WLS, "positionCode":position.POSN_CODE} + return json.dumps(positionDict) + +@main_bp.route("/positionDescriptions/getVersions", methods=['POST']) +def getVersions(): + """ Get all of the positions that are in the selected department """ + try: + rsp = eval(request.data.decode("utf-8")) + returnDict = {} + versions = PositionDescription.select().where(PositionDescription.POSN_CODE == rsp["POSN_CODE"]) + for version in versions: + if not version.endDate: + returnDict[version.positionDescriptionID] = {"createdDate": version.createdDate.strftime('%m/%d/%y'), "endDate": "None", "status": version.status.statusName} + else: + returnDict[version.positionDescriptionID] = {"createdDate": version.createdDate.strftime('%m/%d/%y'), "endDate": version.endDate.strftime('%m/%d/%y'), "status": version.status.statusName} + return jsonify(returnDict) + except Exception as e: + print ("ERROR", e) + +@main_bp.route("/positionDescriptions/getPositionDescription", methods=['POST']) +def getDescription(): + """ Get all of the positions that are in the selected department """ + try: + rsp = eval(request.data.decode("utf-8")) + returnList = [] + positionDescriptionQualifications = PositionDescriptionItem.select().where((PositionDescriptionItem.itemType == "Qualification") & (PositionDescriptionItem.positionDescription == rsp["positionDescriptionID"])) + positionDescriptionLearningOBJ = PositionDescriptionItem.select().where((PositionDescriptionItem.positionDescription == rsp["positionDescriptionID"]) & (PositionDescriptionItem.itemType == "Learning Objective")) + positionDescriptionDuty = PositionDescriptionItem.select().where((PositionDescriptionItem.positionDescription == rsp["positionDescriptionID"]) & (PositionDescriptionItem.itemType == "Duty")) + returnList.append("

Qualifications

") + for item in positionDescriptionQualifications: + returnList.append("

" + item.itemDescription + "

") + returnList.append("

Learning Objectives

") + for item in positionDescriptionLearningOBJ: + returnList.append("

" + item.itemDescription + "

") + returnList.append("

Duties

") + for item in positionDescriptionDuty: + returnList.append("

" + item.itemDescription + "

") + return jsonify(returnList) + except Exception as e: + print ("ERROR", e) diff --git a/app/controllers/main_routes/positionDescriptionEdit.py b/app/controllers/main_routes/positionDescriptionEdit.py new file mode 100644 index 000000000..cafc205b2 --- /dev/null +++ b/app/controllers/main_routes/positionDescriptionEdit.py @@ -0,0 +1,152 @@ +from flask_login import login_required +from app.controllers.main_routes import * +from app.login_manager import require_login +from app.models.user import * +from app.models.formHistory import * +from app.models.position import * +from app.models.positionDescription import * +from app.models.positionDescriptionItem import * +from app.models.position import * +from flask import json, jsonify +from flask import request +from datetime import datetime, date, timedelta +from flask import Flask, redirect, url_for, flash +from app import cfg +from app.logic.emailHandler import* +from app.logic.userInsertFunctions import* + +@main_bp.route('/positionDescriptionEdit/', methods=['GET']) +@main_bp.route('/positionDescriptionEdit/newVersion/', methods=['GET']) +def PositionDescriptionEdit(positionDescriptionID = None, positionCode = None): + """ Render Position Description Form""" + currentUser = require_login() + if not currentUser: # Not logged in + return render_template('errors/403.html'), 403 + if not currentUser.isLaborAdmin: + if currentUser.student and not currentUser.supervisor: + return redirect('/laborHistory/' + currentUser.student.ID) + + if positionDescriptionID: + positionDescriptionItems = PositionDescriptionItem.select().where(PositionDescriptionItem.positionDescription == positionDescriptionID) + positionDescriptionRecord = PositionDescription.select().where(PositionDescription.positionDescriptionID == positionDescriptionID).get() + positionRecord = Position.select().where(Position.POSN_CODE == positionDescriptionRecord.POSN_CODE.POSN_CODE).get() + elif positionCode: + positionDescriptionItems = None + positionDescriptionRecord = None + positionRecord = Position.select().where(Position.POSN_CODE == positionCode).get() + + pendingPositionDescription = PositionDescription.select().where(PositionDescription.POSN_CODE == positionRecord.POSN_CODE) + + if not currentUser.isLaborAdmin: + # Checks all the forms where the current user has been the creator or the supervisor, and grabs all the departments associated with those forms. Will only grab each department once. + if pendingPositionDescription: + for record in pendingPositionDescription: + if record.status.statusName == "Pending": + return render_template('errors/403.html'), 403 + + authorizedUser = False + departments = FormHistory.select(FormHistory.formID.department.DEPT_NAME, FormHistory.formID.department.ACCOUNT, FormHistory.formID.department.ORG) \ + .join_from(FormHistory, LaborStatusForm) \ + .join_from(LaborStatusForm, Department) \ + .where((FormHistory.formID.supervisor == currentUser.supervisor.ID) | (FormHistory.createdBy == currentUser)) \ + .order_by(FormHistory.formID.department.DEPT_NAME.asc()) \ + .distinct() + for department in departments: + if department.formID.department.DEPT_NAME == positionRecord.DEPT_NAME: + authorizedUser = True + break + if not authorizedUser: + return render_template('errors/403.html'), 403 + + itemTypes = ['Learning Objective', 'Qualification', 'Duty'] + + return render_template( 'main/positionDescriptionEdit.html', + showModal=True, + title=('Position Description'), + UserID = currentUser, + positionDescriptionItems = positionDescriptionItems, + itemTypes = itemTypes, + positionDescriptionRecord = positionDescriptionRecord, + positionRecord = positionRecord) + +@main_bp.route("/positionDescriptionEdit/submitRevisions", methods=['POST']) +def submitRevisions(): + """ Get all of the positions that are in the selected department """ + try: + currentUser = require_login() + rsp = eval(request.data.decode("utf-8")) + position = Position.select().where(Position.POSN_CODE == rsp["positionCode"]).get() + positionDescription = PositionDescription.create( createdBy = currentUser, + status = "Pending", + POSN_CODE = rsp["positionCode"], + createdDate = date.today() + ) + for duty in rsp["duties"]: + PositionDescriptionItem.create( positionDescription = positionDescription , + itemDescription = duty, + itemType = "Duty" + ) + for qualification in rsp["qualifications"]: + PositionDescriptionItem.create( positionDescription = positionDescription , + itemDescription = qualification, + itemType = "Qualification" + ) + for learningObjective in rsp["learningObjectives"]: + PositionDescriptionItem.create( positionDescription = positionDescription , + itemDescription = learningObjective, + itemType = "Learning Objective" + ) + message = "Your position description revision for {0} ({1}) - {2} has been submited.".format(position.POSN_TITLE, position.WLS, position.POSN_CODE) + flash(message, "success") + return jsonify({"Success":True}) + except Exception as e: + print ("ERROR", e) + return jsonify({"Success": False}) + +@main_bp.route("/positionDescriptionEdit/adminUpdate", methods=['POST']) +def adminUpdate(): + """ Get all of the positions that are in the selected department """ + try: + currentUser = require_login() + rsp = eval(request.data.decode("utf-8")) + position = PositionDescription.select().where(PositionDescription.positionDescriptionID == rsp["recordID"]).get() + if rsp["adminChoice"] == "Deny": + position.status = "Denied" + position.save() + message = "The position description revision for {0} ({1}) - {2} has been denied.".format(position.POSN_CODE.POSN_TITLE, position.POSN_CODE.WLS, position.POSN_CODE.POSN_CODE) + messageType = "danger" + elif rsp["adminChoice"] == "Approve": + descriptionItems = PositionDescriptionItem.select().where(PositionDescriptionItem.positionDescription == position.positionDescriptionID) + try: + lastPositionDescription = PositionDescription.select().where((PositionDescription.POSN_CODE == position.POSN_CODE) & (PositionDescription.status == "Approved")).order_by(PositionDescription.createdDate.desc())[0] + except: + lastPositionDescription = None + if lastPositionDescription: + lastPositionDescription.endDate = date.today() + lastPositionDescription.save() + position.status = "Approved" + position.save() + for item in descriptionItems: + item.delete_instance() + for duty in rsp["duties"]: + PositionDescriptionItem.create( positionDescription = position.positionDescriptionID, + itemDescription = duty, + itemType = "Duty" + ) + for qualification in rsp["qualifications"]: + PositionDescriptionItem.create( positionDescription = position.positionDescriptionID, + itemDescription = qualification, + itemType = "Qualification" + ) + for learningObjective in rsp["learningObjectives"]: + PositionDescriptionItem.create( positionDescription = position.positionDescriptionID, + itemDescription = learningObjective, + itemType = "Learning Objective" + ) + message = "The position description revision for {0} ({1}) - {2} has been approved.".format(position.POSN_CODE.POSN_TITLE, position.POSN_CODE.WLS, position.POSN_CODE.POSN_CODE) + messageType = "success" + flash(message, messageType) + return jsonify({"Success":True}) + except Exception as e: + print ("ERROR On Admin Position Description Update:", e) + return jsonify({"Success": False}) diff --git a/app/models/laborStatusForm.py b/app/models/laborStatusForm.py index 7178ba278..1c07501e8 100755 --- a/app/models/laborStatusForm.py +++ b/app/models/laborStatusForm.py @@ -4,6 +4,7 @@ from app.models.user import User from app.models.department import Department from app.models.supervisor import Supervisor +from app.models.positionDescription import PositionDescription # All caps fields are pulled from TRACY @@ -14,17 +15,17 @@ class LaborStatusForm (baseModel): studentSupervisee = ForeignKeyField(Student, on_delete="cascade") # foreign key to student supervisor = ForeignKeyField(Supervisor, on_delete="cascade") # foreign key to supervisor department = ForeignKeyField(Department, on_delete="cascade") # Foreign key to department + PositionDescription = ForeignKeyField(PositionDescription, null=True, on_delete="cascade") jobType = CharField() # Primary or secondary WLS = CharField() POSN_TITLE = CharField() # eg. student programmer, customer engagement specialist, receptionist, teaching assistant POSN_CODE = CharField() + positionDescription = CharField(null=True) contractHours = IntegerField(null=True) # total hours for break terms weeklyHours = IntegerField(null=True) # weekly hours 10,12,15... startDate = DateField(null=True) # in case they start different than term start date endDate = DateField(null=True) supervisorNotes = TextField(null=True) laborDepartmentNotes = TextField(null=True) - - def __str__(self): return str(self.__dict__) diff --git a/app/models/position.py b/app/models/position.py new file mode 100644 index 000000000..fe8430e08 --- /dev/null +++ b/app/models/position.py @@ -0,0 +1,13 @@ +from app.models import * + +class Position (baseModel): + + POSN_CODE = CharField(primary_key=True) + POSN_TITLE = CharField() + WLS = CharField() + ORG = CharField() + ACCOUNT = CharField() + DEPT_NAME = CharField() + + def __str__(self): + return str(self.__dict__) diff --git a/app/models/positionDescription.py b/app/models/positionDescription.py new file mode 100644 index 000000000..e74b8e414 --- /dev/null +++ b/app/models/positionDescription.py @@ -0,0 +1,15 @@ +from app.models import * +from app.models.user import User +from app.models.status import Status +from app.models.position import Position + +class PositionDescription (baseModel): + positionDescriptionID = PrimaryKeyField() + createdBy = ForeignKeyField(User, on_delete="cascade") + status = ForeignKeyField(Status) + POSN_CODE = ForeignKeyField(Position) + createdDate = DateField() + endDate = DateField(null=True) + + def __str__(self): + return str(self.__dict__) diff --git a/app/models/positionDescriptionItem.py b/app/models/positionDescriptionItem.py new file mode 100644 index 000000000..0d33484c9 --- /dev/null +++ b/app/models/positionDescriptionItem.py @@ -0,0 +1,11 @@ +from app.models import * +from app.models.positionDescription import PositionDescription + +class PositionDescriptionItem (baseModel): + positionDescriptionItemID = PrimaryKeyField() + positionDescription = ForeignKeyField(PositionDescription, on_delete="cascade") + itemDescription = CharField(max_length=10000) + itemType = CharField() + + def __str__(self): + return str(self.__dict__) diff --git a/app/static/css/positionDescription.css b/app/static/css/positionDescription.css new file mode 100644 index 000000000..6f2390d8a --- /dev/null +++ b/app/static/css/positionDescription.css @@ -0,0 +1,79 @@ +@media(min-width:970px) and (max-width:2500px) { + .container { + width: 83%; + } +} + +.bootstrap-select>.dropdown-toggle.bs-placeholder, +.bootstrap-select>.dropdown-toggle.bs-placeholder:active, +.bootstrap-select>.dropdown-toggle.bs-placeholder:focus, +.bootstrap-select>.dropdown-toggle.bs-placeholder:hover { + color: #757575; +} + +.dropdown-menu>.disabled>a, +.dropdown-menu>.disabled>a:focus, +.dropdown-menu>.disabled>a:hover { + color: #000; +} + +.subHeader { + text-align: center; +} + +#pastPositionDescription { + height: 100%; + border-style: solid; + overflow: auto; + height: 310px; +} + +#titleGlyphicon{ + font-size: 29px; + color: #337ab7; +} + +selectpicker, label { + display: block; +} + +.col-xs-12 { + padding-left: 0px; + padding-right: 0px; +} + +#glyphicon_note{ + font-size: 29px; + color: #337ab7; + padding-right: 0px; + padding-left: 26px; +} + +#flash_container { + width: 87%; + height: 55px; +} + +.card-header { + background-color: #efebeb; + margin-bottom: 7px; +} + +.mb-0, .collapsed { + outline-color: none; + color: black; + font-size: 18px; + font-weight: 525; +} + +.active, .btn-link:focus { + background-color: #66c2cc; +} + +.active, .card-header:hover { + background-color: #66c2cc; +} + +.pt-3-half { + padding-top: 1.4rem; +} diff --git a/app/static/css/positionDescriptionEdit.css b/app/static/css/positionDescriptionEdit.css new file mode 100644 index 000000000..dd02bddac --- /dev/null +++ b/app/static/css/positionDescriptionEdit.css @@ -0,0 +1,28 @@ +#titleGlyphicon{ + font-size: 29px; + color: #337ab7; +} + +.card-header { + background-color: #efebeb; + margin-bottom: 7px; +} + +.mb-0, .collapsed { + outline-color: none; + color: black; + font-size: 18px; + font-weight: 525; +} + +.active, .btn-link:focus { + background-color: #66c2cc; +} + +.active, .card-header:hover { + background-color: #66c2cc; +} + +.subHeader { + text-align: center; +} diff --git a/app/static/css/viewPositionDescriptions.css b/app/static/css/viewPositionDescriptions.css new file mode 100644 index 000000000..2e92ee0da --- /dev/null +++ b/app/static/css/viewPositionDescriptions.css @@ -0,0 +1,7 @@ +div#pendingDescriptions_length.dataTables_length { + text-align: left; +} + +div#pendingDescriptions_info.dataTables_info { + text-align: left; +} diff --git a/app/static/js/positionDescription.js b/app/static/js/positionDescription.js new file mode 100644 index 000000000..84893e45a --- /dev/null +++ b/app/static/js/positionDescription.js @@ -0,0 +1,151 @@ +$("#warningTitle").hide() + +function getDepartmentPositions(object, stopSelectRefresh="") { // get department from select picker + var departmentOrg = $(object).val(); + var departmentAcct = $(object).find('option:selected').attr('value-account'); + var url = "/positionDescriptions/getPositions/" + departmentOrg + "/" + departmentAcct; + $.ajax({ + url: url, + dataType: "json", + success: function (response){ + fillPositions(response, stopSelectRefresh); + } + }); + } + +function fillPositions(response, stopSelectRefresh="") { // prefill Position select picker with the positions of the selected department + var selectedPositions = $("#position"); + $("#position").empty(); + $("#position").prop("disabled", false); + for (var key in response) { + selectedPositions.append( + $("