From dad0a4903db54e8e2ccb9c2878145da83d0785e2 Mon Sep 17 00:00:00 2001 From: kurasova Date: Mon, 20 Jul 2026 14:49:24 -0400 Subject: [PATCH 01/88] Reverted the merges --- app/controllers/main_routes/__init__.py | 1 + .../main_routes/departmentPortal.py | 245 +++++++++++++++++- app/controllers/main_routes/main_routes.py | 50 ++-- app/logic/tracy.py | 8 + app/models/supervisor.py | 9 +- app/static/css/managemembers.css | 154 +++++++++++ app/static/css/managepositions.css | 15 ++ app/static/js/addSupervisorsToDepartment.js | 31 --- app/static/js/manageMembers.js | 223 ++++++++++++++++ app/templates/main/departmentPortal.html | 134 ++++++++++ app/templates/main/manageMembers.html | 128 +++++++++ app/templates/main/managepositions.html | 90 +++++++ app/templates/main/supervisorPortal.html | 2 +- database/demo_data.py | 243 ++++++++++++++++- database/reset_database.sh | 4 +- database/reset_database.sh.bak | 67 +++++ db_test.py | 4 +- 17 files changed, 1341 insertions(+), 67 deletions(-) create mode 100644 app/static/css/managemembers.css create mode 100644 app/static/css/managepositions.css create mode 100644 app/static/js/manageMembers.js create mode 100644 app/templates/main/manageMembers.html create mode 100644 app/templates/main/managepositions.html create mode 100644 database/reset_database.sh.bak diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 54b32d3c5..da3323908 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -14,6 +14,7 @@ def injectGlobalData(): 'lastStaticUpdate': lastStaticUpdate} from app.controllers.main_routes import main_routes +from app.controllers.main_routes import departmentPortal from app.controllers.main_routes import laborStatusForm from app.controllers.main_routes import laborHistory from app.controllers.main_routes import alterLSF diff --git a/app/controllers/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py index 351757701..353287e0f 100644 --- a/app/controllers/main_routes/departmentPortal.py +++ b/app/controllers/main_routes/departmentPortal.py @@ -1 +1,244 @@ -from flask import render_template +import re +from datetime import date +from flask import render_template, request, json, redirect, session, url_for, send_file, g, flash, jsonify +from peewee import JOIN, DoesNotExist, fn, Case +from functools import reduce +import operator +from app.logic.tracy import Tracy +from app.logic.userInsertFunctions import createSupervisorFromTracy +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.laborReleaseForm import LaborReleaseForm +from app.models.term import Term +from app.controllers.admin_routes.allPendingForms import checkAdjustment +from app.controllers.main_routes import main_bp +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.getTableData import getDatatableData +from app.logic.banner import Banner +from flask import abort +from app.logic.search import limitSearchByUserDepartment, studentDbToDict, usernameFromEmail + + + +@main_bp.route('/department///members', methods=['GET']) +def manageStaff(org=None,account=None): + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + session['current_department_id'] = dept.departmentID + session['current_department'] = dept.DEPT_NAME + except (NameError, DoesNotExist): + dept = None + abort(404) + + members = list( + SupervisorDepartment. + select( + SupervisorDepartment, + Supervisor + ).where( + SupervisorDepartment.department == dept + ).join( + Supervisor + ).dicts() + ) + + today = date.today() + released_forms = ( + FormHistory + .select(FormHistory.formID) + .join( + LaborReleaseForm, + on=(FormHistory.releaseForm == LaborReleaseForm.laborReleaseFormID) + ) + .where( + (FormHistory.historyType == "Labor Release Form") & + (FormHistory.status == "Approved") & + (LaborReleaseForm.releaseDate <= today) + ) + ) + + # Conditions for the supervisee counts. Expired and released positions do + # not contribute to any of the four totals. + active_primaries = ( + (LaborStatusForm.jobType == 'Primary') & + (LaborStatusForm.studentConfirmation == True) & + (LaborStatusForm.endDate >= today) + ) + pending_primaries = ( + (LaborStatusForm.jobType == 'Primary') & + (LaborStatusForm.studentConfirmation.is_null(True)) & + (LaborStatusForm.endDate >= today) + ) + active_secondaries = ( + (LaborStatusForm.jobType == 'Secondary') & + (LaborStatusForm.studentConfirmation == True) & + (LaborStatusForm.endDate >= today) + ) + pending_secondaries = ( + (LaborStatusForm.jobType == 'Secondary') & + (LaborStatusForm.studentConfirmation.is_null(True)) & + (LaborStatusForm.endDate >= today) + ) + + + student_count = list( + LaborStatusForm. + select( + fn.SUM(Case(None, ((active_primaries, 1),), 0)).alias("active_primary_positions"), + fn.SUM(Case(None, ((pending_primaries, 1),), 0)).alias("pending_primary_positions"), + fn.SUM(Case(None, ((active_secondaries, 1),), 0)).alias("active_secondary_positions"), + fn.SUM(Case(None, ((pending_secondaries, 1),), 0)).alias("pending_secondary_positions"), + LaborStatusForm.department, + LaborStatusForm.supervisor + ).where( + (LaborStatusForm.department == dept) & + (LaborStatusForm.laborStatusFormID.not_in(released_forms)) + ).group_by( + LaborStatusForm.department, + LaborStatusForm.supervisor + ).dicts() + ) + + counts = {(row["department"], row["supervisor"]): row for row in student_count} + + for member in members: + + key = (member["department"], member["supervisor"]) + row = counts.get(key, {}) + + member["active_primary_positions"] = row.get("active_primary_positions", 0) + member["pending_primary_positions"] = row.get("pending_primary_positions", 0) + member["active_secondary_positions"] = row.get("active_secondary_positions", 0) + member["pending_secondary_positions"] = row.get("pending_secondary_positions", 0) + + return render_template('main/manageMembers.html', + members = members, + department = dept) + + +def supervisorsDbToDict(supervisor): + """ + Given a supervisor object it will return a mapped Dict with supervisor data. + """ + dbToDict = {'username': usernameFromEmail(supervisor.EMAIL.strip()), + 'firstName': supervisor.FIRST_NAME.strip(), + 'lastName': supervisor.LAST_NAME.strip(), + 'bnumber': supervisor.ID.strip(), + 'department': supervisor.DEPT_NAME.strip(), + 'type': 'Supervisor'} + return dbToDict + + +# search student table and STUDATA for student results +@main_bp.route('/members/search/', methods=['GET']) +def add_member(query=None): + currentUser = require_login() + accessAllowed = currentUser and (currentUser.supervisor or currentUser.isLaborAdmin) + if not accessAllowed: + return render_template('errors/403.html'), 403 + + recorded_supervisors = [] # supervisors recorded in the database + current_supervisors = [] # supervisors from Tracy + query = query.strip() + + current_department = session.get('current_department') + + # bnumber search + if re.match(r'[Bb]\d+', query): + recorded_supervisors = list(map(supervisorsDbToDict, Supervisor.select().where(Supervisor.ID % "{}%".format(query.upper())).where(Supervisor.DEPT_NAME != current_department))) + current_supervisors = [ + s for s in map(supervisorsDbToDict, Tracy().getSupervisorsFromUserInput(query)) + if s.get('department') != current_department + ] + + + # name search + else: + if " " not in query: + search = query.upper() + "%" + results = Supervisor.select().where(Supervisor.DEPT_NAME != current_department).where(Supervisor.preferred_name ** search | Supervisor.legal_name ** search | Supervisor.LAST_NAME ** search) + else: + search = query.upper().split() + first_query = search[0] + "%" + last_query = search[-1] + "%" + results = Supervisor.select().where(Supervisor.DEPT_NAME != current_department).where((Supervisor.preferred_name ** first_query | Supervisor.legal_name ** first_query) & Supervisor.LAST_NAME ** last_query) + + recorded_supervisors = list(map(supervisorsDbToDict, results)) + current_supervisors = [ + s for s in map(supervisorsDbToDict, Tracy().getSupervisorsFromUserInput(query)) + if s.get('department') != current_department + ] + + # combine lists, remove duplicates, and then sort + supervisors = list({v['bnumber']:v for v in (current_supervisors + recorded_supervisors)}.values()) + supervisors = sorted(supervisors, key=lambda f: f['firstName'] + f['lastName']) + + return jsonify(supervisors) + + +@main_bp.route('/members/coordinator_switch', methods=['POST']) +def coordinator_switch(): + data = request.get_json() + supervisor_id = data.get("supervisorID") + is_coordinator = data.get("isCoordinator") + + member = SupervisorDepartment.get(SupervisorDepartment.supervisor == supervisor_id) + member.isCoordinator = is_coordinator + member.save() + + return "", 200 + + +@main_bp.route('/members/ban_switch', methods=['POST']) +def ban_switch(): + data = request.get_json() + supervisor_id = data.get("supervisorID") + + member = Supervisor.get(Supervisor.ID == supervisor_id) + + member.isBanned = not member.isBanned + member.save() + + return "", 200 + + +@main_bp.route('/members/remove', methods=['DELETE']) +def remove_member(): + data = request.get_json() + supervisor_id = data.get("supervisorID") + + member = SupervisorDepartment.get( + (SupervisorDepartment.supervisor == supervisor_id) & + (SupervisorDepartment.department == session['current_department_id']) + ) + member.delete_instance() + + return "", 200 + + + +@main_bp.route('/members/add', methods=['GET', 'POST']) +def addUserToDept(): + userDeptData = request.form + supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor = userDeptData['supervisorID'], department = userDeptData['departmentID']) + try: + if supervisorDeptRecord: + return "False" + + else: + supervisorID = userDeptData['supervisorID'] + if not Supervisor.get_or_none(Supervisor.ID == supervisorID): + createSupervisorFromTracy(bnumber=supervisorID) + + SupervisorDepartment.create(supervisor=supervisorID, department=userDeptData['departmentID']) + return "True" + + except Exception as e: + print(f'Could not add user to department: {e}') + return "", 500 \ No newline at end of file diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 0a2f21e4b..d43df38c1 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -16,6 +16,10 @@ from app.login_manager import require_login, logout from app.logic.getTableData import getDatatableData from app.logic.banner import Banner +from app.logic.tracy import Tracy +from app.logic.userInsertFunctions import createSupervisorFromTracy +from app.models.positionHistory import PositionHistory + @main_bp.route('/logout', methods=['GET']) def triggerLogout(): @@ -51,10 +55,14 @@ def supervisorPortal(): @main_bp.route('/department/', methods=['GET']) @main_bp.route('/department//', methods=['GET']) def departmentPortal(org=None,account=None): - try: - dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) - except (NameError, DoesNotExist): + if org and account: + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + dept = None + else: dept = None + @@ -65,23 +73,27 @@ def departmentPortal(org=None,account=None): return render_template('main/departmentPortal.html', departments = departments, - department = dept) - -@main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) -def addUserToDept(): - userDeptData = request.form - supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor = userDeptData['supervisorID'], department = userDeptData['departmentID']) + department = dept, + supervisors = supervisors, + laborCoordinators=laborCoordinators, + currentUser=g.currentUser, + positions = positionsList, + posUrl = posUrl) + +@main_bp.route('/department///positions', methods=['GET']) +def managePositions(org, account): try: - if supervisorDeptRecord: - return "False" - - else: - SupervisorDepartment.create(supervisor=userDeptData['supervisorID'], department=userDeptData['departmentID']) - return "True" - - except Exception as e: - print(f'Could not add user to department: {e}') - return "", 500 + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except DoesNotExist: + return render_template('errors/404.html'), 404 + + positions = Tracy().getPositionsFromDepartment(org, account) + print(positions) + return render_template('main/managepositions.html', + department = dept, + department_name = dept.DEPT_NAME, + positions = positions + ) @main_bp.route('/supervisorPortal/download', methods=['POST']) def downloadSupervisorPortalResults(): diff --git a/app/logic/tracy.py b/app/logic/tracy.py index cd0ecf50a..027cd9fd5 100644 --- a/app/logic/tracy.py +++ b/app/logic/tracy.py @@ -142,6 +142,14 @@ def getStudentsFromBNumberSearch(self, bnum_part: str): students = STUDATA.query.filter(STUDATA.ID.like(bnum_search)).all() return students + def getSupervisorsFromBNumberSearch(self, bnum_part: str): + """ + Return a list of supervisors searching by bnumber + """ + bnum_search = bnum_part.strip().upper() + "%" + supervisors = STUSTAFF.query.filter(STUSTAFF.ID.like(bnum_search)).all() + return supervisors + def checkStudentOrSupervisor(self, username: str): """ Checks if the username belongs to a student or supervisor diff --git a/app/models/supervisor.py b/app/models/supervisor.py index e16d43282..3f9d3608e 100644 --- a/app/models/supervisor.py +++ b/app/models/supervisor.py @@ -13,10 +13,11 @@ class Supervisor(baseModel): ORG = CharField(null=True) DEPT_NAME = CharField(null=True) - legal_name = CharField(null=True) - preferred_name = CharField(null=True) - isActive = BooleanField(default=False) - isBanned = BooleanField(default=False) + legal_name = CharField(null=True) + preferred_name = CharField(null=True) + isActive = BooleanField(default=False) + isBanned = BooleanField(default=False) + @property diff --git a/app/static/css/managemembers.css b/app/static/css/managemembers.css new file mode 100644 index 000000000..3384fddc6 --- /dev/null +++ b/app/static/css/managemembers.css @@ -0,0 +1,154 @@ +/*styles for column alignment in the data table*/ +.centered-header { + text-align: center !important; +} + +.centered-cell { + vertical-align: middle !important; +} + +/* The add-member search bar*/ +#add-member { + display: flex; + justify-content: center; + gap: 0px; + height: 20px; + margin-bottom: 30px; +} + + +@media (min-width: 767px) { + #add-member{ + display: flex; + justify-content: flex-end; + gap: 0px; + /*margin-bottom: -20px;*/ + } +} + + +/* The switch container */ +.switch { + position: relative; + display: inline-block; + width: 31.25px; + height: 16.25px; + vertical-align: middle; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider track */ +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #ccc !important; + transition: .4s; +} + +/* The slider knob */ +.slider:before { + position: absolute; + content: ""; + height: 11.25px; + width: 11.25px; + left: 2.5px; + bottom: 2.5px; + background-color: white !important; + transition: .4s; +} + +/* Checked state track color */ +input:checked + .slider { + background-color: #337ab7 !important; /* Bootstrap 3 Primary Color */ +} + +/* Checked state knob movement */ +input:checked + .slider:before { + transform: translateX(15px) !important; +} + +/* Rounded slider track and knob */ +.slider.round { + border-radius: 21.25px; +} +.slider.round:before { + border-radius: 50%; +} + +/* Optional label spacing */ +.switch-label { + margin-left: 6.25px; + vertical-align: middle; + font-weight: bold; +} + +/* hover indicator */ +a.hover-indicator { + text-decoration: none !important; + position: relative; + border-bottom: 1px dotted #000; + color: #000000; +} + +a.hover-indicator:visited, +a.hover-indicator:link, +a.hover-indicator:hover, +a.hover-indicator:active { + color: #000000; +} + +[class*="hover-indicator"]:after { + content: attr(aria-label); + display: none; + position: absolute; + top: 110%; + left: 10px; + z-index: 5000; + pointer-events: none; + padding: 8px 10px; + line-height: 15px; + white-space: nowrap; + text-decoration: none !important; + text-indent: 0; + overflow: visible; + font-size: .9em; + font-weight: normal; + -webkit-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 1px 2px 6px rgba(0,0,0,0.3); + box-shadow: 1px 2px 6px rgba(0,0,0,0.3); + background-color: #FFFFFF; + color: #000000; +} + +[class*="hover-indicator"]:hover:after, [class*="hover_indicator"]:focus:after { + display: block; + color: #000000; +} + +/*a "banned" badge displayed under the name of a banned department member*/ +.isbanned-badge { + position: relative; + display: inline-block; + margin-top: 3px; + padding: 2px 6px; + background: red; + color: white !important; + border-radius: 10px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.3px; + line-height: 1.2; +} + +/* switches inside the data table */ +.table-switch { + top: 10px; +} \ No newline at end of file diff --git a/app/static/css/managepositions.css b/app/static/css/managepositions.css new file mode 100644 index 000000000..9c40bbe38 --- /dev/null +++ b/app/static/css/managepositions.css @@ -0,0 +1,15 @@ +.width-12{ + width:12%; +} + +*{ + /* outline:solid 1px lime; */ + margin:0; + padding:0; + box-sizing:border-box; +} + +.department-header{ + padding:1%; + margin-top:-20px; +} \ No newline at end of file diff --git a/app/static/js/addSupervisorsToDepartment.js b/app/static/js/addSupervisorsToDepartment.js index 16c3f1d85..e69de29bb 100644 --- a/app/static/js/addSupervisorsToDepartment.js +++ b/app/static/js/addSupervisorsToDepartment.js @@ -1,31 +0,0 @@ -function addSupervisorToDepartment(supervisorID, departmentID, callback=() => {}) { - return $.ajax({ - method: "POST", - url: `/supervisorPortal/addUserToDept`, - data: {"supervisorID": supervisorID, "departmentID": departmentID}, - success: function(response) { - if (response == "True") { - msgFlash("Supervisor has been added to department.", "success") - clearDropdowns() - } else { - msgFlash("Supervisor is already a member of this department.", "warning") - clearDropdowns() - } - if (callback){ - callback() - } - }, - error: function() { - msgFlash("Failed to add supervisor, please try again.", "fail") - clearDropdowns() - }, - }) - -} - -function clearDropdowns(){ - $('select.selectpicker').each(function() { - $(`#${this.id} option:eq(0)`).prop("selected", true); - $(`#${this.id}`).selectpicker("refresh"); - }); -}; \ No newline at end of file diff --git a/app/static/js/manageMembers.js b/app/static/js/manageMembers.js new file mode 100644 index 000000000..18055e1a1 --- /dev/null +++ b/app/static/js/manageMembers.js @@ -0,0 +1,223 @@ +$(document).ready(function() { + + $('#searchBoxContainer').children('.dropdown, .bootstrap-select, .form-control').addClass('open') + $('[type="search"], .form-control').focus(); + + $('#manageMembers').DataTable({ + 'columnDefs': [{ + 'targets': '.no-sorting', + 'orderable': false + }], // hide sort icon on header of first column + 'aaSorting': [ + [0, 'asc'] + ], // start to sort data in second column + searching: false, + pageLength: 10, + language: { + lengthMenu: " _MENU_ entries per page" + }, + //dom: '<"top"l>rt<"bottom"p><"clear">' + }); + + $(document).on("click", ".assign-coordinator", function() { + + let memberName = $(this).data("member-name"); + let supervisorID = $(this).data("supervisor"); + let isChecked = $(this).is(":checked"); + + $.ajax({ + url: "/members/coordinator_switch", + data: JSON.stringify({supervisorID: supervisorID, isCoordinator: isChecked}), + type: "POST", + contentType: "application/json", + success: function() { + if (isChecked) { + $("#flash_container").html("
" + memberName + " has been assigned as a coordinator.
"); + $("#flasher").delay(3000).fadeOut(); + } else { + $("#flash_container").html("
" + memberName + " is no longer a coordinator.
"); + $("#flasher").delay(3000).fadeOut(); + } + + }, + error: function() {console.log("An error has occured.");} + }) + + + }); + + $(document).on("click", ".member-status-btn", function() { + + let button = $(this); + let ban_badge = $(this).closest("tr").find(".isbanned-badge"); + + let memberName = button.data("member-name"); + let supervisorID = button.data("supervisor"); + + let banStatus = button.val(); + let isBanned = banStatus === "Banned" ? true : false; + + let quote = String.fromCharCode(39); + + let category; + + $.ajax({ + url: "/members/ban_switch", + data: JSON.stringify({supervisorID: supervisorID, isBanned: isBanned}), + type: "POST", + contentType: "application/json", + success: function() { + if (!isBanned) { + category = "danger"; + button.removeClass("btn-danger").addClass("btn-success"); + button.text("Unban"); + button.val("Banned"); + ban_badge.css("visibility", "visible"); + } else { + category = "success"; + button.removeClass("btn-success").addClass("btn-danger"); + button.html("  Ban  "); + button.val("Unbanned"); + ban_badge.css("visibility", "hidden"); + } + + $("#flash_container").html("
The status for " + memberName + " has been set to " + quote + banStatus + quote + ".
"); + $("#flasher").delay(3000).fadeOut(); + + }, + error: function() {console.log("An error has occured.");} + }) + }); + + $(document).on("click", ".remove-member", function() { + + let redButton = $(this); + let row = $(this).closest("tr"); + + let memberName = redButton.data("member-name"); + let supervisorID = redButton.data("supervisor"); + + $.ajax({ + url: "/members/remove", + data: JSON.stringify({supervisorID: supervisorID}), + type: "DELETE", + contentType: "application/json", + success: function() { + $("#flash_container").html("
" + memberName + " has been removed from the department.
"); + $("#flasher").delay(3000).fadeOut(); + row.remove(); + }, + error: function() {console.log("An error has occured.");} + }) + }); +}) + +// Creates a dom fragment from html, rather than having to add dom elements +// https://love2dev.com/blog/inserting-html-using-createdocumentfragment-instead-of-using-jquery/ +function createFragment(htmlStr) { + let frag = document.createDocumentFragment(), temp = document.createElement('div'); + temp.innerHTML = htmlStr; + while(temp.firstChild) { frag.appendChild(temp.firstChild); } + return frag; +} + +// highlight search string. doesn't actually check for last name and first name, just highlights what we find +$.fn.selectpicker.Constructor.DEFAULTS.whiteList.mark = []; +function highlight(htmlStr, query) { + query = query.trim().split(" "); + for(i = 0; i < query.length; i++) { + htmlStr = htmlStr.replace(new RegExp(query[i], "gi"), function(match) { return `${match}`; }); + } + return htmlStr; +} + +let typeTimer; + +$('#search').selectpicker('refresh'); +$('.dropdown-menu .bs-searchbox input').on('keyup', function (e) { + // ignore arrow keys + if (e.keyCode == '40' || e.keyCode == '38') return; + + // wait a little longer for bnumber typing + keyInterval = 200 + if (e.keyCode >= 48 && e.keyCode <= 57) { + keyInterval = 500 + } + + // don't search for every key (especially relevant for bnumber) + clearTimeout(typeTimer) + typeTimer = setTimeout(function() { sendQuery(e.target.value); }, keyInterval) +}); + +$('#search').on('changed.bs.select', function () { + let supervisorID = $(this).val(); + let departmentID = $(this).data('department-id'); + + if (!supervisorID || !departmentID) return; + + addSupervisorToDepartment(supervisorID, departmentID, function() { + window.location.reload(); + }); +}); + +// We load the options returned into an html string and then add them to the selectpicker at the end, to save A LOT of time. +function sendQuery(search_str) { + $("#search").empty(); + $('#search').selectpicker("refresh"); + if (search_str.length >= 3) { + $.ajax({ + type: "GET", + url: "/members/search/" + encodeURIComponent(search_str), + contentType: 'application/json', + success: function(response) { + let optionString = "" + for (let key = 0; key < response.length; key++) { + let username = response[key]['username']; + let bnumber = response[key]['bnumber']; + let firstName = response[key]['firstName']; + let lastName = response[key]['lastName']; + let type = response[key]['type']; + if (type == "Supervisor") { + choice_text = bnumber + ': ' + firstName + ' ' + lastName; + highlighted_text = highlight(choice_text, search_str) + `${username}`; + optionString += ``; + } + } + $("#search").append(createFragment(optionString)) + $('#search').selectpicker("refresh"); + } + }); + } +} + +function addSupervisorToDepartment(supervisorID, departmentID, callback=() => {}) { + return $.ajax({ + method: "POST", + url: `/members/add`, + data: {"supervisorID": supervisorID, "departmentID": departmentID}, + success: function(response) { + if (response == "True") { + msgFlash("Supervisor has been added to department.", "success") + clearDropdowns(); + } else { + msgFlash("Supervisor is already a member of this department.", "warning") + clearDropdowns(); + } + if (callback){ + callback(); + } + }, + error: function() { + msgFlash("Failed to add supervisor, please try again.", "fail") + clearDropdowns(); + }, + }) + +} + +function clearDropdowns(){ + $('select.selectpicker').each(function() { + $(`#${this.id} option:eq(0)`).prop("selected", true); + $(`#${this.id}`).selectpicker("refresh"); + }); +}; \ No newline at end of file diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index 8ee01838b..0b9a6134d 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -24,4 +24,138 @@

