fix: Import mit fehlendem Mandanten laut abbrechen statt still verwerfen

Beim ZIP-Import wurden Tenant/TenantGroup bisher per Slug per
.filter().first() aufgelöst. Existierte der referenzierte Mandant auf
der Zielinstanz nicht, wurde das Feld stillschweigend auf None gesetzt.
Das führte bei Instanzen mit Mandanten-Pflichtfeld-Validierung zu einer
generischen ValidationError beim document.save(), ohne Hinweis darauf,
welches Dokument oder welcher Mandant betroffen war.

_resolve_tenant()/_resolve_tenant_group() werfen nun eine ArchiveFailure
mit Dokument-/Ordnertitel und fehlendem Slug, sobald ein referenzierter
Mandant nicht gefunden wird.
This commit is contained in:
2026-09-02 10:41:52 +02:00
parent 117ffac210
commit 1e670b1148
+48 -4
View File
@@ -123,6 +123,48 @@ def _read_json(archive, name):
raise ArchiveFailure("Das Archiv enthält kein gültiges manifest.json.") from exc raise ArchiveFailure("Das Archiv enthält kein gültiges manifest.json.") from exc
def _resolve_tenant_group(slug, context):
"""Look up a tenant group by slug for an imported record.
Returns None if no slug was recorded (i.e. the object genuinely had no
tenant group). Raises ArchiveFailure if a slug *was* recorded but no
matching TenantGroup exists on this instance, instead of silently
dropping the assignment - a missing tenant group later tripping over
an unrelated validation rule further downstream is far more confusing
than failing here with a precise message.
"""
if not slug:
return None
group = TenantGroup.objects.filter(slug=slug).first()
if group is None:
raise ArchiveFailure(
f"{context}: Die Mandantengruppe „{slug}“ existiert auf dieser Instanz nicht. "
"Bitte die Mandantengruppe vorher anlegen oder den Import mit einem angepassten "
"manifest.json wiederholen."
)
return group
def _resolve_tenant(slug, context):
"""Look up a tenant by slug for an imported record.
Same reasoning as _resolve_tenant_group(): fail loudly and specifically
instead of leaving the field empty and letting a generic "a tenant is
required" validation error surface later with no indication of which
tenant or which record was actually the problem.
"""
if not slug:
return None
tenant = Tenant.objects.filter(slug=slug).first()
if tenant is None:
raise ArchiveFailure(
f"{context}: Der Mandant „{slug}“ existiert auf dieser Instanz nicht. "
"Bitte den Mandanten vorher anlegen oder den Import mit einem angepassten "
"manifest.json wiederholen."
)
return tenant
def _category_from_path(names, tenancy_path=None): def _category_from_path(names, tenancy_path=None):
parent = None parent = None
tenancy_path = tenancy_path or [] tenancy_path = tenancy_path or []
@@ -136,8 +178,9 @@ def _category_from_path(names, tenancy_path=None):
category = DocumentCategory.objects.create(name=name, slug=slug, parent=parent) category = DocumentCategory.objects.create(name=name, slug=slug, parent=parent)
tenancy = tenancy_path[index] if index < len(tenancy_path) and isinstance(tenancy_path[index], dict) else {} tenancy = tenancy_path[index] if index < len(tenancy_path) and isinstance(tenancy_path[index], dict) else {}
if tenancy: if tenancy:
category.tenant_group = TenantGroup.objects.filter(slug=tenancy.get("tenant_group")).first() context = f"Ordner „{name}“"
category.tenant = Tenant.objects.filter(slug=tenancy.get("tenant")).first() category.tenant_group = _resolve_tenant_group(tenancy.get("tenant_group"), context)
category.tenant = _resolve_tenant(tenancy.get("tenant"), context)
category.save(update_fields=("tenant_group", "tenant", "last_updated")) category.save(update_fields=("tenant_group", "tenant", "last_updated"))
parent = category parent = category
return parent return parent
@@ -191,8 +234,9 @@ def import_archive(upload, update_existing=False):
document.body_format = record.get("body_format") if record.get("body_format") in {"html", "markdown"} else "html" document.body_format = record.get("body_format") if record.get("body_format") in {"html", "markdown"} else "html"
document.is_published = bool(record.get("is_published", True)) document.is_published = bool(record.get("is_published", True))
document.category = category document.category = category
document.tenant_group = TenantGroup.objects.filter(slug=record.get("tenant_group")).first() context = f"Dokument „{document.title}“"
document.tenant = Tenant.objects.filter(slug=record.get("tenant")).first() document.tenant_group = _resolve_tenant_group(record.get("tenant_group"), context)
document.tenant = _resolve_tenant(record.get("tenant"), context)
document.save() document.save()
for assignment in record.get("assignments") or []: for assignment in record.get("assignments") or []:
label = f"{assignment.get('app_label')}.{assignment.get('model')}" label = f"{assignment.get('app_label')}.{assignment.get('model')}"