Phase 2: core domain, policy engine, packet API, Clerical screen
Migration 0003: projects, aliases, renumbers, units, packets and every child table, change orders, assets, documents, ITPs, station limits, department weld flag. Policy package: Principal, PacketState, Action rules, custody, preconditions, CO hold, order of checks, can/blocked/holder on reads. docs/POLICY.md with the tightenings table for Seth. Write pipeline: FOR UPDATE, CAS on status and dept, If-Match row versions, server-assigned numbers, field CAS, version bump, packet log, audit, NOTIFY, signatures, IT reasons, filed lock and HMAC seal. API: /api/packets (header, transitions, work orders, pages, docs, move, remove, BOM, shorts, welds, heat tags, heat table, stage, checklist, clock, warehouse stage, Q.C., change orders, log, seal), /api/projects, /api/units, /api/assets, /_blob. Frontend: Clerical home, projects and units, packet page with pdf.js and QR codes, hand draft, heat map, send to warehouse, department or Q.C. Tests: policy matrix, API, concurrency, schema, projects; Playwright Clerical draft scenario. Verified on docker-1 with the CI stack and the Samba DC. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""Custody: who holds a packet right now, and the Who predicates. A direct port of guard.js holds()."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from .actions import ACTIVE, SUPPORT_CODES, Who
|
||||
from .principal import PacketState, Principal
|
||||
|
||||
WORKER, LEAD, SUPERVISOR = 1, 2, 3
|
||||
|
||||
|
||||
def works_on(principal: Principal, state: PacketState) -> bool:
|
||||
"""A member of the packet's department whose station limits allow the packet's station.
|
||||
|
||||
Leads and supervisors always pass. A worker passes when they have no station limits or the limits hold
|
||||
the packet's station. This is the old worksOn(); START and SUBMIT add the assigned-welder check.
|
||||
"""
|
||||
level = principal.level(state.dept)
|
||||
if level == 0:
|
||||
return False
|
||||
if level >= LEAD:
|
||||
return True
|
||||
return not principal.station_limits or (
|
||||
state.station_id is not None and state.station_id in principal.station_limits
|
||||
)
|
||||
|
||||
|
||||
def holds(principal: Principal, state: PacketState) -> bool:
|
||||
"""True when the principal holds the packet in its current status. Admin is not a holder; see the bypass."""
|
||||
st = state.status
|
||||
if st == "draft":
|
||||
return "CL" in principal.support
|
||||
if st == "warehouse":
|
||||
return "WH" in principal.support
|
||||
if st in ("ready", "review"):
|
||||
return principal.level(state.dept) >= LEAD
|
||||
if st in ACTIVE:
|
||||
return principal.level(state.dept) >= LEAD or works_on(principal, state)
|
||||
if st == "qc":
|
||||
return "QC" in principal.support
|
||||
if st == "complete":
|
||||
return "CL" in principal.support
|
||||
return False
|
||||
|
||||
|
||||
def holder_name(state: PacketState, dept_name: str | None = None, station_name: str | None = None) -> str:
|
||||
"""Who has the packet, for messages. The department name falls back to its code."""
|
||||
st = state.status
|
||||
if st == "draft":
|
||||
return "Clerical"
|
||||
if st == "warehouse":
|
||||
return "Warehouse"
|
||||
if st == "qc":
|
||||
return "Q.C."
|
||||
if st == "complete":
|
||||
return "Clerical (data book)"
|
||||
if st == "filed":
|
||||
return "the data book (filed)"
|
||||
name = dept_name or state.dept
|
||||
if st in ACTIVE and station_name:
|
||||
return f"{name} · {station_name}"
|
||||
return name
|
||||
|
||||
|
||||
def _works_on_assigned(principal: Principal, state: PacketState) -> bool:
|
||||
"""START and SUBMIT: a lead or above in the department, or the assigned welder within their station limits."""
|
||||
level = principal.level(state.dept)
|
||||
if level >= LEAD:
|
||||
return True
|
||||
return (
|
||||
level == WORKER
|
||||
and principal.user_id is not None
|
||||
and state.welder_id is not None
|
||||
and str(state.welder_id) == str(principal.user_id)
|
||||
and works_on(principal, state)
|
||||
)
|
||||
|
||||
|
||||
def _co_verifier(principal: Principal, extra: Mapping[str, Any]) -> bool:
|
||||
"""A supervisor of the department named in the request (extra["dept"])."""
|
||||
dept = extra.get("dept")
|
||||
if not isinstance(dept, str) or not dept:
|
||||
return False
|
||||
if dept in ("WH", "QC"):
|
||||
return dept in principal.support_sup
|
||||
if dept in SUPPORT_CODES:
|
||||
return False # Clerical issues change orders; it never verifies them. PM has no supervisor.
|
||||
return principal.level(dept) >= SUPERVISOR
|
||||
|
||||
|
||||
def allows(who: Who, principal: Principal, state: PacketState, extra: Mapping[str, Any]) -> bool:
|
||||
"""Evaluate one Who predicate. Admin is never allowed here except by Who.ADMIN; the bypass is a rule."""
|
||||
if who is Who.CL:
|
||||
return "CL" in principal.support
|
||||
if who is Who.CL_SUP:
|
||||
return "CL" in principal.support_sup
|
||||
if who is Who.WH:
|
||||
return "WH" in principal.support
|
||||
if who is Who.WH_SUP:
|
||||
return "WH" in principal.support_sup
|
||||
if who is Who.QC:
|
||||
return "QC" in principal.support
|
||||
if who is Who.QC_SUP:
|
||||
return "QC" in principal.support_sup
|
||||
if who is Who.CL_OR_QC:
|
||||
return "CL" in principal.support or "QC" in principal.support
|
||||
if who is Who.HOLDER:
|
||||
return holds(principal, state)
|
||||
if who is Who.DEPT_LEAD:
|
||||
return principal.level(state.dept) >= LEAD
|
||||
if who is Who.DEPT_SUP:
|
||||
return principal.level(state.dept) >= SUPERVISOR
|
||||
if who is Who.WORKS_ON:
|
||||
return _works_on_assigned(principal, state)
|
||||
if who is Who.CO_VERIFIER:
|
||||
return _co_verifier(principal, extra)
|
||||
if who is Who.ADMIN:
|
||||
return principal.admin
|
||||
if who is Who.SYSTEM:
|
||||
return principal.system is not None
|
||||
return False
|
||||
Reference in New Issue
Block a user