feat: sync saved views and metadata entities to replicas
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:
2026-03-30 08:43:42 +02:00
parent 0ac43ac896
commit 6c2a905afe
2 changed files with 95 additions and 0 deletions

View File

@@ -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(

View File

@@ -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()