04/09 update: redesign user's roles

This commit is contained in:
2026-04-09 17:36:32 -04:00
parent 857372f781
commit dba4cc8b96
29 changed files with 194 additions and 114 deletions
+6 -6
View File
@@ -4,7 +4,7 @@ from urllib.parse import urlparse
from app import db
from app.models.user import User
from app.utils.forms import LoginForm, UserForm, ProfileForm
from app.utils.decorators import admin_required
from app.utils.decorators import admin_required, supervisor_required
import logging
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_LOGIN, ACTION_LOGOUT
@@ -127,7 +127,7 @@ def profile():
@bp.route('/users')
@login_required
@admin_required
@supervisor_required
def list_users():
# Exclude customer accounts — those are managed exclusively via /customers
users = (
@@ -143,7 +143,7 @@ def list_users():
@bp.route('/users/new', methods=['GET', 'POST'])
@login_required
@admin_required
@supervisor_required
def create_user():
form = UserForm()
@@ -169,7 +169,7 @@ def create_user():
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@login_required
@admin_required
@supervisor_required
def edit_user(user_id):
user = User.query.get_or_404(user_id)
form = UserForm(user=user, obj=user)
@@ -196,7 +196,7 @@ def edit_user(user_id):
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
@login_required
@admin_required
@supervisor_required
def delete_user(user_id):
user = User.query.get_or_404(user_id)
@@ -227,7 +227,7 @@ def delete_user(user_id):
@bp.route('/users/<int:user_id>/toggle-active', methods=['POST'])
@login_required
@admin_required
@supervisor_required
def toggle_active(user_id):
user = User.query.get_or_404(user_id)
+12 -12
View File
@@ -20,7 +20,7 @@ from app.models.user import User
from app.models.project import Project, CustomerAssignment
from app.models.facility import Facility
from app.utils.forms import CustomerUserForm, CustomerAssignmentForm, CustomerInviteForm, SetPasswordForm
from app.utils.decorators import admin_required
from app.utils.decorators import admin_required, supervisor_required
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
from app.utils.scope import get_customer_scope
@@ -33,7 +33,7 @@ bp = Blueprint('customers', __name__, url_prefix='/customers')
@bp.route('/')
@login_required
@admin_required
@supervisor_required
def index():
"""Consolidated customer management dashboard."""
customers = (
@@ -101,7 +101,7 @@ def index():
@bp.route('/new', methods=['GET', 'POST'])
@login_required
@admin_required
@supervisor_required
def create():
"""Create a customer account via email invitation.
@@ -235,7 +235,7 @@ def _send_invite_email(user, token):
@bp.route('/<int:customer_id>/resend-invite', methods=['POST'])
@login_required
@admin_required
@supervisor_required
def resend_invite(customer_id):
"""Generate a fresh token and resend the set-password invitation email."""
customer = User.query.get_or_404(customer_id)
@@ -295,7 +295,7 @@ def set_password(token):
@bp.route('/<int:customer_id>/edit', methods=['GET', 'POST'])
@login_required
@admin_required
@supervisor_required
def edit(customer_id):
customer = User.query.get_or_404(customer_id)
if customer.role != 'customer':
@@ -326,7 +326,7 @@ def edit(customer_id):
@bp.route('/<int:customer_id>')
@login_required
@admin_required
@supervisor_required
def manage(customer_id):
"""Single-customer detail page: profile + all assignments."""
customer = User.query.get_or_404(customer_id)
@@ -363,7 +363,7 @@ def manage(customer_id):
@bp.route('/<int:customer_id>/assignments/add', methods=['POST'])
@login_required
@admin_required
@supervisor_required
def add_assignment(customer_id):
customer = User.query.get_or_404(customer_id)
if customer.role != 'customer':
@@ -412,7 +412,7 @@ def add_assignment(customer_id):
@bp.route('/assignments/<int:assignment_id>/remove', methods=['POST'])
@login_required
@admin_required
@supervisor_required
def remove_assignment(assignment_id):
assignment = CustomerAssignment.query.get_or_404(assignment_id)
customer_id = assignment.user_id
@@ -437,7 +437,7 @@ def remove_assignment(assignment_id):
@bp.route('/<int:customer_id>/toggle-active', methods=['POST'])
@login_required
@admin_required
@supervisor_required
def toggle_active(customer_id):
customer = User.query.get_or_404(customer_id)
if customer.role != 'customer':
@@ -463,7 +463,7 @@ def toggle_active(customer_id):
@bp.route('/import/template')
@login_required
@admin_required
@supervisor_required
def import_template():
"""Download a blank CSV template showing the expected import format."""
import csv, io
@@ -495,7 +495,7 @@ def import_template():
@bp.route('/import', methods=['GET', 'POST'])
@login_required
@admin_required
@supervisor_required
def bulk_import():
"""Two-phase CSV import for customer accounts.
@@ -744,7 +744,7 @@ def bulk_import():
@bp.route('/facilities-for-project/<int:project_id>')
@login_required
@admin_required
@supervisor_required
def facilities_for_project(project_id):
from flask import jsonify
project = Project.query.get_or_404(project_id)
+2 -2
View File
@@ -24,7 +24,7 @@ def index():
thirty_days_ago = now - timedelta(days=30)
is_inspector = current_user.role == 'inspector'
is_privileged = current_user.role in ['admin', 'supervisor']
is_privileged = current_user.role in ['admin', 'director']
is_customer = current_user.role == 'customer'
is_project_manager = current_user.role == 'project_manager'
@@ -107,7 +107,7 @@ def index():
followup_q = followup_q.filter(False)
pending_followups = followup_q.count()
# ── System stats (admin/supervisor) ───────────────────────────────────
# ── System stats (admin/director) ────────────────────────────────────────
total_facilities = Facility.query.filter_by(active=True).count() if is_privileged else 0
total_templates = InspectionTemplate.query.count() if is_privileged else 0
total_users = User.query.count() if current_user.role == 'admin' else 0
+1 -1
View File
@@ -673,7 +673,7 @@ def flag_issue(inspection_id):
form = IssueForm()
areas = Area.query.filter_by(facility_id=inspection.facility_id).order_by(Area.name).all()
staff = User.query.filter(User.role.in_(['supervisor', 'inspector'])).order_by(User.username).all()
staff = User.query.filter(User.role.in_(['director', 'inspector'])).order_by(User.username).all()
form.area_id.choices = [(a.id, a.name) for a in areas]
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
+7 -7
View File
@@ -114,7 +114,7 @@ def view(issue_id):
return redirect(url_for('issues.index'))
form = IssueUpdateForm(obj=issue)
staff = User.query.filter(User.role.in_(['supervisor','inspector'])).order_by(User.username).all()
staff = User.query.filter(User.role.in_(['director','inspector'])).order_by(User.username).all()
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
form.status.data = form.status.data or issue.status
@@ -124,7 +124,7 @@ def view(issue_id):
issue.status = form.status.data
if current_user.role in ['admin', 'supervisor']:
if current_user.role in ['admin', 'director']:
issue.assigned_to = form.assigned_to.data or None
if form.status.data == 'resolved' and not issue.resolved_at:
@@ -353,7 +353,7 @@ def unfollow(issue_id):
def create():
form = IssueForm()
areas = Area.query.join(Facility).filter(Facility.active == True).order_by(Facility.name, Area.name).all()
staff = User.query.filter(User.role.in_(['supervisor','inspector'])).order_by(User.username).all()
staff = User.query.filter(User.role.in_(['director','inspector'])).order_by(User.username).all()
form.area_id.choices = [(a.id, f"{a.facility.name}{a.name}") for a in areas]
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
@@ -460,16 +460,16 @@ def verify(issue_id):
@bp.route('/<int:issue_id>/request-verification', methods=['POST'])
@login_required
def request_verification(issue_id):
"""Inspector/assignee marks the issue as pending supervisor verification."""
"""Inspector/assignee marks the issue as pending director verification."""
issue = Issue.query.get_or_404(issue_id)
if current_user.role == 'customer':
flash('Access denied.', 'danger')
return redirect(url_for('issues.index'))
# Only the assignee, supervisor, or admin can request verification
# Only the assignee, director, or admin can request verification
can_act = (
current_user.role in ['admin', 'supervisor']
current_user.role in ['admin', 'director']
or issue.assigned_to == current_user.id
)
if not can_act:
@@ -556,7 +556,7 @@ def verification_queue():
def delete(issue_id):
"""Permanently delete an issue and its associated photos.
Restricted to admin and supervisor roles. The deletion is recorded in
Restricted to admin and director roles. The deletion is recorded in
the audit log before the record is removed so there is always a trace.
"""
issue = Issue.query.get_or_404(issue_id)
+4 -4
View File
@@ -38,8 +38,8 @@ def _date_range():
@bp.route('/')
@login_required
def index():
# Customers get a scoped view; internal staff need supervisor+ access
if current_user.role not in ['admin', 'supervisor', 'project_manager', 'customer']:
# Customers get a scoped view; internal staff need director+ access
if current_user.role not in ['admin', 'director', 'project_manager', 'customer']:
from flask import flash, redirect, url_for
flash('Access denied.', 'danger')
return redirect(url_for('dashboard.index'))
@@ -179,7 +179,7 @@ def index():
@bp.route('/facility/<int:facility_id>')
@login_required
def facility_report(facility_id):
if current_user.role not in ['admin', 'supervisor', 'project_manager', 'customer']:
if current_user.role not in ['admin', 'director', 'project_manager', 'customer']:
from flask import flash, redirect, url_for
flash('Access denied.', 'danger')
return redirect(url_for('dashboard.index'))
@@ -228,7 +228,7 @@ def facility_report(facility_id):
def facility_scorecard(facility_id):
"""Comprehensive per-facility scorecard: score trend, SLA compliance,
issue breakdown by severity, inspection frequency."""
if current_user.role not in ['admin', 'supervisor', 'project_manager', 'customer']:
if current_user.role not in ['admin', 'director', 'project_manager', 'customer']:
from flask import flash, redirect, url_for
flash('Access denied.', 'danger')
return redirect(url_for('dashboard.index'))