Aug 17 - Update forms will be assigned per contract

This commit is contained in:
2026-08-17 15:45:48 -04:00
parent fb85f7dc28
commit 9d7721213b
13 changed files with 471 additions and 25 deletions
+39 -2
View File
@@ -348,7 +348,6 @@ def index():
def start():
form = StartInspectionForm()
templates = InspectionTemplate.query.filter_by(active=True).order_by(InspectionTemplate.name).all()
projects = Project.query.filter_by(active=True).order_by(Project.name).all()
# Scope projects to inspector's assigned contracts
@@ -360,7 +359,6 @@ def start():
}
projects = [p for p in projects if p.id in assigned_pids]
form.template_id.choices = [(t.id, t.name) for t in templates]
form.project_id.choices = [(p.id, p.name) for p in projects]
# Seed facility choices: use submitted project_id, session value, or first project
@@ -374,6 +372,13 @@ def start():
else:
selected_project_id = projects[0].id if projects else None
# phase52 — forms are offered per CONTRACT: shared forms plus any attached
# to the selected contract. This is also the POST validation (SelectField
# validates against its choices), so a crafted template_id for another
# customer's form is rejected here, not merely hidden in the UI.
templates = InspectionTemplate.available_query(selected_project_id).all()
form.template_id.choices = [(t.id, t.name) for t in templates]
if selected_project_id:
facilities = Facility.query.filter_by(active=True, project_id=selected_project_id).order_by(Facility.name).all()
else:
@@ -406,6 +411,19 @@ def start():
if template is None:
abort(404)
# Belt-and-braces: the choices above already reject a form that is not
# available on this contract, but that guard lives in how the list was
# built. Re-assert it against the FACILITY actually chosen, so a future
# change to the choice-building cannot quietly open a cross-customer
# hole here.
_fac = db.session.get(Facility, form.facility_id.data)
if not template.available_for_project(_fac.project_id if _fac else None):
logger_msg = ('INSPECTION START BLOCKED | template=%s not available for '
'facility=%s | user=%s')
current_app.logger.warning(logger_msg, template.id,
form.facility_id.data, current_user.username)
abort(403)
# Inspector facility scope check — prevent crafted POST from selecting
# a facility outside their assigned contracts.
if current_user.is_inspector:
@@ -463,6 +481,25 @@ def facilities_for_project(project_id):
return jsonify([{'id': f.id, 'name': f.name} for f in facilities])
# ── AJAX: forms available on a given contract (phase52) ──────────────────────
@bp.route('/templates_for_project/<int:project_id>')
@login_required
def templates_for_project(project_id):
"""Forms usable on this contract — shared ones plus any attached to it.
Powers the Contract -> Form cascade on the start-inspection page, the same
way facilities_for_project powers Contract -> Facility. Read-only, and the
real gate is still the POST validation in start(); this only keeps the
picker honest as the contract changes.
"""
templates = InspectionTemplate.available_query(project_id).all()
return jsonify([
{'id': t.id, 'name': t.name, 'shared': t.is_shared}
for t in templates
])
# ── Execute ───────────────────────────────────────────────────────────────────
@bp.route('/<int:inspection_id>/execute', methods=['GET', 'POST'])