{% if department %} {{department.DEPT_NAME}} Portal {% e + + + {% if department %} +
+
+ +
+

Insert Allocations Card Here

+ +
+ +
+ +
+
+
+
+
+ + + +
+ +
+

+ Members +

+
+
+

{% if laborCoordinators|length <= 1 %} Labor Coordinator {% else %} Labor Coordinators{% endif %}

+ {% if laborCoordinators %} + {% for coordinator in laborCoordinators %} +

+ {{ coordinator.name }} + + {% if coordinator.email %} + + + + {% endif %} +

+ {% endfor %} + {% else %} +

None assigned. Add one?

+ + + {% endif %} + + +

{% if supervisors|length <= 1 %} Supervisor{% else %} Supervisors{% endif %}

+ + {% if supervisors %} + {% for s in supervisors[:3] %} +

+ {{ s.name }} + + {% if s.email %} + + + + + {% endif %} +

+ {% endfor %} + + {% if supervisors|length > 3 %} +

and {{ supervisors|length - 3 }} more...

+ {% endif %} + + {% elif not laborCoordinators and currentUser.isLaborAdmin %} +

No active supervisors assigned. Add one?

+ {% else %} +

No active supervisors assigned.

+ {% endif %} + +
+ + +
+ +
+ +
+
+
+
+
+ +
+
+

Positions

+
+ +
+
    + {% for p in positions[:7] %} + {% if p == "No active positions in this department" %} +
  • +

    {{ p }}

    +
  • + {% else %} +
  • + {{ p }} +
  • + {% endif %} + {% endfor %} + {% if positions| length >7 %} +

and {{ (positions | length) - 7}} more...

+ {% endif %} + +
+ +
+
+
+ +{% endif %} + {% endblock %} diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html new file mode 100644 index 000000000..8d9758fee --- /dev/null +++ b/app/templates/main/manageMembers.html @@ -0,0 +1,128 @@ +{% extends "base.html" %} + +{% block styles %} +{{super()}} + + + {% endblock %} + +{% block scripts %} +{{super()}} + + + +{% endblock %} + +{% block app_content %} +

{% if department %} Manage {{department.DEPT_NAME}} Members {% else %} Manage Members {% endif %}

+

Academic Year: 2026-2027

+ +

+ +
+ Click to Skip +
+
+ +
+
+
+ +
+ + + + + + + + {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + + + {% endif %} + + + + + {% for i in members %} + + + + + + + + {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + + + {% endif %} + + + {% endfor %} + +
NameSuperviseesAssign CoordinatorChange StatusRemove Member
+ {{ i['legal_name'] + " " + i['LAST_NAME']}} + + +
+ +
+ {% if (i['active_primary_positions'] == 0 and i['active_secondary_positions'] == 0) and (i['pending_primary_positions'] == 0 and i['pending_secondary_positions'] == 0) %} + None + {% else %} + Primary: {{i['active_primary_positions']}} active, {{i['pending_primary_positions']}} pending
+ Secondary: {{i['active_secondary_positions']}} active, {{i['pending_secondary_positions']}} pending + {% endif %} +
+
+
+ + +
+
+
+
+ {% if not i['isBanned'] %} + + {% else %} + + {% endif %} +
+
+
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/main/managepositions.html b/app/templates/main/managepositions.html new file mode 100644 index 000000000..f61558a37 --- /dev/null +++ b/app/templates/main/managepositions.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block scripts %} +{{super()}} + +{% endblock %} + +{% block app_content %} +
+

{{ department_name }}

+
+ +
+ + + +
+ + + + + + + + + + + + + + + {% for position in positions %} + + + + + + + + + {% endfor %} + +
Position (WSL)Position CodeStatusLast Revision DateView Position DescriptionEdit Position Description
{{position.POSN_TITLE}} ({{position.WLS}}){{position.POSN_CODE}} +

+ Active +

+
01/01/2024 + + + +
+
+

+ Total Positions: + {{ positions|length }} +

+
+ +{% endblock %} diff --git a/app/templates/main/supervisorPortal.html b/app/templates/main/supervisorPortal.html index 78383566c..3ab521a54 100644 --- a/app/templates/main/supervisorPortal.html +++ b/app/templates/main/supervisorPortal.html @@ -227,7 +227,7 @@

Form Search


-
+
- - {% for p in positions %} - - {% endfor %} - - -
-

Manage Positions

-
- -
-
- -
- - - - - - - - - - - - - - - {% for position in positions %} - - - - - - - - - {% endfor %} - -
Position (WSL)Position CodeStatusLast Revision DateView Position DescriptionEdit Position Description
{{position.POSN_TITLE}} ({{position.WLS}}){{position.POSN_CODE}} -

- Active -

-
01/01/2024 - - - -
-
-

- Total Positions: - {{ positions|length }} -

-
- -{% endblock %} From 647ba11d0e34d25ab82e4b5d47dc3afa1a5c9f9e Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 22 Jul 2026 16:28:16 -0400 Subject: [PATCH 19/88] Removed the file out of scope --- database/reset_database.sh.bak | 67 ---------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 database/reset_database.sh.bak diff --git a/database/reset_database.sh.bak b/database/reset_database.sh.bak deleted file mode 100644 index 82f6cff53..000000000 --- a/database/reset_database.sh.bak +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -## Check if argument was passed in -if [ "$1" != "test" ] && [ "$1" != "from-backup" ]; then - echo "You must specify which data set you want to restore" - echo "Usage: ./reset_database.sh [test|from-backup]" - exit 1; -fi - -cd database; - -PRODUCTION=0 -if [ "`hostname`" == 'lsf.berea.edu' ]; then - echo "DO NOT RUN THIS SCRIPT ON PRODUCTION UNLESS YOU REALLY REALLY KNOW WHAT YOU ARE DOING" - PRODUCTION=1 - exit 1 -fi - -BACKUP=0 -if [ "$1" == "from-backup" ]; then - BACKUP=1 -fi - -echo "Dropping databases" -mysql -u root -proot --execute="DROP DATABASE \`lsf\`; DROP USER 'lsf_user';" -mysql -u root -proot --execute="DROP DATABASE \`UTE\`; DROP USER 'tracy_user';" - -echo "Recreating databases and users" -mysql -u root -proot --execute="CREATE DATABASE IF NOT EXISTS \`lsf\`; CREATE USER IF NOT EXISTS 'lsf_user'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON *.* TO 'lsf_user'@'%';" -mysql -u root -proot --execute="CREATE DATABASE IF NOT EXISTS \`UTE\`; CREATE USER IF NOT EXISTS 'tracy_user'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON *.* TO 'tracy_user'@'%';" - -cd database - -rm -rf lsf_migrations -rm -rf tracy_migrations -rm -rf migrations.json - -echo "Creating database objects" -if [ $BACKUP -eq 1 ]; then - echo " from backup" - mysql -u root -proot lsf < prod-backup.sql -else - echo " empty" - ./migrate_db.sh -fi - -if [ $PRODUCTION -ne 1 ]; then - ./migrate_db_tracy.sh -fi - -rm -rf lsf_migrations -rm -rf tracy_migrations -rm -rf migrations.json - -# Adding data we need in all environments, unless we are restoring from backup -if [ $BACKUP -ne 1 ]; then - python3 base_data.py -else - echo "You have imported the production DB backup. You probably want to enable real Tracy access as well. Set FLASK_ENV to staging or production." -fi - -# Adding fake data for non-prod, set up admins for prod -if [ $PRODUCTION -eq 1 ]; then - FLASK_ENV=production python3 add_admins.py -elif [ $BACKUP -ne 1 ]; then - python3 demo_data.py -fi From 322f1fd1a944299119eada5b971fb7ff67aa2a2b Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 22 Jul 2026 16:30:12 -0400 Subject: [PATCH 20/88] Removed the extra spaces --- database/reset_database.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/database/reset_database.sh b/database/reset_database.sh index 16f056898..03fce72b4 100755 --- a/database/reset_database.sh +++ b/database/reset_database.sh @@ -7,8 +7,7 @@ if [ "$1" != "test" ] && [ "$1" != "from-backup" ]; then exit 1; fi -cd database; - +cd database; PRODUCTION=0 if [ "`hostname`" == 'lsf.berea.edu' ]; then echo "DO NOT RUN THIS SCRIPT ON PRODUCTION UNLESS YOU REALLY REALLY KNOW WHAT YOU ARE DOING" @@ -45,7 +44,7 @@ else fi if [ $PRODUCTION -ne 1 ]; then - ./migrate_db_tracy.sh + ./migrate_db_tracy.sh fi rm -rf lsf_migrations From c9e7871e93fc6beb3bfb50c7c34fc9305f27a566 Mon Sep 17 00:00:00 2001 From: Bright A Feitsop Date: Wed, 22 Jul 2026 16:34:06 -0400 Subject: [PATCH 21/88] Delete app/static/js/addSupervisorsToDepartment.js --- app/static/js/addSupervisorsToDepartment.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/static/js/addSupervisorsToDepartment.js diff --git a/app/static/js/addSupervisorsToDepartment.js b/app/static/js/addSupervisorsToDepartment.js deleted file mode 100644 index e69de29bb..000000000 From 436198fcce9193421c3a6ce5c824f29520c5a35e Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 22 Jul 2026 16:35:57 -0400 Subject: [PATCH 22/88] Made it same as before --- app/templates/main/supervisorPortal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/main/supervisorPortal.html b/app/templates/main/supervisorPortal.html index 3ab521a54..78383566c 100644 --- a/app/templates/main/supervisorPortal.html +++ b/app/templates/main/supervisorPortal.html @@ -227,7 +227,7 @@

Form Search


-
+
- - {% endblock %} From a9823107a9e445d7ea3b4c502abdc9a490b4b8e9 Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 22 Jul 2026 16:45:09 -0400 Subject: [PATCH 26/88] Removed space --- database/reset_database.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/database/reset_database.sh b/database/reset_database.sh index b1b0e5eb4..340207303 100755 --- a/database/reset_database.sh +++ b/database/reset_database.sh @@ -8,7 +8,6 @@ if [ "$1" != "test" ] && [ "$1" != "from-backup" ]; then fi cd database; - PRODUCTION=0 if [ "`hostname`" == 'lsf.berea.edu' ]; then echo "DO NOT RUN THIS SCRIPT ON PRODUCTION UNLESS YOU REALLY REALLY KNOW WHAT YOU ARE DOING" @@ -45,7 +44,7 @@ else fi if [ $PRODUCTION -ne 1 ]; then - ./migrate_db_tracy.sh + ./migrate_db_tracy.sh fi rm -rf lsf_migrations From 2fa25e9b8935a19371a69b9eba3a96b1feb60de6 Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 22 Jul 2026 16:45:59 -0400 Subject: [PATCH 27/88] Added space same as before --- database/reset_database.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/database/reset_database.sh b/database/reset_database.sh index 340207303..82f6cff53 100755 --- a/database/reset_database.sh +++ b/database/reset_database.sh @@ -8,6 +8,7 @@ if [ "$1" != "test" ] && [ "$1" != "from-backup" ]; then fi cd database; + PRODUCTION=0 if [ "`hostname`" == 'lsf.berea.edu' ]; then echo "DO NOT RUN THIS SCRIPT ON PRODUCTION UNLESS YOU REALLY REALLY KNOW WHAT YOU ARE DOING" From c10d0fe75d348b87918bffcb8fcfe2006f10087d Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 22 Jul 2026 16:47:08 -0400 Subject: [PATCH 28/88] Removed unnecessary function --- app/logic/tracy.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/logic/tracy.py b/app/logic/tracy.py index 027cd9fd5..cd0ecf50a 100644 --- a/app/logic/tracy.py +++ b/app/logic/tracy.py @@ -142,14 +142,6 @@ def getStudentsFromBNumberSearch(self, bnum_part: str): students = STUDATA.query.filter(STUDATA.ID.like(bnum_search)).all() return students - def getSupervisorsFromBNumberSearch(self, bnum_part: str): - """ - Return a list of supervisors searching by bnumber - """ - bnum_search = bnum_part.strip().upper() + "%" - supervisors = STUSTAFF.query.filter(STUSTAFF.ID.like(bnum_search)).all() - return supervisors - def checkStudentOrSupervisor(self, username: str): """ Checks if the username belongs to a student or supervisor From 08ef1ddf99b2b89f2b701ddb529defbe19b45095 Mon Sep 17 00:00:00 2001 From: feitsopb Date: Wed, 22 Jul 2026 16:58:46 -0400 Subject: [PATCH 29/88] added test cases for our functions --- tests/code/test_departmentPortal.py | 895 ++++++++++++++++++++++++++++ 1 file changed, 895 insertions(+) create mode 100644 tests/code/test_departmentPortal.py diff --git a/tests/code/test_departmentPortal.py b/tests/code/test_departmentPortal.py new file mode 100644 index 000000000..d3864a94c --- /dev/null +++ b/tests/code/test_departmentPortal.py @@ -0,0 +1,895 @@ +import pytest + +from types import SimpleNamespace +from unittest.mock import MagicMock +from flask import Flask + +import app.controllers.main_routes.departmentPortal as department_portal + + +@pytest.fixture +def app(): + """ + Creates a small Flask application for testing the department portal routes. + """ + test_app = Flask(__name__) + + test_app.config.update( + TESTING=True, + SECRET_KEY="test-secret-key", + ) + + test_app.register_blueprint(department_portal.main_bp) + + return test_app + + +@pytest.fixture +def client(app): + """ + Creates a Flask test client. + """ + return app.test_client() + + +# ------------------------------------------------------------------- +# manageMembers tests +# ------------------------------------------------------------------- + + +@pytest.mark.integration +def test_manage_members_for_supervisor(client, monkeypatch): + fake_user = SimpleNamespace( + supervisor="B00000001", + student=None, + isLaborAdmin=False, + ) + + fake_supervisor = SimpleNamespace( + ID="B00000001", + legal_name="Test Supervisor", + ) + + fake_department = SimpleNamespace( + departmentID=10, + ORG=2114, + ACCOUNT="60000", + DEPT_NAME="Computer Science", + ) + + fake_members = [ + { + "supervisor": "B00000002", + "LAST_NAME": "Scott", + } + ] + + fake_counts = { + "B00000002": 3, + } + + fake_members_with_counts = [ + { + "supervisor": "B00000002", + "LAST_NAME": "Scott", + "positionCount": 3, + } + ] + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "get", + lambda *args, **kwargs: fake_supervisor, + ) + + monkeypatch.setattr( + department_portal, + "getCurrentDepartment", + lambda org, account: fake_department, + ) + + monkeypatch.setattr( + department_portal, + "getDepartmentMembers", + lambda department: fake_members, + ) + + monkeypatch.setattr( + department_portal, + "getStudentCounts", + lambda department: fake_counts, + ) + + monkeypatch.setattr( + department_portal, + "attachPositionCounts", + lambda members, counts: fake_members_with_counts, + ) + + monkeypatch.setattr( + department_portal, + "currentAcademicYear", + lambda: "2026-2027", + ) + + render_mock = MagicMock(return_value="Manage Members Page") + + monkeypatch.setattr( + department_portal, + "render_template", + render_mock, + ) + + response = client.get( + "/department/2114/60000/members" + ) + + assert response.status_code == 200 + assert response.get_data(as_text=True) == "Manage Members Page" + + render_mock.assert_called_once_with( + "main/manageMembers.html", + members=fake_members_with_counts, + department=fake_department, + currentSupervisor=fake_supervisor, + currentAcademicYear="2026-2027", + ) + + +@pytest.mark.integration +def test_manage_members_redirects_student(client, monkeypatch): + fake_student = SimpleNamespace( + ID="B00000100", + ) + + fake_user = SimpleNamespace( + supervisor=None, + student=fake_student, + isLaborAdmin=False, + ) + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal, + "url_for", + lambda endpoint, **values: + f"/laborhistory/{values['id']}", + ) + + response = client.get( + "/department/2114/60000/members" + ) + + assert response.status_code == 302 + assert response.location.endswith( + "/laborhistory/B00000100" + ) + + +@pytest.mark.integration +def test_manage_members_returns_403_for_unauthorized_user( + client, + monkeypatch, +): + fake_user = SimpleNamespace( + supervisor=None, + student=None, + isLaborAdmin=False, + ) + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal, + "render_template", + lambda template: "Forbidden", + ) + + response = client.get( + "/department/2114/60000/members" + ) + + assert response.status_code == 403 + assert response.get_data(as_text=True) == "Forbidden" + + +@pytest.mark.integration +def test_manage_members_returns_403_when_user_is_none( + client, + monkeypatch, +): + monkeypatch.setattr( + department_portal, + "require_login", + lambda: None, + ) + + monkeypatch.setattr( + department_portal, + "render_template", + lambda template: "Forbidden", + ) + + response = client.get( + "/department/2114/60000/members" + ) + + assert response.status_code == 403 + assert response.get_data(as_text=True) == "Forbidden" + + +# ------------------------------------------------------------------- +# searchMember tests +# ------------------------------------------------------------------- + + +@pytest.mark.integration +def test_search_member_by_bnumber(client, monkeypatch): + fake_user = SimpleNamespace( + supervisor="B00000001", + student=None, + isLaborAdmin=False, + ) + + fake_database_supervisor = SimpleNamespace( + ID="B00000002", + ) + + fake_query = MagicMock() + fake_query.where.return_value = [ + fake_database_supervisor, + ] + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "select", + lambda: fake_query, + ) + + monkeypatch.setattr( + department_portal, + "supervisorsDbToDict", + lambda supervisor: { + "bnumber": supervisor.ID, + "firstName": "John", + "lastName": "Scott", + }, + ) + + response = client.get( + "/members/search/B00000002" + ) + + assert response.status_code == 200 + + assert response.get_json() == [ + { + "bnumber": "B00000002", + "firstName": "John", + "lastName": "Scott", + } + ] + + +@pytest.mark.integration +def test_search_member_by_single_name(client, monkeypatch): + fake_user = SimpleNamespace( + supervisor="B00000001", + student=None, + isLaborAdmin=False, + ) + + fake_database_supervisor = SimpleNamespace( + ID="B00000003", + ) + + fake_query = MagicMock() + fake_query.where.return_value = [ + fake_database_supervisor, + ] + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "select", + lambda: fake_query, + ) + + monkeypatch.setattr( + department_portal, + "supervisorsDbToDict", + lambda supervisor: { + "bnumber": supervisor.ID, + "firstName": "Mary", + "lastName": "Johnson", + }, + ) + + response = client.get( + "/members/search/Mary" + ) + + assert response.status_code == 200 + + assert response.get_json() == [ + { + "bnumber": "B00000003", + "firstName": "Mary", + "lastName": "Johnson", + } + ] + + +@pytest.mark.integration +def test_search_member_by_full_name(client, monkeypatch): + fake_user = SimpleNamespace( + supervisor="B00000001", + student=None, + isLaborAdmin=False, + ) + + fake_database_supervisor = SimpleNamespace( + ID="B00000004", + ) + + fake_query = MagicMock() + fake_query.where.return_value = [ + fake_database_supervisor, + ] + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "select", + lambda: fake_query, + ) + + monkeypatch.setattr( + department_portal, + "supervisorsDbToDict", + lambda supervisor: { + "bnumber": supervisor.ID, + "firstName": "James", + "lastName": "Smith", + }, + ) + + response = client.get( + "/members/search/James%20Smith" + ) + + assert response.status_code == 200 + + assert response.get_json() == [ + { + "bnumber": "B00000004", + "firstName": "James", + "lastName": "Smith", + } + ] + + +@pytest.mark.integration +def test_search_member_removes_duplicates(client, monkeypatch): + fake_user = SimpleNamespace( + supervisor="B00000001", + student=None, + isLaborAdmin=False, + ) + + duplicate_supervisor_one = SimpleNamespace( + ID="B00000002", + ) + + duplicate_supervisor_two = SimpleNamespace( + ID="B00000002", + ) + + fake_query = MagicMock() + fake_query.where.return_value = [ + duplicate_supervisor_one, + duplicate_supervisor_two, + ] + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "select", + lambda: fake_query, + ) + + monkeypatch.setattr( + department_portal, + "supervisorsDbToDict", + lambda supervisor: { + "bnumber": supervisor.ID, + "firstName": "John", + "lastName": "Scott", + }, + ) + + response = client.get( + "/members/search/B00000002" + ) + + assert response.status_code == 200 + assert len(response.get_json()) == 1 + + +@pytest.mark.integration +def test_search_member_returns_403_for_unauthorized_user( + client, + monkeypatch, +): + fake_user = SimpleNamespace( + supervisor=None, + student=None, + isLaborAdmin=False, + ) + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal, + "render_template", + lambda template: "Forbidden", + ) + + response = client.get( + "/members/search/Scott" + ) + + assert response.status_code == 403 + assert response.get_data(as_text=True) == "Forbidden" + + +@pytest.mark.integration +def test_search_member_allows_labor_admin( + client, + monkeypatch, +): + fake_user = SimpleNamespace( + supervisor=None, + student=None, + isLaborAdmin=True, + ) + + fake_query = MagicMock() + fake_query.where.return_value = [] + + monkeypatch.setattr( + department_portal, + "require_login", + lambda: fake_user, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "select", + lambda: fake_query, + ) + + response = client.get( + "/members/search/Unknown" + ) + + assert response.status_code == 200 + assert response.get_json() == [] + + +# ------------------------------------------------------------------- +# coordinatorSwitch tests +# ------------------------------------------------------------------- + + +@pytest.mark.integration +def test_coordinator_switch_assigns_coordinator( + client, + monkeypatch, +): + fake_member = SimpleNamespace( + isCoordinator=False, + save=MagicMock(), + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get", + lambda *args, **kwargs: fake_member, + ) + + with client.session_transaction() as test_session: + test_session["current_department_id"] = 10 + + response = client.post( + "/members/coordinator_switch", + json={ + "supervisorID": "B00000001", + "isCoordinator": True, + }, + ) + + assert response.status_code == 200 + assert fake_member.isCoordinator is True + fake_member.save.assert_called_once() + + +@pytest.mark.integration +def test_coordinator_switch_removes_coordinator( + client, + monkeypatch, +): + fake_member = SimpleNamespace( + isCoordinator=True, + save=MagicMock(), + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get", + lambda *args, **kwargs: fake_member, + ) + + with client.session_transaction() as test_session: + test_session["current_department_id"] = 10 + + response = client.post( + "/members/coordinator_switch", + json={ + "supervisorID": "B00000001", + "isCoordinator": False, + }, + ) + + assert response.status_code == 200 + assert fake_member.isCoordinator is False + fake_member.save.assert_called_once() + + +@pytest.mark.integration +def test_coordinator_switch_requires_department_session( + client, +): + response = client.post( + "/members/coordinator_switch", + json={ + "supervisorID": "B00000001", + "isCoordinator": True, + }, + ) + + assert response.status_code == 400 + + +# ------------------------------------------------------------------- +# eligibility switch tests +# ------------------------------------------------------------------- + + +@pytest.mark.integration +def test_eligibility_switch_bans_supervisor( + client, + monkeypatch, +): + fake_supervisor = SimpleNamespace( + ID="B00000001", + isBanned=False, + save=MagicMock(), + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "get", + lambda *args, **kwargs: fake_supervisor, + ) + + response = client.post( + "/members/ban_switch", + json={ + "supervisorID": "B00000001", + }, + ) + + assert response.status_code == 200 + assert fake_supervisor.isBanned is True + fake_supervisor.save.assert_called_once() + + +@pytest.mark.integration +def test_eligibility_switch_unbans_supervisor( + client, + monkeypatch, +): + fake_supervisor = SimpleNamespace( + ID="B00000001", + isBanned=True, + save=MagicMock(), + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "get", + lambda *args, **kwargs: fake_supervisor, + ) + + response = client.post( + "/members/ban_switch", + json={ + "supervisorID": "B00000001", + }, + ) + + assert response.status_code == 200 + assert fake_supervisor.isBanned is False + fake_supervisor.save.assert_called_once() + + +# ------------------------------------------------------------------- +# removeMember tests +# ------------------------------------------------------------------- + + +@pytest.mark.integration +def test_remove_member(client, monkeypatch): + fake_member = SimpleNamespace( + delete_instance=MagicMock(), + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get", + lambda *args, **kwargs: fake_member, + ) + + with client.session_transaction() as test_session: + test_session["current_department_id"] = 10 + + response = client.delete( + "/members/remove", + json={ + "supervisorID": "B00000001", + }, + ) + + assert response.status_code == 200 + fake_member.delete_instance.assert_called_once() + + +# ------------------------------------------------------------------- +# addUserToDept tests +# ------------------------------------------------------------------- + + +@pytest.mark.integration +def test_add_existing_supervisor_to_department( + client, + monkeypatch, +): + existing_supervisor = SimpleNamespace( + ID="B00000001", + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get_or_none", + lambda *args, **kwargs: None, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "get_or_none", + lambda *args, **kwargs: existing_supervisor, + ) + + create_mock = MagicMock() + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "create", + create_mock, + ) + + tracy_mock = MagicMock() + + monkeypatch.setattr( + department_portal, + "createSupervisorFromTracy", + tracy_mock, + ) + + response = client.post( + "/members/add", + data={ + "supervisorID": "B00000001", + "departmentID": "10", + }, + ) + + assert response.status_code == 200 + assert response.get_data(as_text=True) == "True" + + tracy_mock.assert_not_called() + + create_mock.assert_called_once_with( + supervisor="B00000001", + department="10", + ) + + +@pytest.mark.integration +def test_add_new_supervisor_to_department( + client, + monkeypatch, +): + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get_or_none", + lambda *args, **kwargs: None, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "get_or_none", + lambda *args, **kwargs: None, + ) + + tracy_mock = MagicMock() + create_mock = MagicMock() + + monkeypatch.setattr( + department_portal, + "createSupervisorFromTracy", + tracy_mock, + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "create", + create_mock, + ) + + response = client.post( + "/members/add", + data={ + "supervisorID": "B00000001", + "departmentID": "10", + }, + ) + + assert response.status_code == 200 + assert response.get_data(as_text=True) == "True" + + tracy_mock.assert_called_once_with( + bnumber="B00000001" + ) + + create_mock.assert_called_once_with( + supervisor="B00000001", + department="10", + ) + + +@pytest.mark.integration +def test_add_user_rejects_duplicate_member( + client, + monkeypatch, +): + existing_record = SimpleNamespace( + supervisor="B00000001", + department=10, + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get_or_none", + lambda *args, **kwargs: existing_record, + ) + + create_mock = MagicMock() + tracy_mock = MagicMock() + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "create", + create_mock, + ) + + monkeypatch.setattr( + department_portal, + "createSupervisorFromTracy", + tracy_mock, + ) + + response = client.post( + "/members/add", + data={ + "supervisorID": "B00000001", + "departmentID": "10", + }, + ) + + assert response.status_code == 200 + assert response.get_data(as_text=True) == "False" + + create_mock.assert_not_called() + tracy_mock.assert_not_called() + + +@pytest.mark.integration +def test_add_user_returns_500_when_creation_fails( + client, + monkeypatch, +): + existing_supervisor = SimpleNamespace( + ID="B00000001", + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "get_or_none", + lambda *args, **kwargs: None, + ) + + monkeypatch.setattr( + department_portal.Supervisor, + "get_or_none", + lambda *args, **kwargs: existing_supervisor, + ) + + monkeypatch.setattr( + department_portal.SupervisorDepartment, + "create", + MagicMock( + side_effect=Exception("Database failed") + ), + ) + + response = client.post( + "/members/add", + data={ + "supervisorID": "B00000001", + "departmentID": "10", + }, + ) + + assert response.status_code == 500 \ No newline at end of file From f5a692482f51d66a523751903fc5adbb836b89eb Mon Sep 17 00:00:00 2001 From: feitsopb Date: Wed, 22 Jul 2026 17:22:35 -0400 Subject: [PATCH 30/88] edited the message and then made it to fade out after 5seconds --- app/templates/main/manageMembers.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html index e0e49e918..7e2a05ed4 100644 --- a/app/templates/main/manageMembers.html +++ b/app/templates/main/manageMembers.html @@ -19,7 +19,11 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc {%if currentSupervisor.isBanned %}

- + + {%endif%}

From fd37b4828e4c907e9bf80284661ad886508ec87d Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Mon, 27 Jul 2026 09:25:13 -0400 Subject: [PATCH 31/88] Handle missing user in manage members route --- app/controllers/main_routes/departmentPortal.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py index 339f331b4..f78da34b5 100644 --- a/app/controllers/main_routes/departmentPortal.py +++ b/app/controllers/main_routes/departmentPortal.py @@ -29,7 +29,10 @@ def manageMembers(org=None, account=None): """Generates the Manage Members page.""" currentUser = require_login() - if not currentUser or not currentUser.supervisor: + if currentUser is None: + return render_template('errors/403.html'), 403 + + if not currentUser.supervisor: if currentUser.student: return redirect(url_for('main.laborhistory', id=currentUser.student.ID)) return render_template('errors/403.html'), 403 From 6fd34413748dc961e89d92a7a7a7bebf6f2edadc Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Mon, 27 Jul 2026 09:51:15 -0400 Subject: [PATCH 32/88] Fix manage members template context and markup --- app/static/js/addSupervisorsToDepartment.js | 31 +++++++++++++++++++++ app/templates/main/manageMembers.html | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 app/static/js/addSupervisorsToDepartment.js diff --git a/app/static/js/addSupervisorsToDepartment.js b/app/static/js/addSupervisorsToDepartment.js new file mode 100644 index 000000000..16c3f1d85 --- /dev/null +++ b/app/static/js/addSupervisorsToDepartment.js @@ -0,0 +1,31 @@ +function addSupervisorToDepartment(supervisorID, departmentID, callback=() => {}) { + return $.ajax({ + method: "POST", + url: `/supervisorPortal/addUserToDept`, + data: {"supervisorID": supervisorID, "departmentID": departmentID}, + success: function(response) { + if (response == "True") { + msgFlash("Supervisor has been added to department.", "success") + clearDropdowns() + } else { + msgFlash("Supervisor is already a member of this department.", "warning") + clearDropdowns() + } + if (callback){ + callback() + } + }, + error: function() { + msgFlash("Failed to add supervisor, please try again.", "fail") + clearDropdowns() + }, + }) + +} + +function clearDropdowns(){ + $('select.selectpicker').each(function() { + $(`#${this.id} option:eq(0)`).prop("selected", true); + $(`#${this.id}`).selectpicker("refresh"); + }); +}; \ No newline at end of file diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html index 7e2a05ed4..9670e1e26 100644 --- a/app/templates/main/manageMembers.html +++ b/app/templates/main/manageMembers.html @@ -29,7 +29,7 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc
Click to Skip -
+
+ @@ -94,7 +94,7 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc - {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + {% if g.currentUser.isLaborAdmin or g.currentUser.isLaborDepartmentStudent %}
{% if not i['isBanned'] %} @@ -122,6 +122,7 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc {% else %} @@ -113,8 +113,8 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc type="button" class="btn btn-danger member-status-btn" align="center" - data-supervisor="{{i['supervisor']}}" - data-member-name="{{ i['legal_name'] + ' ' + i['LAST_NAME'] }}" + data-supervisor="{{member.supervisor.ID }}" + data-member-name="{{ member.supervisor.legal_name }} {{ member.supervisor.LAST_NAME }}" value="Ineligible"> Ineligible {% endif %} diff --git a/tests/code/test_manageMembers.py b/tests/code/test_manageMembers.py index 31e72d120..555fb999e 100644 --- a/tests/code/test_manageMembers.py +++ b/tests/code/test_manageMembers.py @@ -23,16 +23,19 @@ from app.models.supervisorDepartment import SupervisorDepartment from app.models.term import Term + @pytest.mark.integration def test_getCurrentDepartment(): with mainDB.atomic() as transaction: - testDept = Department.create(ORG = 2114, - ACCOUNT = "60000", - DEPT_NAME = "Computer Science") + testDept = Department.create( + ORG=2114, + ACCOUNT="60000", + DEPT_NAME="Computer Science" + ) with app.test_request_context(): - # Case 1: verify the department is found and stashed in session - dept = getCurrentDepartment(org = 2114, account = "60000") + # Case 1: verify the department is found + dept = getCurrentDepartment(org=2114, account="60000") assert dept.departmentID == testDept.departmentID assert dept.DEPT_NAME == "Computer Science" @@ -40,7 +43,7 @@ def test_getCurrentDepartment(): with app.test_request_context(): # Case 2: confirm a non-existent org/account combo 404s with pytest.raises(NotFound): - getCurrentDepartment(org = 9999, account = "00000") + getCurrentDepartment(org=9999, account="00000") transaction.rollback() @@ -48,31 +51,39 @@ def test_getCurrentDepartment(): @pytest.mark.integration def test_getDepartmentMembers(): with mainDB.atomic() as transaction: - testDept = Department.create(ORG = 2114, - ACCOUNT = "60000", - DEPT_NAME = "Computer Science") + testDept = Department.create( + ORG=2114, + ACCOUNT="60000", + DEPT_NAME="Computer Science" + ) - testingSupervisor = Supervisor.create(ID = "B00000001", - PIDM = 75, - legal_name = "Not", - LAST_NAME = "Scott", - EMAIL = "None", - CPO = "None", - DEPT_NAME = "Computer Science") + testingSupervisor = Supervisor.create( + ID="B00000001", + PIDM=75, + legal_name="Not", + LAST_NAME="Scott", + EMAIL="None", + CPO="None", + DEPT_NAME="Computer Science" + ) - SupervisorDepartment.create(supervisor = testingSupervisor.ID, - department = testDept.departmentID) + SupervisorDepartment.create( + supervisor=testingSupervisor.ID, + department=testDept.departmentID + ) with app.test_request_context(): # Case 1: confirm the supervisor tied to the department comes back members = getDepartmentMembers(testDept) assert len(members) == 1 - assert members[0]['supervisor'] == testingSupervisor.ID - assert members[0]['LAST_NAME'] == "Scott" + assert members[0].supervisor.ID == testingSupervisor.ID + assert members[0].supervisor.LAST_NAME == "Scott" + assert members[0].department_id == testDept.departmentID transaction.rollback() - + + def test_supervisorsDbToDict_formats_supervisor_data(): supervisor = SimpleNamespace( EMAIL=" scott.heggen@berea.edu ", @@ -118,10 +129,10 @@ def today(): def test_attachPositionCounts_adds_existing_counts(): members = [ - { - "department": 1, - "supervisor": "B00000001" - } + SimpleNamespace( + department_id=1, + supervisor_id="B00000001" + ) ] counts = { @@ -135,26 +146,26 @@ def test_attachPositionCounts_adds_existing_counts(): result = attachPositionCounts(members, counts) - assert result[0]["active_primary_positions"] == 2 - assert result[0]["pending_primary_positions"] == 1 - assert result[0]["active_secondary_positions"] == 3 - assert result[0]["pending_secondary_positions"] == 4 + assert result[0].active_primary_positions == 2 + assert result[0].pending_primary_positions == 1 + assert result[0].active_secondary_positions == 3 + assert result[0].pending_secondary_positions == 4 def test_attachPositionCounts_defaults_missing_counts_to_zero(): members = [ - { - "department": 1, - "supervisor": "B00000001" - } + SimpleNamespace( + department_id=1, + supervisor_id="B00000001" + ) ] result = attachPositionCounts(members, {}) - assert result[0]["active_primary_positions"] == 0 - assert result[0]["pending_primary_positions"] == 0 - assert result[0]["active_secondary_positions"] == 0 - assert result[0]["pending_secondary_positions"] == 0 + assert result[0].active_primary_positions == 0 + assert result[0].pending_primary_positions == 0 + assert result[0].active_secondary_positions == 0 + assert result[0].pending_secondary_positions == 0 @pytest.mark.integration @@ -258,7 +269,8 @@ def test_getStudentCounts_counts_active_and_pending_positions(): assert row["pending_secondary_positions"] == 1 transaction.rollback() - + + @pytest.mark.integration def test_getStudentCounts_excludes_released_forms(): with mainDB.atomic() as transaction: From 1a923d3f36e127b20010aae9930be68f165eb2d6 Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Mon, 27 Jul 2026 13:11:27 -0400 Subject: [PATCH 37/88] Clean up manage members review changes --- app/logic/manageMembers.py | 4 ++-- app/templates/main/manageMembers.html | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/logic/manageMembers.py b/app/logic/manageMembers.py index ce886454b..e9010d209 100644 --- a/app/logic/manageMembers.py +++ b/app/logic/manageMembers.py @@ -39,7 +39,7 @@ def currentAcademicYear(): return currentAcademicYear def getCurrentDepartment(org, account): - """Look up the department by org/account, stash it in session, or 404.""" + """Look up the department by org/account, or 404.""" try: return Department.get( Department.ORG == org, @@ -50,7 +50,7 @@ def getCurrentDepartment(org, account): def getDepartmentMembers(dept): - """Supervisor-department rows for a department, as dicts.""" + """Supervisor-department rows for a department""" return list( SupervisorDepartment.select(SupervisorDepartment, Supervisor) .join(Supervisor) diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html index a27ad0880..bda69ea5b 100644 --- a/app/templates/main/manageMembers.html +++ b/app/templates/main/manageMembers.html @@ -77,8 +77,8 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc {% if (member.active_primary_positions == 0 and member.active_secondary_positions == 0) and (member.pending_primary_positions == 0 and member.pending_secondary_positions == 0) %} None {% else %} - Primary: {{member.active_primary_positions}} active, {{member.pending_primary_positions]}} pending
- Secondary: {{member.active_secondary_positions ]}} active, {{member.pending_secondary_positions }} pending + Primary: {{member.active_primary_positions}} active, {{member.pending_primary_positions}} pending
+ Secondary: {{member.active_secondary_positions }} active, {{member.pending_secondary_positions }} pending {% endif %} @@ -121,9 +121,9 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc + data-member-name="{{ member.supervisor.legal_name }} {{ member.supervisor.LAST_NAME }}"
From f648de838ee5dfb8825b5b25aa33419878962b9f Mon Sep 17 00:00:00 2001 From: feitsopb Date: Tue, 28 Jul 2026 11:55:28 -0400 Subject: [PATCH 44/88] refactored the formatting of the code --- app/static/js/manageMembers.js | 1 - app/templates/main/manageMembers.html | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/manageMembers.js b/app/static/js/manageMembers.js index edac369f3..54bf26883 100644 --- a/app/static/js/manageMembers.js +++ b/app/static/js/manageMembers.js @@ -91,7 +91,6 @@ $(document).ready(function() { let departmentID = redButton.data("department"); let confirmed = confirm( "Are you sure you want to remove " + memberName + " from this department?" ); - // Stop here if the user clicks Cancel if (!confirmed) { return; diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html index 873f038d0..6d2cf6de1 100644 --- a/app/templates/main/manageMembers.html +++ b/app/templates/main/manageMembers.html @@ -124,6 +124,7 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc data-supervisor="{{member.supervisor.ID }}" data-department="{{ department.departmentID }}" data-member-name="{{ member.supervisor.legal_name }} {{ member.supervisor.LAST_NAME }}"> + From fd165410a0ad5fc4b33f6457e8e53584cb99d428 Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 29 Jul 2026 10:39:47 -0400 Subject: [PATCH 45/88] Address manage members review comments --- app/controllers/main_routes/__init__.py | 13 ++- .../main_routes/departmentPortal.py | 32 +++---- app/logic/manageMembers.py | 94 ++++++++----------- .../{managemembers.css => manageMembers.css} | 17 +--- app/static/js/manageMembers.js | 3 +- app/templates/main/manageMembers.html | 15 +-- 6 files changed, 73 insertions(+), 101 deletions(-) rename app/static/css/{managemembers.css => manageMembers.css} (81%) diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index da3323908..ff662e435 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -1,3 +1,4 @@ +from datetime import date from flask import render_template from flask import Blueprint from app.login_manager import require_login @@ -5,13 +6,23 @@ main_bp = Blueprint('main', __name__) @main_bp.context_processor +def currentAcademicYear(): + today = date.today() + currentYear = today.year + + if today.month < 7: + return currentYear - 1, currentYear + + return currentYear, currentYear + 1 + def injectGlobalData(): currentUser = require_login() lastStaticUpdate = str(max(os.path.getmtime(os.path.join(root_path, f)) for root_path, dirs, files in os.walk('app/static') for f in files)) return {'currentUser': currentUser, - 'lastStaticUpdate': lastStaticUpdate} + 'lastStaticUpdate': lastStaticUpdate, + 'currentAcademicYear': currentAcademicYear()} from app.controllers.main_routes import main_routes from app.controllers.main_routes import departmentPortal diff --git a/app/controllers/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py index c5c8a1ede..065c3d9bc 100644 --- a/app/controllers/main_routes/departmentPortal.py +++ b/app/controllers/main_routes/departmentPortal.py @@ -1,19 +1,12 @@ from flask import g, jsonify, redirect, render_template, request, url_for from app.controllers.main_routes import main_bp -from app.logic.manageMembers import ( - attachPositionCounts, - canManageMembers, - currentAcademicYear, - getCurrentDepartment, - getDepartmentMembers, - getStudentCounts, - supervisorsDbToDict, -) -from app.logic.search import searchPerson -from app.logic.userInsertFunctions import createSupervisorFromTracy from app.models.supervisor import Supervisor from app.models.supervisorDepartment import SupervisorDepartment +from app.logic.manageMembers import * +from app.logic.search import searchPerson +from app.logic.userInsertFunctions import createSupervisorFromTracy + @main_bp.route('/department///members', methods=['GET']) def manageMembers(org=None, account=None): @@ -22,9 +15,11 @@ def manageMembers(org=None, account=None): if not currentUser.supervisor: return redirect(url_for('main.laborhistory', id=currentUser.student.ID)) + + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): + return render_template('errors/403.html'), 403 - dept = getCurrentDepartment(org, account) - members = getDepartmentMembers(dept) + dept, members = getCurrentDeptMembers(org, account) counts = getStudentCounts(dept) members = attachPositionCounts(members, counts) @@ -32,7 +27,6 @@ def manageMembers(org=None, account=None): 'main/manageMembers.html', members=members, department=dept, - currentAcademicYear=currentAcademicYear() ) @@ -44,7 +38,7 @@ def searchMember(query=None): """ currentUser = g.currentUser - if not canManageMembers(currentUser): + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): return render_template('errors/403.html'), 403 supervisors = ( @@ -64,7 +58,7 @@ def updateCoordinator(): """ currentUser = g.currentUser - if not canManageMembers(currentUser): + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): return render_template('errors/403.html'), 403 supervisorID = request.form.get("supervisorID") @@ -92,7 +86,7 @@ def updateEligibility(): """ currentUser = g.currentUser - if not canManageMembers(currentUser): + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): return render_template('errors/403.html'), 403 supervisorID = request.form.get("supervisorID") @@ -114,7 +108,7 @@ def removeMember(): """ currentUser = g.currentUser - if not canManageMembers(currentUser): + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): return render_template('errors/403.html'), 403 supervisorID = request.form.get("supervisorID") @@ -141,7 +135,7 @@ def addUserToDept(): """ currentUser = g.currentUser - if not canManageMembers(currentUser): + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): return render_template('errors/403.html'), 403 supervisorID = request.form.get("supervisorID") diff --git a/app/logic/manageMembers.py b/app/logic/manageMembers.py index e9010d209..af0a4dd8b 100644 --- a/app/logic/manageMembers.py +++ b/app/logic/manageMembers.py @@ -23,46 +23,30 @@ def supervisorsDbToDict(supervisor): 'type': 'Supervisor'} return dbToDict -def currentAcademicYear(): - """ - Finds the current academic year - """ - today = date.today() - currentYear = today.year - if today.month < 7: - currentAcademicYear = (currentYear - 1, currentYear) - else: - currentAcademicYear = (currentYear, currentYear + 1) - # Note that the start of July is - # normally considered the start of a new academic year. - - return currentAcademicYear - -def getCurrentDepartment(org, account): - """Look up the department by org/account, or 404.""" +def getCurrentDeptMembers(org, account): + """Return the current department and its supervisor-department rows.""" try: - return Department.get( - Department.ORG == org, - Department.ACCOUNT == account - ) + return Department.get( Department.ORG == org, Department.ACCOUNT == account) except (NameError, DoesNotExist): abort(404) - - -def getDepartmentMembers(dept): - """Supervisor-department rows for a department""" - return list( - SupervisorDepartment.select(SupervisorDepartment, Supervisor) + + members = list( + SupervisorDepartment + .select(SupervisorDepartment, Supervisor) .join(Supervisor) .where(SupervisorDepartment.department == dept) ) + return dept, members + -def getReleasedFormIds(): - """formIDs for labor release forms already in effect as of today.""" - today = date.today() - return ( - FormHistory.select(FormHistory.formID) +def getStudentCounts(dept): + """Active/pending primary/secondary position counts, keyed by (dept, supervisor).""" + today = date.today() + + releasedFormIds = ( + FormHistory + .select(FormHistory.formID) .join(LaborReleaseForm) .where( (FormHistory.historyType == "Labor Release Form") & @@ -71,31 +55,41 @@ def getReleasedFormIds(): ) ) - -def getStudentCounts(dept): - """Active/pending primary/secondary position counts, keyed by (dept, supervisor).""" - releasedFormIds = getReleasedFormIds() - - activePrimaries = (LaborStatusForm.jobType == 'Primary') & (LaborStatusForm.studentConfirmation == True) - pendingPrimaries = (LaborStatusForm.jobType == 'Primary') & (LaborStatusForm.studentConfirmation.is_null(True)) - activeSecondaries = (LaborStatusForm.jobType == 'Secondary') & (LaborStatusForm.studentConfirmation == True) - pendingSecondaries = (LaborStatusForm.jobType == 'Secondary') & (LaborStatusForm.studentConfirmation.is_null(True)) + activePrimaries = ( + (LaborStatusForm.jobType == 'Primary') & + (LaborStatusForm.studentConfirmation == True) + ) + pendingPrimaries = ( + (LaborStatusForm.jobType == 'Primary') & + (LaborStatusForm.studentConfirmation.is_null(True)) + ) + activeSecondaries = ( + (LaborStatusForm.jobType == 'Secondary') & + (LaborStatusForm.studentConfirmation == True) + ) + pendingSecondaries = ( + (LaborStatusForm.jobType == 'Secondary') & + (LaborStatusForm.studentConfirmation.is_null(True)) + ) rows = list( - LaborStatusForm.select( + LaborStatusForm + .select( fn.SUM(Case(None, ((activePrimaries, 1),), 0)).alias("active_primary_positions"), fn.SUM(Case(None, ((pendingPrimaries, 1),), 0)).alias("pending_primary_positions"), fn.SUM(Case(None, ((activeSecondaries, 1),), 0)).alias("active_secondary_positions"), fn.SUM(Case(None, ((pendingSecondaries, 1),), 0)).alias("pending_secondary_positions"), LaborStatusForm.department, LaborStatusForm.supervisor - ).where( + ) + .where( (LaborStatusForm.department == dept) & (LaborStatusForm.laborStatusFormID.not_in(releasedFormIds)) - ).group_by( - LaborStatusForm.department, LaborStatusForm.supervisor - ).dicts() + ) + .group_by(LaborStatusForm.department, LaborStatusForm.supervisor) + .dicts() ) + return {(row["department"], row["supervisor"]): row for row in rows} @@ -116,11 +110,3 @@ def attachPositionCounts(members, counts): return members -def canManageMembers(currentUser): - """ - Returns True if the current user is allowed to manage department members. - """ - return ( - getattr(currentUser, "isLaborAdmin", False) or - getattr(currentUser, "isLaborDepartmentStudent", False) - ) diff --git a/app/static/css/managemembers.css b/app/static/css/manageMembers.css similarity index 81% rename from app/static/css/managemembers.css rename to app/static/css/manageMembers.css index 5283446cd..9631fec63 100644 --- a/app/static/css/managemembers.css +++ b/app/static/css/manageMembers.css @@ -1,4 +1,3 @@ -/*styles for column alignment in the data table*/ .centered-header { text-align: center !important; } @@ -7,12 +6,10 @@ vertical-align: middle !important; } -/*The Supervisor Eligibility button*/ .member-status-btn { width: 80px; } -/* The add-member search bar*/ #add-member { display: flex; justify-content: center; @@ -34,8 +31,6 @@ } } - -/* The switch container */ .switch { position: relative; display: inline-block; @@ -44,14 +39,12 @@ vertical-align: middle; } -/* Hide default HTML checkbox */ .switch input { opacity: 0; width: 0; height: 0; } -/* The slider track */ .slider { position: absolute; cursor: pointer; @@ -60,7 +53,6 @@ transition: .4s; } -/* The slider knob */ .slider:before { position: absolute; content: ""; @@ -72,17 +64,14 @@ transition: .4s; } -/* Checked state track color */ input:checked + .slider { - background-color: #337ab7 !important; /* Bootstrap 3 Primary Color */ + background-color: #337ab7 !important; } -/* Checked state knob movement */ input:checked + .slider:before { transform: translateX(15px) !important; } -/* Rounded slider track and knob */ .slider.round { border-radius: 21.25px; } @@ -90,14 +79,12 @@ input:checked + .slider:before { border-radius: 50%; } -/* Optional label spacing */ .switch-label { margin-left: 6.25px; vertical-align: middle; font-weight: bold; } -/* hover indicator */ a.hover-indicator { text-decoration: none !important; position: relative; @@ -141,7 +128,6 @@ a.hover-indicator:active { color: #000000; } -/*a "banned" badge displayed under the name of a banned department member*/ .isbanned-badge { position: relative; display: inline-block; @@ -156,7 +142,6 @@ a.hover-indicator:active { line-height: 1.2; } -/* switches inside the data table */ .table-switch { top: 10px; } \ No newline at end of file diff --git a/app/static/js/manageMembers.js b/app/static/js/manageMembers.js index 54bf26883..81163214a 100644 --- a/app/static/js/manageMembers.js +++ b/app/static/js/manageMembers.js @@ -16,6 +16,7 @@ $(document).ready(function() { //dom: '<"top"l>rt<"bottom"p><"clear">' }); $('[data-toggle="popover"]').popover(); + setTimeout(function() { $(".alert-danger").fadeOut();}, 5000); $(document).on("click", ".assign-coordinator", function() { let memberName = $(this).data("member-name"); @@ -46,7 +47,7 @@ $(document).ready(function() { $(document).on("click", ".member-status-btn", function() { let button = $(this); - let ban_badge = $(this).closest("tr").find(".isbanned-badge"); + let banBadge = $(this).closest("tr").find(".isbanned-badge"); let memberName = button.data("member-name"); let supervisorID = button.data("supervisor"); diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html index 6d2cf6de1..4cd0d2fca 100644 --- a/app/templates/main/manageMembers.html +++ b/app/templates/main/manageMembers.html @@ -21,9 +21,6 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc

