04/09 update: redesign user's roles
This commit is contained in:
+6
-6
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user