"""Change C8 (Gitea issue 23): checklists per department. The department supervisor edits their own department's list, Admin edits any, a lead, a worker and Q.C. do not. A save carries the version (`base` or If-Match) and is audited. A stage freezes its list at START, so a change reaches only stages not yet started. A department without its own list uses the default for its kind. Nothing here reads VISUAL or M-Files.""" from __future__ import annotations from collections.abc import Iterator from typing import Any import pytest from fakedir import FakeDirectory from shophelpers import ( PLAIN_KEYS, WELD_KEYS, Shop, add_weld, fresh, ok, one, open_shop, ready_packet, refused, set_filler, start, tick_all, to_station, ) from packetstation.domain import checklists from packetstation.settings import Settings URL = "/api/checklists" NEW_LIST = [ {"group": "Before welding", "key": "dwg", "text": "Drawing {drawing} Rev {rev} on the bench"}, {"group": "Before welding", "key": "purge", "text": "Back purge set"}, {"group": "After welding", "key": "vt", "text": "Visual done"}, ] @pytest.fixture def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]: yield from open_shop(pg_settings, fake_dir) def keys(pkt: dict[str, Any]) -> list[str]: return [i["key"] for i in pkt["checklist_items"]] def put(shop: Shop, sam: str, dept: str, items: Any, base: int | None) -> Any: body: dict[str, Any] = {"items": items} if base is not None: body["base"] = base return shop(sam).put(f"{URL}/{dept}", json=body) def test_list_and_get(shop: Shop) -> None: rows = {r["dept_code"]: r for r in ok(shop("sup.wes").get(URL))} assert "03" in rows and "CL" not in rows and "QC" not in rows assert rows["03"]["editable"] is True and rows["03"]["weld"] is True and rows["03"]["custom"] is False assert rows["03"]["version"] == 0 and rows["03"]["dept_name"] == "Weld Parts" assert not any(r["editable"] for code, r in rows.items() if code != "03") assert all(r["editable"] for r in ok(shop("admin.ann").get(URL))) for sam in ("lead.lou", "weld.andy", "qc.quinn", "clerk.clara"): assert not any(r["editable"] for r in ok(shop(sam).get(URL))), sam got = ok(shop("qc.quinn").get(f"{URL}/03")) assert [i["key"] for i in got["items"]] == list(WELD_KEYS) and got["defaults"] == got["items"] assert got["editable"] is False and got["custom"] is False assert "{drawing}" in got["items"][0]["text"] # unfilled plain = ok(shop("sup.wes").get(f"{URL}/09")) assert [i["key"] for i in plain["items"]] == list(PLAIN_KEYS) and plain["editable"] is False refused(shop("sup.wes").get(f"{URL}/QC"), 404, "not_found") def test_who_may_save(shop: Shop) -> None: for sam in ("lead.lou", "weld.andy", "qc.quinn", "clerk.clara", "pm.paula"): refused(put(shop, sam, "03", NEW_LIST, 0), 403, "forbidden") refused(put(shop, "sup.wes", "09", NEW_LIST, 0), 403, "forbidden") # not his department out = ok(put(shop, "sup.wes", "03", NEW_LIST, 0)) assert out["custom"] is True and out["version"] == 1 and [i["key"] for i in out["items"]] == ["dwg", "purge", "vt"] # Admin edits any department out = ok(put(shop, "admin.ann", "09", NEW_LIST, 0)) assert out["custom"] is True and out["dept_code"] == "09" # the other departments keep the default for their kind assert ok(shop("sup.wes").get(f"{URL}/04"))["custom"] is False row = one( shop.settings, "SELECT count(*) FROM audit_events WHERE action = 'checklists.dept_updated' AND subject_id = 'checklist:03'", ) assert row is not None and row[0] == 1 def test_version_cas_and_validation(shop: Shop) -> None: ok(put(shop, "sup.wes", "03", NEW_LIST, 0)) stale = refused(put(shop, "sup.wes", "03", NEW_LIST, 0), 409, "version_conflict") assert stale["current"]["version"] == 1 and stale["current"]["custom"] is True refused(put(shop, "sup.wes", "03", NEW_LIST, None), 428, "if_match_required") r = shop("sup.wes").put(f"{URL}/03", json={"items": NEW_LIST}, headers={"If-Match": '"1"'}) assert ok(r)["version"] == 2 bad = refused(put(shop, "sup.wes", "03", [{"group": "G", "key": "Bad-Key", "text": "t"}], 2), 422, "bad_checklist") assert "key" in bad["detail"] refused(put(shop, "sup.wes", "03", [], 2), 422, "bad_checklist") refused(put(shop, "sup.wes", "03", [{"group": "G", "key": "x", "text": "{stamp}"}], 2), 422, "bad_checklist") # null sets the department back to the default for its kind back = ok(put(shop, "sup.wes", "03", None, 2)) assert back["custom"] is False and back["version"] == 3 and [i["key"] for i in back["items"]] == list(WELD_KEYS) def test_a_change_reaches_only_stages_not_started(shop: Shop) -> None: andy = shop("weld.andy") started = start(shop, to_station(shop, ready_packet(shop, drawing="D-7007", rev="A"))) assigned = to_station(shop, ready_packet(shop), station="Stall 2") waiting = ready_packet(shop) ok(put(shop, "sup.wes", "03", NEW_LIST, 0)) # the started stage keeps the default it started with assert keys(fresh(andy, started["id"])) == list(WELD_KEYS) refused(andy.put(f"/api/packets/{started['id']}/stage/checklist/purge", json={"done": True}), 422, "invalid_key") # the stages not yet started read the new list assert keys(fresh(andy, assigned["id"])) == ["dwg", "purge", "vt"] assert keys(fresh(shop("sup.wes"), waiting["id"])) == ["dwg", "purge", "vt"] # START freezes: a later change does not reach it later = start(shop, assigned) ok(put(shop, "sup.wes", "03", [*NEW_LIST, {"group": "After welding", "key": "tag", "text": "Tagged"}], 1)) assert keys(fresh(andy, later["id"])) == ["dwg", "purge", "vt"] assert keys(fresh(shop("sup.wes"), waiting["id"])) == ["dwg", "purge", "vt", "tag"] stage = one( shop.settings, "SELECT checklist_template FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL", (later["id"],), ) assert stage is not None and [i["key"] for i in stage[0]] == ["dwg", "purge", "vt"] # completeness reads the frozen list: three ticks, a weld and the filler let it submit out = tick_all(shop, later, keys=("dwg", "purge", "vt")) out = set_filler(shop, add_weld(shop, out)) assert "submit" in fresh(andy, out["id"])["can"] # the started packet still needs the default keys assert "submit" not in fresh(andy, started["id"])["can"] # the data book's checklist section reads the frozen list too with shop_conn(shop) as conn: t = checklists.stage_templates(conn, stage_id(shop, later["id"]), "03") assert [i["key"] for i in t["weld"]] == ["dwg", "purge", "vt"] live = checklists.stage_templates(conn, None, "03") assert [i["key"] for i in live["weld"]] == ["dwg", "purge", "vt", "tag"] def stage_id(shop: Shop, packet_id: int) -> int: row = one(shop.settings, "SELECT id FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL", (packet_id,)) assert row is not None return int(row[0]) def shop_conn(shop: Shop) -> Any: from pshelpers import app_conn return app_conn(shop.settings)