Aug 19 - Update code to catch up with ST

This commit is contained in:
2026-08-19 14:05:18 -04:00
parent c9984e7ae6
commit 12141c2f75
52 changed files with 3321 additions and 342 deletions
+88 -9
View File
@@ -16,11 +16,13 @@ GET /api/v1/templates/<template_id>
import logging
from flask import Blueprint, g
from flask import Blueprint, g, request
from app import db
from app.models.inspection import InspectionTemplate
from app.models.facility import Facility
from app.api.errors import api_ok, api_error
from app.api.decorators import jwt_required
from app.utils.scope import get_inspector_scope
logger = logging.getLogger(__name__)
@@ -31,6 +33,71 @@ _ALLOWED_ROLES = {'admin', 'director', 'inspector', 'external_inspector',
'project_manager', 'auditor'}
def _visible_project_ids(user):
"""Contract ids whose forms this user may see, or None for "no limit".
An inspector (ours or a customer's) is limited to the contracts they are
assigned; every other allowed role sees all. Derived from the facilities
get_inspector_scope() returns, so the API can never disagree with the web.
"""
if not user.is_inspector:
return None
fids = get_inspector_scope(user) or []
if not fids:
return []
return sorted({
f.project_id
for f in Facility.query.filter(Facility.id.in_(fids)).all()
if f.project_id
})
def _visible_templates(user, project_id=None):
"""Forms this user may use, optionally narrowed to one contract.
phase52 — a form attached to specific contracts must not reach an
inspector working for a different customer. Shared forms (no contract
links) stay visible to everyone, which is what keeps existing installs
behaving exactly as before.
With `project_id`: exactly the web picker's list for that contract.
Without: the union across every contract the user can reach — the iPad
caches templates up-front and picks the facility later, so it needs the
whole set it might legitimately use.
"""
pids = _visible_project_ids(user)
if project_id is not None:
# The caller names a contract. Their OWN scope still applies — without
# this, passing another customer's facility_id would list that
# customer's form names back to an inspector who has no business
# seeing them. Empty list, not an error: the endpoint must not confirm
# whether that contract exists either.
if pids is not None and project_id not in pids:
logger.warning('API TEMPLATES | out-of-scope project filter | '
'user=%s | project_id=%s', user.username, project_id)
return []
return InspectionTemplate.available_query(project_id).all()
if pids is None:
return (InspectionTemplate.query
.filter_by(active=True)
.order_by(InspectionTemplate.name)
.all())
seen, out = set(), []
# Always include the shared forms, even when the user has no contracts —
# otherwise an unassigned inspector would see nothing at all rather than
# the standard forms.
for pid in list(pids) + [None]:
for t in InspectionTemplate.available_query(pid).all():
if t.id not in seen:
seen.add(t.id)
out.append(t)
out.sort(key=lambda t: (t.name or '').lower())
return out
def _template_summary_payload(template: InspectionTemplate) -> dict:
"""Serialize a template to the lightweight summary dict (no form_schema)."""
return {
@@ -88,17 +155,21 @@ def list_templates():
user.username, user.role)
return api_error('Access denied', 403)
templates = (
InspectionTemplate.query
.filter_by(active=True)
.order_by(InspectionTemplate.name)
.all()
)
# Optional ?project_id= narrows to one contract (matches the web picker);
# ?facility_id= is accepted as a convenience and resolved to its contract.
project_id = request.args.get('project_id', type=int)
if project_id is None:
facility_id = request.args.get('facility_id', type=int)
if facility_id is not None:
facility = db.session.get(Facility, facility_id)
project_id = facility.project_id if facility else None
templates = _visible_templates(user, project_id)
payload = [_template_summary_payload(t) for t in templates]
logger.info('API TEMPLATES | list | user=%s | count=%d',
user.username, len(payload))
logger.info('API TEMPLATES | list | user=%s | project_id=%s | count=%d',
user.username, project_id, len(payload))
return api_ok({'templates': payload, 'count': len(payload)})
@@ -144,6 +215,14 @@ def get_template(template_id):
if template is None:
return api_error('Template not found', 404)
# phase52 — a restricted form must not be fetchable by an inspector on a
# different customer's contracts. 404 rather than 403: whether another
# customer's form exists is itself not this user's business.
if template.id not in {t.id for t in _visible_templates(user)}:
logger.warning('API TEMPLATES | out-of-contract fetch blocked | '
'user=%s | template_id=%s', user.username, template_id)
return api_error('Template not found', 404)
logger.info('API TEMPLATES | detail | user=%s | template_id=%d | name=%s',
user.username, template_id, template.name)