feat: sync saved views and metadata entities to replicas
All checks were successful
Deploy / deploy (push) Successful in 34s
All checks were successful
Deploy / deploy (push) Successful in 34s
- Sync Tags, Correspondents, Document Types, Custom Fields via _ensure_schema_parity (already existed) - Add saved views sync: create/update on replica with filter rule ID translation - _FILTER_RULE_ENTITY_MAP translates entity-referencing rule types (correspondents, document types, tags, custom fields) to replica IDs - Saved views sync is best-effort (non-fatal on failure) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,77 @@ def _translate_metadata(meta: dict, maps: dict) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
# Maps paperless-ngx FilterRuleType integers that reference entity IDs.
|
||||
# Based on paperless-ngx FilterRuleType enum (stable since v1.14).
|
||||
_FILTER_RULE_ENTITY_MAP: dict[int, str] = {
|
||||
3: "correspondents", # CORRESPONDENT
|
||||
4: "document_types", # DOCUMENT_TYPE
|
||||
6: "tags", # HAS_TAG
|
||||
17: "tags", # DOES_NOT_HAVE_TAG
|
||||
21: "correspondents", # DOES_NOT_HAVE_CORRESPONDENT
|
||||
22: "document_types", # DOES_NOT_HAVE_DOCUMENT_TYPE
|
||||
25: "custom_fields", # HAS_CUSTOM_FIELD
|
||||
26: "custom_fields", # DOES_NOT_HAVE_CUSTOM_FIELD
|
||||
}
|
||||
|
||||
|
||||
async def _sync_saved_views(
|
||||
master: PaperlessClient,
|
||||
replica: PaperlessClient,
|
||||
maps: dict,
|
||||
replica_obj: Replica,
|
||||
run_id: int,
|
||||
session: Session,
|
||||
) -> None:
|
||||
"""Sync saved views from master to replica, translating entity IDs in filter rules."""
|
||||
master_views = await master.get_saved_views()
|
||||
replica_views = {v["name"]: v for v in await replica.get_saved_views()}
|
||||
|
||||
created = updated = 0
|
||||
for view in master_views:
|
||||
translated_rules = []
|
||||
for rule in view.get("filter_rules", []):
|
||||
rule_type = rule.get("rule_type")
|
||||
value = rule.get("value")
|
||||
entity_key = _FILTER_RULE_ENTITY_MAP.get(rule_type)
|
||||
if entity_key and value is not None:
|
||||
try:
|
||||
replica_id = maps[entity_key].get(int(value))
|
||||
if replica_id is None:
|
||||
continue # entity not synced yet — skip rule
|
||||
value = str(replica_id)
|
||||
except (ValueError, TypeError):
|
||||
pass # non-integer value, pass through unchanged
|
||||
translated_rules.append({"rule_type": rule_type, "value": value})
|
||||
|
||||
view_data = {
|
||||
"name": view["name"],
|
||||
"show_on_dashboard": view.get("show_on_dashboard", False),
|
||||
"show_in_sidebar": view.get("show_in_sidebar", False),
|
||||
"sort_field": view.get("sort_field", ""),
|
||||
"sort_reverse": view.get("sort_reverse", False),
|
||||
"filter_rules": translated_rules,
|
||||
}
|
||||
|
||||
existing = replica_views.get(view["name"])
|
||||
if existing:
|
||||
await replica.update_saved_view(existing["id"], view_data)
|
||||
updated += 1
|
||||
else:
|
||||
await replica.create_saved_view(view_data)
|
||||
created += 1
|
||||
|
||||
if created or updated:
|
||||
emit_log(
|
||||
"info",
|
||||
f"Saved views synced: {created} created, {updated} updated",
|
||||
replica=replica_obj.name,
|
||||
replica_id=replica_obj.id,
|
||||
run_id=run_id,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
def _sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
@@ -262,6 +333,19 @@ async def _sync_replica(
|
||||
)
|
||||
raise
|
||||
|
||||
# Step 5a2: sync saved views (best-effort, non-fatal)
|
||||
try:
|
||||
await _sync_saved_views(master, replica, maps, replica_obj, run_id, session)
|
||||
except Exception as e:
|
||||
emit_log(
|
||||
"warning",
|
||||
f"Saved views sync failed (non-fatal): {e}",
|
||||
replica=replica_obj.name,
|
||||
replica_id=replica_obj.id,
|
||||
run_id=run_id,
|
||||
session=session,
|
||||
)
|
||||
|
||||
# Step 5b: resolve pending tasks
|
||||
_progress.phase = f"resolving tasks — {replica_obj.name}"
|
||||
await _resolve_pending_tasks(
|
||||
|
||||
@@ -195,6 +195,17 @@ class PaperlessClient:
|
||||
)
|
||||
return r.json()
|
||||
|
||||
async def get_saved_views(self) -> list[dict]:
|
||||
return await self._get_all("/api/saved_views/")
|
||||
|
||||
async def create_saved_view(self, view_data: dict) -> dict:
|
||||
r = await self._request("POST", "/api/saved_views/", json=view_data)
|
||||
return r.json()
|
||||
|
||||
async def update_saved_view(self, view_id: int, view_data: dict) -> dict:
|
||||
r = await self._request("PUT", f"/api/saved_views/{view_id}/", json=view_data)
|
||||
return r.json()
|
||||
|
||||
async def test_connection(self) -> dict:
|
||||
"""Returns {ok, error, latency_ms, doc_count}."""
|
||||
t0 = time.monotonic()
|
||||
|
||||
Reference in New Issue
Block a user