drawings: on a rev mismatch, Clerical can keep the retired rev VISUAL calls (#41)
When VISUAL calls a rev that M-Files has retired, the draft is still flagged rev_mismatch, but the drawing job now also copies the rev VISUAL calls (MFilesKit 0.1.3 DrawingLookup.revision) into drawing_revisions and visual.called. A Clerical Supervisor picks either rev on the packet: "Use latest rev N" as before, or "Use rev V as VISUAL calls it" (rev-approve with use=called), with a reason and a signature. The kept rev holds while M-Files' Effective rev stays the one chosen against: no flag, no change order. A newer Effective rev flags the packet again; a new VISUAL rev clears the choice. Migration 0009 adds drawing_revisions. Needs mfileskit 0.1.3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -4,7 +4,9 @@ flag, stage or open a change order on the packets that use them.
|
||||
Per run: the queue (drawings/queue.py) cut to `drawing_budget`; per drawing one `Lookup.status`, a fetch when a
|
||||
released file exists (drawings/fetch.py downloads only when its M-Files identity changed), the `drawing_lookups`
|
||||
row, then `decide` for every live packet that uses the drawing (its own drawing, or the target of its pending feed
|
||||
change order), applied through `feed.writes.system_write`. A busy packet is left for the next run.
|
||||
change order), applied through `feed.writes.system_write`. A busy packet is left for the next run. On a draft
|
||||
flagged rev_mismatch, the rev VISUAL calls is looked up too (`Lookup.revision`) and copied into assets when M-Files
|
||||
still holds it (Retired, say), so Clerical can choose it or the Effective rev.
|
||||
|
||||
Never writes to M-Files.
|
||||
"""
|
||||
@@ -56,6 +58,7 @@ def new_counts() -> Counts:
|
||||
"busy": 0,
|
||||
"refused": 0,
|
||||
"no_file": 0,
|
||||
"called_fetched": 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +127,9 @@ def _drawing(ctx: JobContext, lk: Lookup, item: queue.QueueItem, counts: Counts,
|
||||
vrev_seen = item.visual_rev if item.co_id is None else own_rev(conn, item.dwg)
|
||||
save_lookup(conn, item, found, fetched, now, fetch_failed=fetch_failed, visual_rev_seen=vrev_seen)
|
||||
scope = [str(d) for d in (getattr(ctx.feed_settings, "departments", None) or [])]
|
||||
called: dict[int, dict[str, Any] | None] = {} # VISUAL rev -> what M-Files holds at that rev, this drawing
|
||||
for pid, relations in sharing(conn, item.dwg, scope):
|
||||
_packet(ctx, item.dwg, found, fetched, pid, relations, counts)
|
||||
_packet(ctx, lk, item.dwg, found, fetched, pid, relations, counts, called)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- drawing_lookups
|
||||
@@ -206,6 +210,92 @@ def save_lookup(
|
||||
)
|
||||
|
||||
|
||||
def save_revision(conn: Any, dwg: str, rf: Found, fetched: FetchResult, now: datetime) -> None:
|
||||
"""Upsert the drawing_revisions row for a rev `Lookup.revision` found and the copy of its file."""
|
||||
conn.execute(
|
||||
"INSERT INTO drawing_revisions (dwg, rev, checked_at, controlled, state, name, obj_id, obj_version, file_id,"
|
||||
" file_version, file_size, base, asset_id, sha256) VALUES (%(dwg)s, %(rev)s, %(now)s, %(controlled)s,"
|
||||
" %(state)s, %(name)s, %(obj_id)s, %(obj_version)s, %(file_id)s, %(file_version)s, %(file_size)s, %(base)s,"
|
||||
" %(asset_id)s, %(sha)s) ON CONFLICT (dwg, rev) DO UPDATE SET checked_at = EXCLUDED.checked_at,"
|
||||
" controlled = EXCLUDED.controlled, state = EXCLUDED.state, name = EXCLUDED.name, obj_id = EXCLUDED.obj_id,"
|
||||
" obj_version = EXCLUDED.obj_version, file_id = EXCLUDED.file_id, file_version = EXCLUDED.file_version,"
|
||||
" file_size = EXCLUDED.file_size, base = EXCLUDED.base, asset_id = EXCLUDED.asset_id, sha256 = EXCLUDED.sha256",
|
||||
{
|
||||
"dwg": dwg,
|
||||
"rev": rf.rev,
|
||||
"now": now,
|
||||
"controlled": rf.controlled,
|
||||
"state": rf.state or None,
|
||||
"name": fetched.name or rf.name,
|
||||
"obj_id": rf.obj_id,
|
||||
"obj_version": rf.obj_version,
|
||||
"file_id": rf.file_id,
|
||||
"file_version": rf.file_version,
|
||||
"file_size": rf.file_size,
|
||||
"base": rf.base,
|
||||
"asset_id": fetched.asset_id,
|
||||
"sha": fetched.sha256,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def called_rev(
|
||||
ctx: JobContext, lk: Lookup, dwg: str, visual_rev: str, memo: dict[int, dict[str, Any] | None], counts: Counts
|
||||
) -> dict[str, Any] | None:
|
||||
"""The rev VISUAL calls, as M-Files holds it: {rev, controlled, state, name, asset_id, sha256, pages}, or
|
||||
{rev, missing: true} when M-Files has no document at that rev. None when it could not be read this run (the
|
||||
packet keeps what it had) or the VISUAL rev is not a number."""
|
||||
v = D.as_int(visual_rev)
|
||||
if v is None:
|
||||
return None
|
||||
if v in memo:
|
||||
return memo[v]
|
||||
memo[v] = None
|
||||
conn = ctx.conn
|
||||
try:
|
||||
rf = lk.revision(dwg, v)
|
||||
except LookupError as e:
|
||||
ctx.error(f"drawing {dwg}: rev {v}: {e}")
|
||||
return None
|
||||
if rf is None:
|
||||
memo[v] = {"rev": str(v), "missing": True}
|
||||
return memo[v]
|
||||
try:
|
||||
fetched = fetch(conn, ctx.settings, lk, rf, dwg=dwg)
|
||||
except FetchError as e:
|
||||
counts["fetch_failed"] += 1
|
||||
ctx.error(str(e))
|
||||
return None
|
||||
with conn.transaction():
|
||||
save_revision(conn, dwg, rf, fetched, datetime.now(UTC))
|
||||
if fetched.downloaded:
|
||||
counts["called_fetched"] += 1
|
||||
audit.append(
|
||||
conn,
|
||||
action="drawing.fetched",
|
||||
role_snapshot="system",
|
||||
subject_type="asset",
|
||||
subject_id=str(fetched.asset_id),
|
||||
detail={
|
||||
"dwg": dwg,
|
||||
"rev": v,
|
||||
"controlled": rf.controlled,
|
||||
"sha256": fetched.sha256.hex(),
|
||||
"reused": fetched.reused,
|
||||
},
|
||||
)
|
||||
memo[v] = {
|
||||
"rev": str(v),
|
||||
"controlled": rf.controlled,
|
||||
"state": rf.state or None,
|
||||
"name": fetched.name or rf.name,
|
||||
"asset_id": str(fetched.asset_id),
|
||||
"sha256": fetched.sha256.hex(),
|
||||
"pages": fetched.pages,
|
||||
}
|
||||
return memo[v]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- packets
|
||||
|
||||
|
||||
@@ -307,10 +397,13 @@ def target_for(dwg: str, pf: PacketFacts, *, own: bool) -> D.Target:
|
||||
co_waiting = _dwg_rev(waiting)
|
||||
co_staged = _dwg_rev(staged)
|
||||
packet_rev = str(row["rev"] or "").strip()
|
||||
rev_ok = None
|
||||
rev_ok = kept_latest = None
|
||||
ro = visual.get("rev_ok")
|
||||
if isinstance(ro, dict) and ro.get("rev") is not None and str(ro.get("vis_rev") or "").strip() == packet_rev:
|
||||
rev_ok = str(ro["rev"]).strip()
|
||||
if ro.get("use") == "called":
|
||||
kept_latest = None if ro.get("latest") is None else str(ro["latest"]).strip()
|
||||
else:
|
||||
rev_ok = str(ro["rev"]).strip()
|
||||
visual_rev = packet_rev if own or co_waiting is None else co_waiting[1]
|
||||
return D.Target(
|
||||
dwg=dwg,
|
||||
@@ -326,6 +419,7 @@ def target_for(dwg: str, pf: PacketFacts, *, own: bool) -> D.Target:
|
||||
co_pending=co is not None,
|
||||
co_staged_sha=_sha(staged.get("sha256")) if isinstance(staged, dict) else None,
|
||||
rejected_sha=pf.rejected_sha,
|
||||
kept_latest=kept_latest,
|
||||
)
|
||||
|
||||
|
||||
@@ -360,12 +454,14 @@ def new_flags(flags: list[str], d: D.Decision) -> list[str]:
|
||||
|
||||
def _packet(
|
||||
ctx: JobContext,
|
||||
lk: Lookup,
|
||||
dwg: str,
|
||||
found: Found,
|
||||
fetched: FetchResult | None,
|
||||
pid: int,
|
||||
relations: set[str],
|
||||
counts: Counts,
|
||||
called_memo: dict[int, dict[str, Any] | None],
|
||||
) -> None:
|
||||
conn = ctx.conn
|
||||
pf = facts(conn, pid)
|
||||
@@ -393,6 +489,12 @@ def _packet(
|
||||
confirmed = D.rev_confirmed(t, found)
|
||||
if bool(visual.get("rev_confirmed")) != confirmed:
|
||||
patch["rev_confirmed"] = confirmed
|
||||
if d.kind == D.REV_MISMATCH and row["status"] == "draft":
|
||||
called = called_rev(ctx, lk, dwg, t.visual_rev, called_memo, counts)
|
||||
if called is not None and visual.get("called") != called:
|
||||
patch["called"] = called
|
||||
elif visual.get("called") is not None:
|
||||
patch["called"] = None
|
||||
state_after = d.drawing_state if own else str(row["drawing_state"])
|
||||
material = (
|
||||
d.kind in (D.ATTACH, D.OPEN_CO, D.STAGE_FOR_CO)
|
||||
@@ -405,6 +507,11 @@ def _packet(
|
||||
return
|
||||
action = A.VIS_UPDATE_DRAFT if row["status"] == "draft" else A.VIS_OPEN_CO
|
||||
label, note = "Drawing status from M-Files", d.note or None
|
||||
called_now = patch.get("called") or {}
|
||||
if note and called_now.get("controlled"):
|
||||
note = f"{note}; rev {called_now['rev']} is {called_now['controlled']} in M-Files"
|
||||
elif note and called_now.get("missing"):
|
||||
note = f"{note}; rev {called_now['rev']} is not in M-Files"
|
||||
if d.kind == D.ATTACH and fetched is not None:
|
||||
label = "Drawing attached from M-Files"
|
||||
note = "; ".join(x for x in (fetched.name or found.name or "", d.note) if x) or None
|
||||
@@ -533,4 +640,4 @@ def _cadence(fs: Any, now: datetime) -> int | None:
|
||||
|
||||
SPEC = JobSpec(name="drawing-lookup", run=run, cadence=_cadence, pausable=True)
|
||||
|
||||
__all__ = ["SPEC", "new_flags", "run", "save_lookup", "sharing", "target_for"]
|
||||
__all__ = ["SPEC", "called_rev", "new_flags", "run", "save_lookup", "save_revision", "sharing", "target_for"]
|
||||
|
||||
Reference in New Issue
Block a user