Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+8 -41
View File
@@ -1,11 +1,11 @@
"""Admin: departments and stations, people's profiles (one at a time and the CSV import), checklist templates.
"""Admin: departments and stations, people's profiles (one at a time and the CSV import), the default checklists by
kind (weld and plain; each department's own list is at /api/checklists, change C8).
Every route needs the admin role and a password session. Every change is audited (`admin.*`).
"""
from __future__ import annotations
import re
import uuid
from typing import Any
@@ -171,9 +171,6 @@ def profile_import(
# ---------------------------------------------------------------- checklists
RX_BRACES = re.compile(r"\{[^{}]*\}")
ALLOWED_PLACEHOLDERS = ("{drawing}", "{rev}", "{wps}")
class ChecklistsPut(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -183,7 +180,8 @@ class ChecklistsPut(BaseModel):
def check_templates(value: Any) -> tuple[dict[str, list[dict[str, str]]], list[str]]:
"""The checklist templates cleaned, and one error line per problem."""
"""The default checklists by kind (weld and plain) cleaned, and one error line per problem. A department with
its own list (change C8, /api/checklists) does not read these."""
errors: list[str] = []
out: dict[str, list[dict[str, str]]] = {}
if not isinstance(value, dict):
@@ -192,41 +190,10 @@ def check_templates(value: Any) -> tuple[dict[str, list[dict[str, str]]], list[s
if extra:
errors.append(f"unknown list: {', '.join(extra)}")
for name in ("weld", "plain"):
items = value.get(name)
if not isinstance(items, list) or not items:
errors.append(f"{name}: at least one item")
continue
if len(items) > checklists.ITEMS_MAX:
errors.append(f"{name}: at most {checklists.ITEMS_MAX} items")
seen: set[str] = set()
clean: list[dict[str, str]] = []
for i, it in enumerate(items, start=1):
where = f"{name} item {i}"
if not isinstance(it, dict):
errors.append(f"{where}: must be an object with group, key and text")
continue
group = " ".join(str(it.get("group") or "").split())
key = str(it.get("key") or "").strip()
text = " ".join(str(it.get("text") or "").split())
if not checklists.RX_KEY.match(key):
errors.append(f"{where}: the key is a lower-case letter, then letters, digits or _ (at most 30)")
elif key in seen:
errors.append(f"{where}: key {key} is used twice")
seen.add(key)
if not group:
errors.append(f"{where}: the group is empty")
if not text:
errors.append(f"{where}: the text is empty")
if len(group) > checklists.TEXT_MAX or len(text) > checklists.TEXT_MAX:
errors.append(f"{where}: at most {checklists.TEXT_MAX} characters")
bad = [m for m in RX_BRACES.findall(text) if m not in ALLOWED_PLACEHOLDERS]
stray = RX_BRACES.sub("", text)
if bad:
errors.append(f"{where}: unknown placeholder {', '.join(bad)} (use {{drawing}}, {{rev}} or {{wps}})")
elif "{" in stray or "}" in stray:
errors.append(f"{where}: a brace without its pair")
clean.append({"group": group, "key": key, "text": text})
out[name] = clean
clean, errs = checklists.check_list(name, value.get(name))
errors += errs
if clean is not None:
out[name] = clean
return out, errors