feat: gleichnamige Dokumentationen nach Bestätigung aktualisieren
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,9 +340,20 @@ 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]
|
||||||
|
document = Document.objects.select_for_update().filter(
|
||||||
|
category=preview.category, title__iexact=title,
|
||||||
|
).order_by("pk").first()
|
||||||
|
if document:
|
||||||
|
document.title = title[:200]
|
||||||
|
document.body = sheet.markdown
|
||||||
|
document.body_format = sheet.body_format
|
||||||
|
document.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"
|
base_slug = slugify(title)[:180] or "arbeitsblatt"
|
||||||
document_slug, counter = base_slug, 2
|
document_slug, counter = base_slug, 2
|
||||||
while Document.objects.filter(slug=document_slug).exists():
|
while Document.objects.filter(slug=document_slug).exists():
|
||||||
@@ -327,6 +364,7 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
|
|||||||
body_format=sheet.body_format, category=preview.category,
|
body_format=sheet.body_format, category=preview.category,
|
||||||
summary=f"Importiert aus {preview.original_name}",
|
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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user