Aug 19 - Update code to catch up with ST
This commit is contained in:
@@ -3,6 +3,43 @@ from app.utils.time_utils import now_eastern
|
||||
import json
|
||||
|
||||
|
||||
class TemplateContract(db.Model):
|
||||
"""Restricts a form to specific contracts (phase52).
|
||||
|
||||
A customer's bespoke form must not be visible to — or startable against —
|
||||
another customer's facilities. One row = "this template is available on
|
||||
this contract".
|
||||
|
||||
**No rows means the template is SHARED** (available on every contract), not
|
||||
"available nowhere". That is what makes the feature additive: every
|
||||
template that existed before phase52 has no rows, so nothing changed on
|
||||
deploy, and a form becomes customer-specific only when an admin attaches it
|
||||
to at least one contract. The empty-set-means-all convention is the whole
|
||||
migration story — do not "fix" it to mean the opposite.
|
||||
"""
|
||||
|
||||
__tablename__ = 'template_contracts'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
template_id = db.Column(db.Integer,
|
||||
db.ForeignKey('inspection_templates.id', ondelete='CASCADE'),
|
||||
nullable=False, index=True)
|
||||
project_id = db.Column(db.Integer,
|
||||
db.ForeignKey('projects.id', ondelete='CASCADE'),
|
||||
nullable=False, index=True)
|
||||
created_at = db.Column(db.DateTime, default=now_eastern, nullable=False)
|
||||
|
||||
project = db.relationship('Project', backref='template_contracts')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('template_id', 'project_id',
|
||||
name='uq_template_contract'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<TemplateContract template={self.template_id} project={self.project_id}>'
|
||||
|
||||
|
||||
class InspectionTemplate(db.Model):
|
||||
__tablename__ = 'inspection_templates'
|
||||
|
||||
@@ -18,6 +55,82 @@ class InspectionTemplate(db.Model):
|
||||
checklist_items = db.relationship('ChecklistItem', backref='template', lazy='dynamic', cascade='all, delete-orphan')
|
||||
inspections = db.relationship('Inspection', backref='template', lazy='dynamic')
|
||||
|
||||
# phase52 — contract restrictions. Deleting a template removes its links.
|
||||
contract_links = db.relationship('TemplateContract', backref='template',
|
||||
lazy='dynamic',
|
||||
cascade='all, delete-orphan')
|
||||
|
||||
# ── Contract availability (phase52) ──────────────────────────────────
|
||||
|
||||
@property
|
||||
def contract_ids(self):
|
||||
"""Project ids this form is restricted to; empty = shared with all."""
|
||||
return sorted(l.project_id for l in self.contract_links.all())
|
||||
|
||||
@property
|
||||
def is_shared(self):
|
||||
"""True when the form carries no restriction and is available anywhere."""
|
||||
return self.contract_links.count() == 0
|
||||
|
||||
def available_for_project(self, project_id):
|
||||
"""Can this form be used on `project_id`?
|
||||
|
||||
Shared forms are usable anywhere, including on a facility that has no
|
||||
contract at all. A restricted form needs an explicit link, so a
|
||||
facility with no contract (project_id None) can only ever use shared
|
||||
forms — fail-closed, which is the right side to err on.
|
||||
"""
|
||||
if self.is_shared:
|
||||
return True
|
||||
if project_id is None:
|
||||
return False
|
||||
return project_id in set(self.contract_ids)
|
||||
|
||||
@staticmethod
|
||||
def available_query(project_id, active_only=True):
|
||||
"""Query of templates usable on `project_id` (shared + linked).
|
||||
|
||||
The single definition of "which forms may this contract use". Every
|
||||
picker, the POST validation behind it, and the mobile API all go
|
||||
through here so they cannot disagree — a picker that offers more than
|
||||
the validator accepts silently drops work (see rule 93 for the same
|
||||
failure in the assignee dropdown).
|
||||
"""
|
||||
q = InspectionTemplate.query
|
||||
if active_only:
|
||||
q = q.filter(InspectionTemplate.active == True)
|
||||
|
||||
shared = ~InspectionTemplate.contract_links.any()
|
||||
if project_id is None:
|
||||
# No contract to match against — only unrestricted forms apply.
|
||||
return q.filter(shared).order_by(InspectionTemplate.name)
|
||||
|
||||
linked = InspectionTemplate.contract_links.any(
|
||||
TemplateContract.project_id == project_id
|
||||
)
|
||||
return q.filter(db.or_(shared, linked)).order_by(InspectionTemplate.name)
|
||||
|
||||
def set_contracts(self, project_ids):
|
||||
"""Replace this form's contract restrictions.
|
||||
|
||||
Pass an empty list to make the form shared again. Does NOT commit —
|
||||
the caller owns the transaction. Returns True if anything changed.
|
||||
"""
|
||||
wanted = {int(p) for p in project_ids or []}
|
||||
existing = {l.project_id: l for l in self.contract_links.all()}
|
||||
|
||||
changed = False
|
||||
for pid, link in existing.items():
|
||||
if pid not in wanted:
|
||||
db.session.delete(link)
|
||||
changed = True
|
||||
for pid in wanted:
|
||||
if pid not in existing:
|
||||
db.session.add(TemplateContract(template_id=self.id, project_id=pid))
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def get_form_schema(self):
|
||||
if self.form_schema is None:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user