"""The policy's view of a person (Principal) and of a packet (PacketState).""" from __future__ import annotations from collections.abc import Iterable, Mapping from dataclasses import dataclass, field from types import MappingProxyType from ..auth.groups import SHOP_LEVELS, parse_role SUPPORT = ("CL", "WH", "QC") LEVELS: dict[str, int] = {level: i + 1 for i, level in enumerate(SHOP_LEVELS)} # worker 1, lead 2, supervisor 3 @dataclass(frozen=True) class Principal: user_id: str | None # uuid as text; None for system admin: bool = False support: frozenset[str] = frozenset() # codes among CL, WH, QC the user is a member of (any level) support_sup: frozenset[str] = frozenset() # codes among CL, WH, QC where the user is Supervisor shop: Mapping[str, int] = field(default_factory=lambda: MappingProxyType({})) # code -> 1 worker, 2 lead, 3 sup station_limits: frozenset[int] = frozenset() # station ids a worker is limited to; empty = any pm: bool = False session_kind: str = "password" # password | pin system: str | None = None # "visual-feed" for the worker; never from a session def level(self, dept: str) -> int: """Shop level in a department: 0 none, 1 worker, 2 lead, 3 supervisor.""" return int(self.shop.get(dept, 0)) @dataclass(frozen=True) class PacketState: id: int status: str # packet_status value dept: str # dept_code station_id: int | None welder_id: str | None # uuid text of the assigned welder wh_skipped: bool routed_to: str | None # databook | shipping | None vclosed: bool removed: bool source_kind: str # visual | hand weld_dept: bool # departments.weld co_hold: bool # an approved CO with pending verifications, or a pending CO with hold=true co_hold_why: str = "" # text for the error open_shorts: int = 0 drawing_attached: bool = False heat_map: bool | None = None bom_total: int = 0 # lines not marked n/a bom_ready: int = 0 # lines pulled, or with an open short bom_missing_heat: int = 0 # pulled lines with pieces issued and no heat (not n/a, not a sub-assembly) welds: int = 0 checklist_complete: bool = False # every key of the department checklist is done (empty list is complete) filler_set: bool = False qc_missing: tuple[str, ...] = () # the old qcMissing() list, computed by the read model has_qc_sent: bool = False # qc_inspections row exists for the current stage shorts_unapproved: int = 0 # open shorts plus received shorts the supervisor has not approved (C7) itp_open: bool = False # the packet's unit has an ITP that is not complete (C3) ncr_required: bool = False # a Q.C. reject since the last accept has no NCR number (C2) visual_open_wos: tuple[str, ...] = () # the work orders the feed last saw open in VISUAL (C5) SYSTEM_FEED = Principal(user_id=None, system="visual-feed") def principal_from_roles( user_id: str | None, roles: Iterable[str], station_limits: Iterable[int] = (), session_kind: str = "password", ) -> Principal: """Build the policy Principal from phase 1 role strings. A role that does not parse is ignored.""" admin = False pm = False support: set[str] = set() support_sup: set[str] = set() shop: dict[str, int] = {} for text in roles: try: role = parse_role(text) except ValueError: continue if role.kind == "admin": admin = True elif role.kind == "pm": pm = True elif role.kind.startswith("shop_") and role.dept_code: level = LEVELS[role.kind.removeprefix("shop_")] shop[role.dept_code] = max(level, shop.get(role.dept_code, 0)) elif role.dept_code in SUPPORT: support.add(role.dept_code) if role.kind.endswith("_supervisor"): support_sup.add(role.dept_code) return Principal( user_id=user_id, admin=admin, support=frozenset(support), support_sup=frozenset(support_sup), shop=MappingProxyType(shop), station_limits=frozenset(int(s) for s in station_limits), pm=pm, session_kind=session_kind, )