- {%endif%}

@@ -120,15 +117,13 @@

Academic Year: {{currentAcademicYear[0]}}–{{currentAc {% endif %} - - - - + {% endif %} From 5de642196770e717017d9365d20b2006db8f53c1 Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 29 Jul 2026 10:43:12 -0400 Subject: [PATCH 46/88] Changed g.currentUser to currentUser --- app/controllers/main_routes/main_routes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index 157baf09c..7f162ca58 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -54,6 +54,7 @@ def supervisorPortal(): @main_bp.route('/department/', methods=['GET']) @main_bp.route('/department//', methods=['GET']) def departmentPortal(org=None,account=None): + currentUser = g.currentUser if org and account: try: dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) @@ -64,17 +65,17 @@ def departmentPortal(org=None,account=None): - if g.currentUser.isLaborAdmin: + if currentUser.isLaborAdmin: departments = list(Department.select().order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) else: - departments = list(getDepartmentsForSupervisor(g.currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) + departments = list(getDepartmentsForSupervisor(currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) positionsList, posURL = getActivePositions(dept) return render_template('main/departmentPortal.html', departments = departments, department = dept, - currentUser=g.currentUser, + currentUser=currentUser, positions = positionsList, posURL = posURL) From e3ddeb4317c678ac131978fe7b8725de16a9ae40 Mon Sep 17 00:00:00 2001 From: BhushanSah Date: Wed, 29 Jul 2026 10:57:23 -0400 Subject: [PATCH 47/88] Address remaining manage members review comments --- app/controllers/main_routes/departmentPortal.py | 13 +++---------- app/static/js/manageMembers.js | 4 ++-- app/static/js/supervisorPortal.js | 14 -------------- app/templates/main/manageMembers.html | 2 +- app/templates/main/supervisorPortal.html | 4 ---- database/demo_data.py | 4 ---- 6 files changed, 6 insertions(+), 35 deletions(-) diff --git a/app/controllers/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py index 065c3d9bc..c03b8137c 100644 --- a/app/controllers/main_routes/departmentPortal.py +++ b/app/controllers/main_routes/departmentPortal.py @@ -5,7 +5,6 @@ from app.models.supervisorDepartment import SupervisorDepartment from app.logic.manageMembers import * from app.logic.search import searchPerson -from app.logic.userInsertFunctions import createSupervisorFromTracy @main_bp.route('/department///members', methods=['GET']) @@ -145,21 +144,15 @@ def addUserToDept(): return "", 400 try: - supervisorDeptRecord = SupervisorDepartment.get_or_none( - supervisor=supervisorID, - department=departmentID - ) + supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor=supervisorID, department=departmentID) if supervisorDeptRecord: return "False" if not Supervisor.get_or_none(Supervisor.ID == supervisorID): - createSupervisorFromTracy(bnumber=supervisorID) + return "", 400 - SupervisorDepartment.create( - supervisor=supervisorID, - department=departmentID - ) + SupervisorDepartment.create(supervisor=supervisorID, department=departmentID) return "True" diff --git a/app/static/js/manageMembers.js b/app/static/js/manageMembers.js index 81163214a..17bdedc16 100644 --- a/app/static/js/manageMembers.js +++ b/app/static/js/manageMembers.js @@ -65,14 +65,14 @@ $(document).ready(function() { button.removeClass("btn-success").addClass("btn-danger"); button.text("Ineligible"); button.val("Ineligible"); - ban_badge.css("visibility", "visible"); + banBadge.css("visibility", "visible"); $("#flash_container").html("
" + memberName + " is no longer an eligible coordinator, so they cannot add students or create labor status forms .
"); $("#flasher").delay(3000).fadeOut(); } else { button.removeClass("btn-danger").addClass("btn-success"); button.html("Eligible"); button.val("Eligible"); - ban_badge.css("visibility", "hidden"); + banBadge.css("visibility", "hidden"); $("#flash_container").html("
" + memberName + " is an eligible coordinator now; they can create labor status forms and add new students.
"); $("#flasher").delay(3000).fadeOut(); } diff --git a/app/static/js/supervisorPortal.js b/app/static/js/supervisorPortal.js index a4810ebb4..e62046cc5 100644 --- a/app/static/js/supervisorPortal.js +++ b/app/static/js/supervisorPortal.js @@ -25,10 +25,6 @@ $(document).ready(function () { $('#sortOptions').show(); }); - $('#addUserToDept').on('click', function () { - $("#addSupervisorToDeptModal").modal("show"); - $('#addUser').prop('disabled', true) - }) $("#sortByButton").on('click', function () { var isDisabled = $('#fieldPicker').prop('disabled'); if (!isDisabled && $('#fieldPicker').val() == '') { @@ -43,16 +39,6 @@ $(document).ready(function () { $('.selectpicker').selectpicker('refresh') } - $('#addUser').on('click', function () { - let supervisorID = $('#supervisorModalSelect :selected').val() - let departmentID = $('#departmentModalSelect :selected').val() - - addSupervisorToDepartment(supervisorID, departmentID) - }) - - $('#departmentModalSelect').on('change', disableButtonHandler) - $('#supervisorModalSelect').on('change', disableButtonHandler) - $('#clearSelectionsButton').on('click', function () { $("input:checkbox").removeAttr("checked"); clearDropdowns() diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html index 4cd0d2fca..8cd02c341 100644 --- a/app/templates/main/manageMembers.html +++ b/app/templates/main/manageMembers.html @@ -3,7 +3,7 @@ {% block styles %} {{super()}} - + {% endblock %} {% block scripts %} diff --git a/app/templates/main/supervisorPortal.html b/app/templates/main/supervisorPortal.html index 78383566c..5e3c26720 100644 --- a/app/templates/main/supervisorPortal.html +++ b/app/templates/main/supervisorPortal.html @@ -28,8 +28,6 @@ src="{{url_for('static', filename='js/allPendingForms.js')}}?u={{lastStaticUpdate}}"> -