feat: gleichnamige Dokumentationen nach Bestätigung aktualisieren

This commit is contained in:
2026-07-23 16:20:14 +02:00
parent 83cac4447a
commit 8d6b6a46db
5 changed files with 74 additions and 17 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ class DocumentationConfig(PluginConfig):
name = "netbox_documentation" name = "netbox_documentation"
verbose_name = "NetBox Dokumentation" verbose_name = "NetBox Dokumentation"
description = "Wiki und Office-Dokumentation direkt in NetBox" description = "Wiki und Office-Dokumentation direkt in NetBox"
version = "0.7.11" version = "0.7.12"
author = "LKE" author = "LKE"
base_url = "documentation" base_url = "documentation"
min_version = "4.0.0" min_version = "4.0.0"
+3
View File
@@ -232,6 +232,9 @@ class BaseExcelSheetSelectionFormSet(forms.BaseFormSet):
indices = [item["index"] for item in selected] indices = [item["index"] for item in selected]
if len(indices) != len(set(indices)): if len(indices) != len(set(indices)):
raise forms.ValidationError("Die Arbeitsblattauswahl ist ungültig.") raise forms.ValidationError("Die Arbeitsblattauswahl ist ungültig.")
titles = [item["title"].strip().casefold() for item in selected]
if len(titles) != len(set(titles)):
raise forms.ValidationError("Ausgewählte Arbeitsblätter dürfen nicht denselben Dokumenttitel haben.")
ExcelSheetSelectionFormSet = forms.formset_factory( ExcelSheetSelectionFormSet = forms.formset_factory(
@@ -26,6 +26,16 @@
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% if overwrite_conflicts %}
<input type="hidden" name="confirm_overwrite" value="1">
<div class="alert alert-warning mx-0" role="alert">
<h3 class="alert-title">Bestehende Dokumentationen aktualisieren?</h3>
<p>Die folgenden Dokumentationen existieren bereits im Zielordner. Ihr Inhalt wird überschrieben; bestehende NetBox-Zuordnungen bleiben erhalten:</p>
<ul class="mb-0">
{% for document in overwrite_conflicts %}<li>{{ document.title }}</li>{% endfor %}
</ul>
</div>
{% endif %}
{{ formset.management_form }} {{ formset.management_form }}
{% if formset.non_form_errors %}<div class="alert alert-danger">{{ formset.non_form_errors }}</div>{% endif %} {% if formset.non_form_errors %}<div class="alert alert-danger">{{ formset.non_form_errors }}</div>{% endif %}
@@ -62,7 +72,10 @@
<div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always"> <div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always">
<div class="btn-list"> <div class="btn-list">
<button type="submit" name="action" value="cancel" class="btn btn-outline-secondary">Abbrechen</button> <button type="submit" name="action" value="cancel" class="btn btn-outline-secondary">Abbrechen</button>
<button type="submit" name="action" value="confirm" class="btn btn-primary"><i class="mdi mdi-file-import"></i> Ausgewählte Arbeitsblätter importieren</button> <button type="submit" name="action" value="confirm" class="btn {% if overwrite_conflicts %}btn-warning{% else %}btn-primary{% endif %}">
<i class="mdi mdi-file-import"></i>
{% if overwrite_conflicts %}Überschreiben und importieren{% else %}Ausgewählte Arbeitsblätter importieren{% endif %}
</button>
</div> </div>
</div> </div>
</form> </form>
+55 -14
View File
@@ -8,6 +8,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.http import FileResponse, Http404, JsonResponse from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.text import slugify from django.utils.text import slugify
@@ -228,7 +229,7 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
finally: finally:
preview.file.close() preview.file.close()
def render_preview(self, request, preview, sheets, formset): def render_preview(self, request, preview, sheets, formset, overwrite_conflicts=None):
items = [] items = []
for form, sheet in zip(formset.forms, sheets): for form, sheet in zip(formset.forms, sheets):
sample = sheet.markdown sample = sheet.markdown
@@ -251,6 +252,7 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
}) })
return render(request, self.template_name, { return render(request, self.template_name, {
"batch": preview, "formset": formset, "items": items, "batch": preview, "formset": formset, "items": items,
"overwrite_conflicts": overwrite_conflicts or [],
}) })
def get(self, request, token): def get(self, request, token):
@@ -296,6 +298,29 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
metadata = preview.sheet_metadata[data["index"]] metadata = preview.sheet_metadata[data["index"]]
source_index = metadata.get("source_index", data["index"]) source_index = metadata.get("source_index", data["index"])
selections[source_index] = data["title"].strip() selections[source_index] = data["title"].strip()
title_query = Q()
for title in selections.values():
title_query |= Q(title__iexact=title)
conflicts = list(Document.objects.filter(
title_query, category=preview.category,
).order_by("title"))
if conflicts:
allowed_ids = set(Document.objects.restrict(request.user, "change").filter(
pk__in=[document.pk for document in conflicts]
).values_list("pk", flat=True))
if any(document.pk not in allowed_ids for document in conflicts):
messages.error(request, "Mindestens eine gleichnamige Dokumentation darf nicht aktualisiert werden.")
return redirect("plugins:netbox_documentation:excel_import_preview", token=preview.pk)
if request.POST.get("confirm_overwrite") != "1":
try:
preview_sheets = self.read_sheets(preview, for_preview=True)
except (ImportFailure, Exception) as exc:
messages.error(request, f"Die Bestätigung konnte nicht angezeigt werden: {exc}")
return redirect("plugins:netbox_documentation:excel_import_preview", token=preview.pk)
return self.render_preview(
request, preview, preview_sheets, formset,
overwrite_conflicts=conflicts,
)
try: try:
selected_sheets = self.read_sheets(preview, selected_indexes=selections) selected_sheets = self.read_sheets(preview, selected_indexes=selections)
except (ImportFailure, Exception) as exc: except (ImportFailure, Exception) as exc:
@@ -305,7 +330,8 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
result = self.create_documents(preview, sheets_by_index, selections) result = self.create_documents(preview, sheets_by_index, selections)
self.delete_preview(preview) self.delete_preview(preview)
messages.success(request, ( messages.success(request, (
f"{result['documents']} Arbeitsblätter als Dokumentationen in „{result['category']}“ importiert; " f"{result['documents']} Arbeitsblätter verarbeitet "
f"({result['created']} neu, {result['updated']} aktualisiert) in „{result['category']}“; "
f"{result['images']} Bilder übernommen." f"{result['images']} Bilder übernommen."
)) ))
return redirect("plugins:netbox_documentation:document_list") return redirect("plugins:netbox_documentation:document_list")
@@ -314,19 +340,31 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
@transaction.atomic @transaction.atomic
def create_documents(preview, sheets, selections): def create_documents(preview, sheets, selections):
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
created_documents, image_count = [], 0 created_documents, image_count, created_count, updated_count = [], 0, 0, 0
for index, title in selections.items(): for index, title in selections.items():
sheet = sheets[index] sheet = sheets[index]
base_slug = slugify(title)[:180] or "arbeitsblatt" document = Document.objects.select_for_update().filter(
document_slug, counter = base_slug, 2 category=preview.category, title__iexact=title,
while Document.objects.filter(slug=document_slug).exists(): ).order_by("pk").first()
document_slug = f"{base_slug[:190-len(str(counter))]}-{counter}" if document:
counter += 1 document.title = title[:200]
document = Document.objects.create( document.body = sheet.markdown
title=title[:200], slug=document_slug, body=sheet.markdown, document.body_format = sheet.body_format
body_format=sheet.body_format, category=preview.category, document.summary = f"Importiert aus {preview.original_name}"
summary=f"Importiert aus {preview.original_name}", document.save(update_fields=("title", "body", "body_format", "summary", "last_updated"))
) updated_count += 1
else:
base_slug = slugify(title)[:180] or "arbeitsblatt"
document_slug, counter = base_slug, 2
while Document.objects.filter(slug=document_slug).exists():
document_slug = f"{base_slug[:190-len(str(counter))]}-{counter}"
counter += 1
document = Document.objects.create(
title=title[:200], slug=document_slug, body=sheet.markdown,
body_format=sheet.body_format, category=preview.category,
summary=f"Importiert aus {preview.original_name}",
)
created_count += 1
if sheet.images: if sheet.images:
image_lines = [] image_lines = []
for image in sheet.images: for image in sheet.images:
@@ -362,7 +400,10 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
attachment.save() attachment.save()
finally: finally:
preview.file.close() preview.file.close()
return {"documents": len(created_documents), "images": image_count, "category": preview.category} return {
"documents": len(created_documents), "created": created_count,
"updated": updated_count, "images": image_count, "category": preview.category,
}
@staticmethod @staticmethod
def delete_preview(preview): def delete_preview(preview):
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netbox-documentation" name = "netbox-documentation"
version = "0.7.11" version = "0.7.12"
description = "Integrated Markdown wiki and office document importer for NetBox" description = "Integrated Markdown wiki and office document importer for NetBox"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"