04/09 update: redesign user's roles
This commit is contained in:
+2
-2
@@ -99,9 +99,9 @@ def create_app(config_name='default'):
|
||||
unread = Notification.query.filter_by(
|
||||
user_id=current_user.id, is_read=False
|
||||
).count()
|
||||
# Pending verification count — only computed for supervisor+ roles
|
||||
# Pending verification count — only computed for director+ roles
|
||||
pv_count = 0
|
||||
if current_user.role in ('admin', 'supervisor'):
|
||||
if current_user.role in ('admin', 'director'):
|
||||
pv_count = Issue.query.filter_by(
|
||||
status='pending_verification'
|
||||
).count()
|
||||
|
||||
@@ -8,7 +8,7 @@ One row per (event_type, role_key) pair.
|
||||
role_key values
|
||||
---------------
|
||||
admin — all users with role='admin'
|
||||
supervisor — all users with role='supervisor'
|
||||
director — all users with role='director'
|
||||
inspector — all users with role='inspector'
|
||||
project_manager — all users with role='project_manager'
|
||||
customer — all customer-portal users assigned to the relevant facility
|
||||
@@ -18,16 +18,16 @@ custom — free-form extra email addresses stored in custom_emails JSO
|
||||
|
||||
Default matrix (mirrors current hardcoded behaviour)
|
||||
-----------------------------------------------------
|
||||
inspection_completed : admin ✓ supervisor ✓ inspector ✗ pm ✗ customer ✓
|
||||
issue_assigned : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||
issue_status : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||
issue_comment : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||
issue_follow_update : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (followers implicit)
|
||||
issue_flagged : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
||||
issue_created : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
||||
issue_updated_customer : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✓
|
||||
verification_requested : admin ✓ supervisor ✓ inspector ✗ pm ✗ customer ✗
|
||||
sla_alert : admin ✓ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee + followers implicit)
|
||||
inspection_completed : admin ✓ director ✓ inspector ✗ pm ✗ customer ✓
|
||||
issue_assigned : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||
issue_status : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||
issue_comment : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||
issue_follow_update : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (followers implicit)
|
||||
issue_flagged : admin ✗ director ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
||||
issue_created : admin ✗ director ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
||||
issue_updated_customer : admin ✗ director ✗ inspector ✗ pm ✗ customer ✓
|
||||
verification_requested : admin ✓ director ✓ inspector ✗ pm ✗ customer ✗
|
||||
sla_alert : admin ✓ director ✗ inspector ✗ pm ✗ customer ✗ (assignee + followers implicit)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -36,7 +36,7 @@ from app import db
|
||||
# Role keys available in the matrix UI
|
||||
MATRIX_ROLES = [
|
||||
('admin', 'Admin'),
|
||||
('supervisor', 'Supervisor'),
|
||||
('director', 'Director'),
|
||||
('inspector', 'Inspector'),
|
||||
('project_manager', 'Project Manager'),
|
||||
('customer', 'Customer'),
|
||||
@@ -65,84 +65,84 @@ MATRIX_EVENTS = {
|
||||
MATRIX_DEFAULTS = {
|
||||
# inspection_completed
|
||||
('inspection_completed', 'admin'): True,
|
||||
('inspection_completed', 'supervisor'): True,
|
||||
('inspection_completed', 'director'): True,
|
||||
('inspection_completed', 'inspector'): False,
|
||||
('inspection_completed', 'project_manager'): False,
|
||||
('inspection_completed', 'customer'): True,
|
||||
('inspection_completed', 'custom'): False,
|
||||
# issue_assigned (assignee is always notified implicitly)
|
||||
('issue_assigned', 'admin'): False,
|
||||
('issue_assigned', 'supervisor'): False,
|
||||
('issue_assigned', 'director'): False,
|
||||
('issue_assigned', 'inspector'): False,
|
||||
('issue_assigned', 'project_manager'): False,
|
||||
('issue_assigned', 'customer'): False,
|
||||
('issue_assigned', 'custom'): False,
|
||||
# issue_reassigned
|
||||
('issue_reassigned', 'admin'): False,
|
||||
('issue_reassigned', 'supervisor'): False,
|
||||
('issue_reassigned', 'director'): False,
|
||||
('issue_reassigned', 'inspector'): False,
|
||||
('issue_reassigned', 'project_manager'): False,
|
||||
('issue_reassigned', 'customer'): False,
|
||||
('issue_reassigned', 'custom'): False,
|
||||
# issue_unassigned
|
||||
('issue_unassigned', 'admin'): False,
|
||||
('issue_unassigned', 'supervisor'): False,
|
||||
('issue_unassigned', 'director'): False,
|
||||
('issue_unassigned', 'inspector'): False,
|
||||
('issue_unassigned', 'project_manager'): False,
|
||||
('issue_unassigned', 'customer'): False,
|
||||
('issue_unassigned', 'custom'): False,
|
||||
# issue_status
|
||||
('issue_status', 'admin'): False,
|
||||
('issue_status', 'supervisor'): False,
|
||||
('issue_status', 'director'): False,
|
||||
('issue_status', 'inspector'): False,
|
||||
('issue_status', 'project_manager'): False,
|
||||
('issue_status', 'customer'): False,
|
||||
('issue_status', 'custom'): False,
|
||||
# issue_comment
|
||||
('issue_comment', 'admin'): False,
|
||||
('issue_comment', 'supervisor'): False,
|
||||
('issue_comment', 'director'): False,
|
||||
('issue_comment', 'inspector'): False,
|
||||
('issue_comment', 'project_manager'): False,
|
||||
('issue_comment', 'customer'): False,
|
||||
('issue_comment', 'custom'): False,
|
||||
# issue_follow_update (followers always notified implicitly)
|
||||
('issue_follow_update', 'admin'): False,
|
||||
('issue_follow_update', 'supervisor'): False,
|
||||
('issue_follow_update', 'director'): False,
|
||||
('issue_follow_update', 'inspector'): False,
|
||||
('issue_follow_update', 'project_manager'): False,
|
||||
('issue_follow_update', 'customer'): False,
|
||||
('issue_follow_update', 'custom'): False,
|
||||
# issue_flagged (from inspection)
|
||||
('issue_flagged', 'admin'): False,
|
||||
('issue_flagged', 'supervisor'): False,
|
||||
('issue_flagged', 'director'): False,
|
||||
('issue_flagged', 'inspector'): False,
|
||||
('issue_flagged', 'project_manager'): False,
|
||||
('issue_flagged', 'customer'): True,
|
||||
('issue_flagged', 'custom'): False,
|
||||
# issue_created (standalone)
|
||||
('issue_created', 'admin'): False,
|
||||
('issue_created', 'supervisor'): False,
|
||||
('issue_created', 'director'): False,
|
||||
('issue_created', 'inspector'): False,
|
||||
('issue_created', 'project_manager'): False,
|
||||
('issue_created', 'customer'): True,
|
||||
('issue_created', 'custom'): False,
|
||||
# issue_updated_customer
|
||||
('issue_updated_customer', 'admin'): False,
|
||||
('issue_updated_customer', 'supervisor'): False,
|
||||
('issue_updated_customer', 'director'): False,
|
||||
('issue_updated_customer', 'inspector'): False,
|
||||
('issue_updated_customer', 'project_manager'): False,
|
||||
('issue_updated_customer', 'customer'): True,
|
||||
('issue_updated_customer', 'custom'): False,
|
||||
# verification_requested
|
||||
('verification_requested', 'admin'): True,
|
||||
('verification_requested', 'supervisor'): True,
|
||||
('verification_requested', 'director'): True,
|
||||
('verification_requested', 'inspector'): False,
|
||||
('verification_requested', 'project_manager'): False,
|
||||
('verification_requested', 'customer'): False,
|
||||
('verification_requested', 'custom'): False,
|
||||
# sla_alert (assignee + followers always notified implicitly)
|
||||
('sla_alert', 'admin'): True,
|
||||
('sla_alert', 'supervisor'): False,
|
||||
('sla_alert', 'director'): False,
|
||||
('sla_alert', 'inspector'): False,
|
||||
('sla_alert', 'project_manager'): False,
|
||||
('sla_alert', 'customer'): False,
|
||||
|
||||
+4
-1
@@ -16,7 +16,10 @@ class User(UserMixin, db.Model):
|
||||
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
role = db.Column(
|
||||
db.Enum('admin', 'supervisor', 'inspector', 'project_manager', 'customer'),
|
||||
# 'supervisor' retained temporarily so the Enum is valid before the
|
||||
# migration UPDATE runs. The migration removes it after all rows are
|
||||
# updated to 'director'.
|
||||
db.Enum('admin', 'supervisor', 'director', 'inspector', 'project_manager', 'customer'),
|
||||
nullable=False
|
||||
)
|
||||
created_at = db.Column(db.DateTime, default=now_eastern)
|
||||
|
||||
+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'))
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<strong>{{ entry.username }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'supervisor' %}warning{% else %}info{% endif %} bg-opacity-75">
|
||||
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'director' %}warning{% else %}info{% endif %} bg-opacity-75">
|
||||
{{ entry.user_role | title }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
<dt class="col-sm-4 text-muted">Role at Time</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'supervisor' %}warning{% else %}info{% endif %}">
|
||||
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'director' %}warning{% else %}info{% endif %}">
|
||||
{{ entry.user_role | title }}
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
<p class="text-muted mb-1" style="font-size:.85rem;">@{{ current_user.username }}</p>
|
||||
{% endif %}
|
||||
<p class="text-muted mb-2">{{ current_user.email }}</p>
|
||||
<span class="badge fs-6 bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %}">
|
||||
<i class="bi bi-{% if current_user.role == 'admin' %}shield-fill{% elif current_user.role == 'supervisor' %}star-fill{% else %}person-badge-fill{% endif %} me-1"></i>
|
||||
<span class="badge fs-6 bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'director' %}warning{% else %}info{% endif %}">
|
||||
<i class="bi bi-{% if current_user.role == 'admin' %}shield-fill{% elif current_user.role == 'director' %}star-fill{% else %}person-badge-fill{% endif %} me-1"></i>
|
||||
{{ current_user.role | title }}
|
||||
</span>
|
||||
<hr>
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
{% block title %}User Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h2><i class="bi bi-people-fill"></i> Internal User Management</h2>
|
||||
<p class="text-muted mb-0">Manage staff accounts (Admin, Supervisor, Inspector, Project Manager). For customer accounts, visit <a href="{{ url_for('customers.index') }}">Customer Management</a>.</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="col-md-6 text-end">
|
||||
<a href="{{ url_for('auth.create_user') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add User
|
||||
</a>
|
||||
@@ -37,7 +36,7 @@
|
||||
<td>{{ user.full_name or '—' }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'supervisor' %}warning{% elif user.role == 'project_manager' %}primary{% elif user.role == 'customer' %}success{% else %}info{% endif %}">
|
||||
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'director' %}warning{% elif user.role == 'project_manager' %}primary{% elif user.role == 'customer' %}success{% else %}info{% endif %}">
|
||||
{{ user.role.replace('_',' ')|title }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
||||
</li>
|
||||
{% if current_user.role in ['admin', 'supervisor', 'project_manager'] %}
|
||||
{% if current_user.role in ['admin', 'director', 'project_manager'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('projects.index') }}">Projects</a>
|
||||
</li>
|
||||
@@ -109,7 +109,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('facilities.list_facilities') }}">Facilities</a>
|
||||
</li>
|
||||
{% if current_user.role != 'customer' %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('templates.index') }}">Templates</a>
|
||||
</li>
|
||||
@@ -120,7 +120,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('reports.index') }}">Reports</a>
|
||||
</li>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('scheduled_reports.index') }}">Scheduled Reports</a>
|
||||
</li>
|
||||
@@ -128,7 +128,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('issues.index') }}">Issues</a>
|
||||
</li>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center gap-1"
|
||||
href="{{ url_for('issues.verification_queue') }}">
|
||||
@@ -142,13 +142,15 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.role == 'admin' %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.list_users') }}">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('customers.index') }}">Customers</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.role == 'admin' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('audit.index') }}">Audit Trail</a>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="mb-0">Welcome, {{ current_user.username }}!</h2>
|
||||
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% elif current_user.role == 'project_manager' %}primary{% elif current_user.role == 'customer' %}success{% else %}info{% endif %} mt-1">
|
||||
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'director' %}warning{% elif current_user.role == 'project_manager' %}primary{% elif current_user.role == 'customer' %}success{% else %}info{% endif %} mt-1">
|
||||
{{ current_user.role.replace('_',' ')|title }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -171,8 +171,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── System quick stats (admin/supervisor) ──────────────────────────────── #}
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{# ── System quick stats (admin/director) ─────────────────────────────────── #}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-{% if current_user.role == 'admin' %}4{% else %}6{% endif %}">
|
||||
<div class="card shadow-sm text-center">
|
||||
@@ -208,7 +208,7 @@
|
||||
|
||||
{# ── Score trend chart + Facility performance ───────────────────────────── #}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-{% if current_user.role in ['admin','supervisor'] and facility_perf %}7{% else %}12{% endif %}">
|
||||
<div class="col-lg-{% if current_user.role in ['admin','director'] and facility_perf %}7{% else %}12{% endif %}">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-semibold">
|
||||
<i class="bi bi-graph-up me-1"></i>Inspection Score Trend (Last 30 Days)
|
||||
@@ -226,7 +226,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.role in ['admin', 'supervisor'] and facility_perf %}
|
||||
{% if current_user.role in ['admin', 'director'] and facility_perf %}
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-semibold">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h2><i class="bi bi-building"></i> Facilities</h2>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('facilities.create_facility') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add Facility
|
||||
</a>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<h2><i class="bi bi-building"></i> {{ facility.name }}</h2>
|
||||
</div>
|
||||
<div class="col-md-4 text-end d-flex gap-2 justify-content-end align-items-start">
|
||||
{% if current_user.role in ['admin', 'supervisor', 'project_manager', 'customer'] %}
|
||||
{% if current_user.role in ['admin', 'director', 'project_manager', 'customer'] %}
|
||||
<a href="{{ url_for('reports.facility_report', facility_id=facility.id) }}"
|
||||
class="btn btn-outline-info">
|
||||
<i class="bi bi-graph-up-arrow"></i> Scorecard
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
@@ -126,7 +126,7 @@
|
||||
</td>
|
||||
<td>{{ area.inspections.count() }}</td>
|
||||
<td>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
{% else %}
|
||||
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary">View</a>
|
||||
{% endif %}
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-danger ms-1"
|
||||
data-bs-toggle="modal"
|
||||
@@ -121,7 +121,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<!-- Delete Inspection Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteInspectionModal" tabindex="-1" aria-labelledby="deleteInspectionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
@@ -155,7 +155,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const modal = document.getElementById('deleteInspectionModal');
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
<i class="bi bi-arrow-repeat"></i> Re-inspect
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.role in ['admin','supervisor'] %}
|
||||
{% if current_user.role in ['admin','director'] %}
|
||||
{% if not inspection.follow_up_required %}
|
||||
<button type="button" class="btn btn-sm btn-outline-warning"
|
||||
data-bs-toggle="modal" data-bs-target="#followupModal"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-exclamation-triangle"></i> Issues</h2>
|
||||
{% if current_user.role in ['admin','supervisor'] %}
|
||||
{% if current_user.role in ['admin','director'] %}
|
||||
<a href="{{ url_for('issues.create') }}" class="btn btn-danger">
|
||||
<i class="bi bi-plus-circle"></i> Log Issue
|
||||
</a>
|
||||
@@ -100,13 +100,13 @@
|
||||
|
||||
<a href="{{ url_for('issues.view', issue_id=issue.id) }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
{% if current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %}
|
||||
{% if current_user.role in ['admin','director'] or issue.assigned_to == current_user.id %}
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
{% else %}
|
||||
<i class="bi bi-eye"></i> View
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<form method="POST" action="{{ url_for('issues.delete', issue_id=issue.id) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Permanently delete Issue #{{ issue.id }}? This cannot be undone.');">
|
||||
|
||||
@@ -83,8 +83,8 @@
|
||||
<hr>
|
||||
<div class="alert alert-info py-2 mb-0">
|
||||
<i class="bi bi-hourglass-split me-1"></i>
|
||||
<strong>Awaiting supervisor verification.</strong>
|
||||
{% if current_user.role in ['admin','supervisor'] %}
|
||||
<strong>Awaiting director verification.</strong>
|
||||
{% if current_user.role in ['admin','director'] %}
|
||||
<form method="POST" action="{{ url_for('issues.verify', issue_id=issue.id) }}" class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-2">
|
||||
@@ -163,7 +163,7 @@
|
||||
</div>
|
||||
|
||||
{# ── Update Form ────────────────────────────────────────────────────── #}
|
||||
{% set can_edit = current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %}
|
||||
{% set can_edit = current_user.role in ['admin','director'] or issue.assigned_to == current_user.id %}
|
||||
{% if can_edit %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light"><h6 class="mb-0">Update Issue</h6></div>
|
||||
@@ -174,7 +174,7 @@
|
||||
{{ form.status.label(class="form-label fw-semibold") }}
|
||||
{{ form.status(class="form-select") }}
|
||||
</div>
|
||||
{% if current_user.role in ['admin','supervisor'] %}
|
||||
{% if current_user.role in ['admin','director'] %}
|
||||
<div class="mb-3">
|
||||
{{ form.assigned_to.label(class="form-label fw-semibold") }}
|
||||
{{ form.assigned_to(class="form-select") }}
|
||||
@@ -209,7 +209,7 @@
|
||||
class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-info w-100"
|
||||
onclick="return confirm('Mark this issue as pending supervisor verification?')">
|
||||
onclick="return confirm('Mark this issue as pending director verification?')">
|
||||
<i class="bi bi-hourglass-split me-1"></i> Request Verification
|
||||
</button>
|
||||
</form>
|
||||
@@ -224,7 +224,7 @@
|
||||
<a href="{{ url_for('issues.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to Issues
|
||||
</a>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteIssueModal">
|
||||
<i class="bi bi-trash"></i> Delete Issue
|
||||
@@ -232,7 +232,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteIssueModal" tabindex="-1" aria-labelledby="deleteIssueModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-folder2-open"></i> Projects</h2>
|
||||
</div>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<div class="col-auto">
|
||||
<a href="{{ url_for('projects.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> New Project
|
||||
@@ -52,7 +52,7 @@
|
||||
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-auto d-flex gap-2">
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h2><i class="bi bi-file-earmark-text"></i> Inspection Templates</h2>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('templates.create_template') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create Template
|
||||
</a>
|
||||
@@ -41,7 +41,7 @@
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<!-- Rename -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
@@ -90,7 +90,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<!-- Edit Template Modal -->
|
||||
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
@@ -190,7 +190,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Edit template modal
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-shrink-0 mt-1">
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-pencil-square"></i> Edit Form
|
||||
</a>
|
||||
@@ -167,7 +167,7 @@
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-layout-text-sidebar-reverse"></i>
|
||||
<p>No fields defined yet.</p>
|
||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
|
||||
class="btn btn-primary btn-sm mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Open Form Editor
|
||||
|
||||
+12
-5
@@ -12,20 +12,27 @@ def admin_required(f):
|
||||
return decorated_function
|
||||
|
||||
def supervisor_required(f):
|
||||
"""Grants access to admin and director roles.
|
||||
|
||||
The decorator is intentionally kept as 'supervisor_required' so that all
|
||||
existing route decorators (@supervisor_required) continue to work without
|
||||
any changes to the route files. The access list now reflects the renamed
|
||||
Director role instead of the retired Supervisor role.
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated or current_user.role not in ['admin', 'supervisor']:
|
||||
flash('Supervisor access required.', 'danger')
|
||||
if not current_user.is_authenticated or current_user.role not in ['admin', 'director']:
|
||||
flash('Director access required.', 'danger')
|
||||
return redirect(url_for('dashboard.index'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def project_manager_required(f):
|
||||
"""Grants access to admin, supervisor, and project_manager roles."""
|
||||
"""Grants access to admin, director, and project_manager roles."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated or current_user.role not in [
|
||||
'admin', 'supervisor', 'project_manager'
|
||||
'admin', 'director', 'project_manager'
|
||||
]:
|
||||
flash('Project Manager access required.', 'danger')
|
||||
return redirect(url_for('dashboard.index'))
|
||||
@@ -35,7 +42,7 @@ def project_manager_required(f):
|
||||
def customer_required(f):
|
||||
"""Restricts access to customer-role users only.
|
||||
|
||||
Internal staff (admin, supervisor, inspector, project_manager) should
|
||||
Internal staff (admin, director, inspector, project_manager) should
|
||||
never be routed through customer-scoped views — use their own routes.
|
||||
"""
|
||||
@wraps(f)
|
||||
|
||||
+4
-3
@@ -51,10 +51,11 @@ class UserForm(FlaskForm):
|
||||
password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)])
|
||||
confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')])
|
||||
role = SelectField('Role', choices=[
|
||||
('admin', 'Administrator'),
|
||||
('supervisor', 'Supervisor'),
|
||||
('inspector', 'Inspector'),
|
||||
('admin', 'Administrator'),
|
||||
('director', 'Director'),
|
||||
('inspector', 'Inspector'),
|
||||
('project_manager', 'Project Manager'),
|
||||
# 'customer' is intentionally excluded — customer accounts are managed via /customers
|
||||
], validators=[DataRequired()])
|
||||
|
||||
def __init__(self, user=None, *args, **kwargs):
|
||||
|
||||
@@ -511,7 +511,7 @@ def notify_by_matrix(
|
||||
|
||||
role_to_db = {
|
||||
'admin': 'admin',
|
||||
'supervisor': 'supervisor',
|
||||
'director': 'director',
|
||||
'inspector': 'inspector',
|
||||
'project_manager': 'project_manager',
|
||||
'customer': 'customer',
|
||||
|
||||
Reference in New Issue
Block a user