183 lines
6.3 KiB
Python
183 lines
6.3 KiB
Python
"""Printed copies and exports: who the PDF builders stamp, the file names, and the append-only print log.
|
|||
|
|
|
||
|
|
Every print and every export writes one print_log row and one audit row, in its own transaction, after the bytes
|
||
|
|
were built. Prints and exports are not packet changes: they write no packet_log row and do not bump the version.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from collections.abc import Mapping
|
||
|
|
from typing import TYPE_CHECKING, Any, Literal, cast
|
||
|
|
from urllib.parse import quote
|
||
|
|
|
||
|
|
from .. import audit
|
||
|
|
from ..api import deps
|
||
|
|
from . import assets
|
||
|
|
from . import packets as packets_dom
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from ..pdf.style import WhoRef
|
||
|
|
|
||
|
|
PRINTS_LIMIT = 200
|
||
|
|
|
||
|
|
# The print_log kinds of the exports (prints use the print kind itself), and the audit kind of each.
|
||
|
|
EXPORT_KINDS = {"databook-export": "databook", "traveler": "traveler", "csv": "csv", "itp-pdf": "itp"}
|
||
|
|
|
||
|
|
|
||
|
|
def who_of(p: deps.Principal) -> WhoRef:
|
||
|
|
"""The signed-in person as the builders' WhoRef: {"id", "display_name", "emp_no"}."""
|
||
|
|
emp = p.user.get("emp_no")
|
||
|
|
return cast(
|
||
|
|
"WhoRef",
|
||
|
|
{
|
||
|
|
"id": str(p.user_id),
|
||
|
|
"display_name": str(p.user.get("display_name") or p.user.get("sam_account") or ""),
|
||
|
|
"emp_no": None if emp is None or str(emp).strip() == "" else str(emp),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def who_of_user(conn: Any, user_id: Any) -> WhoRef:
|
||
|
|
"""The same WhoRef for a user id (the ITP hook gets an actor, not a principal)."""
|
||
|
|
r = conn.execute("SELECT display_name, emp_no FROM users WHERE id = %s", (user_id,)).fetchone()
|
||
|
|
name = str(r[0] or "") if r else ""
|
||
|
|
emp = r[1] if r else None
|
||
|
|
return cast(
|
||
|
|
"WhoRef",
|
||
|
|
{"id": str(user_id), "display_name": name, "emp_no": None if emp is None or str(emp) == "" else str(emp)},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- file names
|
||
|
|
|
||
|
|
|
||
|
|
def job_base(read: Mapping[str, Any]) -> str:
|
||
|
|
"""{project}-Lot{lot}: the top project number and the unit's lot, as the old build named its files."""
|
||
|
|
unit = read.get("unit") or {}
|
||
|
|
project = (unit.get("project") or {}).get("number") or ""
|
||
|
|
lot = unit.get("lot") or unit.get("number") or ""
|
||
|
|
return f"{project}-Lot{lot}"
|
||
|
|
|
||
|
|
|
||
|
|
def _named(text: str, fallback: str) -> str:
|
||
|
|
return assets.safe_name(text) or fallback
|
||
|
|
|
||
|
|
|
||
|
|
def databook_name(read: Mapping[str, Any]) -> str:
|
||
|
|
return _named(f"{job_base(read)}_{read.get('drawing') or ''}_Manufacturing_Data_Book.pdf", "Data_Book.pdf")
|
||
|
|
|
||
|
|
|
||
|
|
def traveler_name(read: Mapping[str, Any]) -> str:
|
||
|
|
return _named(f"{job_base(read)}_Job_Traveler.pdf", "Job_Traveler.pdf")
|
||
|
|
|
||
|
|
|
||
|
|
def print_name(read: Mapping[str, Any], what: str) -> str:
|
||
|
|
return _named(f"{job_base(read)} {what}.pdf", "Print.pdf")
|
||
|
|
|
||
|
|
|
||
|
|
def csv_name(read: Mapping[str, Any]) -> str:
|
||
|
|
return _named(f"{job_base(read)}_{read.get('drawing') or ''}_traceability.csv", "traceability.csv")
|
||
|
|
|
||
|
|
|
||
|
|
def itp_name(no: Any) -> str:
|
||
|
|
return _named(f"{no or 'ITP'}.pdf", "ITP.pdf")
|
||
|
|
|
||
|
|
|
||
|
|
def disposition(filename: str, download: bool = False) -> str:
|
||
|
|
"""Content-Disposition: inline unless a download is asked for. An ASCII filename, plus filename* (RFC 5987) when
|
||
|
|
the name has characters outside ASCII."""
|
||
|
|
kind = "attachment" if download else "inline"
|
||
|
|
plain = filename.encode("ascii", "replace").decode("ascii").replace("?", "_")
|
||
|
|
plain = plain.replace('"', "").replace(";", "_").replace("\\", "_").strip() or "download"
|
||
|
|
out = f'{kind}; filename="{plain}"'
|
||
|
|
if plain != filename:
|
||
|
|
out += f"; filename*=UTF-8''{quote(filename, safe='')}"
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- the log
|
||
|
|
|
||
|
|
|
||
|
|
def record(
|
||
|
|
conn: Any,
|
||
|
|
p: deps.Principal,
|
||
|
|
*,
|
||
|
|
action: Literal["print", "export"],
|
||
|
|
kind: str,
|
||
|
|
what: str,
|
||
|
|
pages: int | None,
|
||
|
|
packet_id: int | None = None,
|
||
|
|
unit_id: int | None = None,
|
||
|
|
itp_id: int | None = None,
|
||
|
|
asset_id: Any = None,
|
||
|
|
stamp: str | None = None,
|
||
|
|
stamp_sig: str | None = None,
|
||
|
|
detail: Mapping[str, Any] | None = None,
|
||
|
|
) -> int:
|
||
|
|
"""One print_log row and one audit row (action `print` or `export`), in one transaction. The audit detail of an
|
||
|
|
export names its kind as databook, traveler, csv or itp."""
|
||
|
|
audit_detail: dict[str, Any] = {
|
||
|
|
"kind": EXPORT_KINDS.get(kind, kind) if action == "export" else kind,
|
||
|
|
"what": what,
|
||
|
|
"pages": pages,
|
||
|
|
"stamp_sig": stamp_sig,
|
||
|
|
}
|
||
|
|
audit_detail.update({k: v for k, v in (detail or {}).items() if v is not None})
|
||
|
|
if asset_id is not None:
|
||
|
|
audit_detail["asset_id"] = str(asset_id)
|
||
|
|
if itp_id is not None:
|
||
|
|
audit_detail["itp_id"] = itp_id
|
||
|
|
with conn.transaction():
|
||
|
|
new_id = conn.execute(
|
||
|
|
"INSERT INTO print_log (by, by_name, session_id, packet_id, unit_id, itp_id, kind, what, pages, asset_id,"
|
||
|
|
" stamp, stamp_sig, ip) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id",
|
||
|
|
(
|
||
|
|
p.user_id,
|
||
|
|
p.user.get("display_name"),
|
||
|
|
p.session_id,
|
||
|
|
packet_id,
|
||
|
|
unit_id,
|
||
|
|
itp_id,
|
||
|
|
kind,
|
||
|
|
what,
|
||
|
|
pages,
|
||
|
|
asset_id,
|
||
|
|
stamp,
|
||
|
|
stamp_sig,
|
||
|
|
p.ip,
|
||
|
|
),
|
||
|
|
).fetchone()[0]
|
||
|
|
audit.append(
|
||
|
|
conn,
|
||
|
|
action=action,
|
||
|
|
user_id=p.user_id,
|
||
|
|
session_id=p.session_id,
|
||
|
|
role_snapshot=p.role_snapshot,
|
||
|
|
subject_type="itp" if itp_id is not None and packet_id is None else "packet",
|
||
|
|
subject_id=itp_id if itp_id is not None and packet_id is None else packet_id,
|
||
|
|
packet_id=packet_id,
|
||
|
|
detail=audit_detail,
|
||
|
|
ip=p.ip,
|
||
|
|
)
|
||
|
|
return int(new_id)
|
||
|
|
|
||
|
|
|
||
|
|
def list_prints(conn: Any, packet_id: int, limit: int = PRINTS_LIMIT) -> list[dict[str, Any]]:
|
||
|
|
"""A packet's printed copies and exports, newest first."""
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"id": r["id"],
|
||
|
|
"at": r["at"],
|
||
|
|
"by": None if r["by"] is None else {"id": str(r["by"]), "display_name": r["by_name"]},
|
||
|
|
"kind": r["kind"],
|
||
|
|
"what": r["what"],
|
||
|
|
"pages": r["pages"],
|
||
|
|
}
|
||
|
|
for r in packets_dom.rows(
|
||
|
|
conn,
|
||
|
|
"SELECT id, at, by, by_name, kind, what, pages FROM print_log WHERE packet_id = %s"
|
||
|
|
" ORDER BY id DESC LIMIT %s",
|
||
|
|
(packet_id, limit),
|
||
|
|
)
|
||
|
|
]
|