Phase 9: Admin completion, reconcile, notifications, integrity, backups
Admin: Departments (name and stations; add shows the three groups IT must create), Profiles (welder stamp, station limits, active) with a CSV import that previews and applies only when clean, Checklists (weld and plain), Security (session, lockout and rate-limit settings with bounds, alerts for the last 24 hours, the integrity result), Access log with filters and a guarded CSV, Who is in, Stamp check, Notifications, and the employeeID flags. Every Admin change is audited. Worker: ad-reconcile (nightly and on "refresh from AD now"), session-refresh (15 minutes), integrity (seals, asset hashes, missing and orphan files, the audit chain), backup-check, notify-send and notify-digest. Notifications route per event type to AD groups, users, mailboxes or the department Supervisor group, immediately or in a daily digest, over STARTTLS verified against the PESCO CA, with dry run on the test stack. Backups: scripts/backup/ps-backup.sh and ps-restore.sh with a systemd timer at 02:00 America/Denver. Restore rehearsed on the test stack 2026-10-21. CI: pip-audit and npm audit. pypdf 6.21.1 and certifi 2026.10.19. Migration 0010. Closes the app part of I6, M3, O1, O2 and S3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""qc_reject is raised in the write's transaction: a Q.C. reject of a packet (its department) and an ITP line reject
|
||||
(the department working to the line's page, when only one does)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
|
||||
import pytest
|
||||
from fakedir import FakeDirectory
|
||||
from pshelpers import needs_pg
|
||||
from qchelpers import at_qc, open_qc_shop, reject, reject_line, start_itp
|
||||
from shophelpers import Shop, make_unit, ok, one
|
||||
|
||||
from packetstation.settings import Settings
|
||||
|
||||
pytestmark = needs_pg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
|
||||
yield from open_qc_shop(pg_settings, fake_dir)
|
||||
|
||||
|
||||
def test_qc_and_itp_rejects_raise_qc_reject(shop: Shop) -> None:
|
||||
pkt = at_qc(shop)
|
||||
ok(reject(shop, pkt, "Weld 2 undercut too deep"))
|
||||
row = one(
|
||||
shop.settings,
|
||||
"SELECT kind, event_type, subject, dept_code, title, body, resolved_at, dispatched_at"
|
||||
" FROM notification_events WHERE event_type = 'qc_reject'",
|
||||
)
|
||||
assert row[0] == row[1] == "qc_reject"
|
||||
dept = one(shop.settings, "SELECT dept_code FROM packets WHERE id = %s", (pkt["id"],))[0]
|
||||
assert row[2] == f"packet:{pkt['id']}" and row[3] == dept == "03"
|
||||
assert row[4].startswith(f"Q.C. rejected packet {pkt['id']}")
|
||||
assert "Weld 2 undercut too deep" in row[5] and "Quinn QC" in row[5]
|
||||
assert row[6] is not None and row[7] is None # a one-shot, waiting for notify-send
|
||||
|
||||
_, unit_id = make_unit(shop.settings)
|
||||
itp = start_itp(shop, unit_id)
|
||||
ok(reject_line(shop("qc.quinn"), itp, "1.4", note="Manual is rev C"))
|
||||
row = one(
|
||||
shop.settings,
|
||||
"SELECT subject, dept_code, title, body, detail FROM notification_events"
|
||||
" WHERE event_type = 'qc_reject' AND subject <> %s",
|
||||
(f"packet:{pkt['id']}",),
|
||||
)
|
||||
assert row[0] == f"itp:{itp['id']}:1.4"
|
||||
assert row[1] is None # no packet of that unit works to the page
|
||||
assert row[2].startswith(f"ITP {itp['no']} line 1.4 rejected")
|
||||
assert "NCR # pending" in row[3] and "Manual is rev C" in row[3]
|
||||
assert row[4]["line"] == "1.4" and row[4]["itp_id"] == itp["id"]
|
||||
Reference in New Issue
Block a user