Aug 7 - Update: add external inspector
This commit is contained in:
@@ -10,6 +10,14 @@ role_key values
|
||||
admin — all users with role='admin'
|
||||
director — all users with role='director'
|
||||
inspector — all users with role='inspector'
|
||||
EXCEPTION: for event 'inspection_completed', the inspector
|
||||
column notifies ONLY the inspection's own inspector
|
||||
(the submitter), not the whole inspector pool. Scoping is
|
||||
applied in notify_by_matrix() via the inspection_id.
|
||||
external_inspector — all users with role='external_inspector' (customer /
|
||||
third-party inspectors). Separate column so third parties
|
||||
can be routed differently from the tenant's own crew; the
|
||||
'inspection_completed' scoping above applies here too.
|
||||
project_manager — all users with role='project_manager'
|
||||
customer — all customer-portal users assigned to the relevant facility
|
||||
assignee — the specific user the issue/inspection is assigned to
|
||||
@@ -40,6 +48,7 @@ MATRIX_ROLES = [
|
||||
('admin', 'Admin'),
|
||||
('director', 'Director'),
|
||||
('inspector', 'Inspector'),
|
||||
('external_inspector', 'External Inspector'),
|
||||
('project_manager', 'Project Manager'),
|
||||
('auditor', 'Auditor'),
|
||||
('customer', 'Customer'),
|
||||
@@ -171,6 +180,18 @@ MATRIX_DEFAULTS = {
|
||||
('score_alert', 'custom'): False,
|
||||
}
|
||||
|
||||
# MT-15 — the External Inspector column defaults to whatever the internal
|
||||
# Inspector column defaults to, for every event. Mirroring rather than listing
|
||||
# 14 more literals means a future event added for 'inspector' automatically
|
||||
# gets a matching external default instead of silently falling back to the
|
||||
# is_enabled() fallback. Admins can diverge the two columns in the UI at any
|
||||
# time; this only seeds rows that do not exist yet.
|
||||
MATRIX_DEFAULTS.update({
|
||||
(_event, 'external_inspector'): _enabled
|
||||
for (_event, _role), _enabled in list(MATRIX_DEFAULTS.items())
|
||||
if _role == 'inspector'
|
||||
})
|
||||
|
||||
|
||||
class NotificationMatrix(db.Model):
|
||||
"""Admin-controlled per-event notification routing."""
|
||||
|
||||
+51
-1
@@ -3,6 +3,20 @@ from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app.utils.time_utils import now_eastern
|
||||
|
||||
# Display labels for the role ENUM. 'external_inspector' would otherwise title
|
||||
# case to "External Inspector" anyway, but the map keeps every label in one
|
||||
# place for templates that show a role name.
|
||||
ROLE_LABELS = {
|
||||
'admin': 'Admin',
|
||||
'director': 'Director',
|
||||
'project_manager': 'Project Manager',
|
||||
'auditor': 'Auditor',
|
||||
'inspector': 'Inspector',
|
||||
'external_inspector': 'External Inspector',
|
||||
'customer': 'Customer',
|
||||
}
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from app import db
|
||||
@@ -11,6 +25,19 @@ def load_user(user_id):
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
# ── Inspector roles (MT-15) ───────────────────────────────────────────────
|
||||
# 'external_inspector' is an inspector employed by the customer or a third
|
||||
# party rather than by the tenant. It has exactly the same capabilities as
|
||||
# the internal 'inspector' role and is scoped the same way — through
|
||||
# InspectorAssignment rows, via get_inspector_scope().
|
||||
#
|
||||
# Every place that used to test `role == 'inspector'` must test membership
|
||||
# of this tuple instead, or external inspectors silently fall into the
|
||||
# privileged (org-wide) branch and see every contract. Use the
|
||||
# `is_inspector` property below — it is an ordinary attribute, so it reads
|
||||
# the same way in Python and in Jinja (`current_user.is_inspector`).
|
||||
INSPECTOR_ROLES = ('inspector', 'external_inspector')
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
||||
full_name = db.Column(db.String(150), nullable=True)
|
||||
@@ -19,7 +46,10 @@ class User(UserMixin, db.Model):
|
||||
role = db.Column(
|
||||
# Phase 11 migration complete — 'supervisor' removed from both the DB
|
||||
# ENUM and this Python-side declaration. Director is the canonical role.
|
||||
db.Enum('admin', 'director', 'inspector', 'project_manager', 'customer', 'auditor'),
|
||||
# MT-15 — 'external_inspector' added: a customer / third-party
|
||||
# inspector with identical capabilities to 'inspector'.
|
||||
db.Enum('admin', 'director', 'inspector', 'project_manager', 'customer',
|
||||
'auditor', 'external_inspector'),
|
||||
nullable=False
|
||||
)
|
||||
created_at = db.Column(db.DateTime, default=now_eastern)
|
||||
@@ -64,6 +94,26 @@ class User(UserMixin, db.Model):
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
@property
|
||||
def is_inspector(self):
|
||||
"""True for both the internal and the external inspector role.
|
||||
|
||||
Prefer this over `role == 'inspector'` for capability and scoping
|
||||
checks. Use an explicit `role == 'external_inspector'` test only where
|
||||
the two genuinely differ (currently: display labelling only).
|
||||
"""
|
||||
return self.role in self.INSPECTOR_ROLES
|
||||
|
||||
@property
|
||||
def is_external_inspector(self):
|
||||
"""True only for third-party / customer-employed inspectors."""
|
||||
return self.role == 'external_inspector'
|
||||
|
||||
@property
|
||||
def role_label(self):
|
||||
"""Human-readable role name, used in staff-facing lists."""
|
||||
return ROLE_LABELS.get(self.role, (self.role or '').replace('_', ' ').title())
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Return full name if set, otherwise fall back to username."""
|
||||
|
||||
Reference in New Issue
Block a user