feat: ZIP-Export auf durchsuchbare Ordnerauswahl umstellen
This commit is contained in:
@@ -21,7 +21,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
|
|||||||
|
|
||||||
## Kompatibilität
|
## Kompatibilität
|
||||||
|
|
||||||
Die Version `0.4.0` zielt auf NetBox 4.x (mindestens 4.0). Vor einem produktiven Rollout sollte das Plugin gegen die konkret eingesetzte NetBox-Minor-Version in einer Testinstanz geprüft werden.
|
Die Version `0.4.1` zielt auf NetBox 4.x (mindestens 4.0). Vor einem produktiven Rollout sollte das Plugin gegen die konkret eingesetzte NetBox-Minor-Version in einer Testinstanz geprüft werden.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ Nach Aktivierung stehen die üblichen NetBox-Plugin-Endpunkte bereit:
|
|||||||
|
|
||||||
## ZIP-Archiv
|
## ZIP-Archiv
|
||||||
|
|
||||||
Unter **Dokumentation → ZIP Export & Import** können mehrere Dokumentationen ausgewählt und gemeinsam heruntergeladen werden. Das Archiv enthält:
|
Unter **Dokumentation → ZIP Export & Import** können mehrere Ordner über ein Suchfeld hinzugefügt und gemeinsam heruntergeladen werden. Optional werden alle Unterordner rekursiv einbezogen. Dokumentationen, die durch überlappende Ordnerauswahlen mehrfach gefunden werden, landen nur einmal im Archiv. Das Archiv enthält:
|
||||||
|
|
||||||
- Dokumentinhalt und Metadaten
|
- Dokumentinhalt und Metadaten
|
||||||
- vollständige Ordnerpfade
|
- vollständige Ordnerpfade
|
||||||
|
|||||||
@@ -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.4.0"
|
version = "0.4.1"
|
||||||
author = "LKE"
|
author = "LKE"
|
||||||
base_url = "documentation"
|
base_url = "documentation"
|
||||||
min_version = "4.0.0"
|
min_version = "4.0.0"
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ class ArchiveFailure(ValueError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def documents_for_categories(categories, include_subfolders=True, user=None):
|
||||||
|
"""Return distinct permitted documents contained in the selected category trees."""
|
||||||
|
category_ids = set(categories.values_list("pk", flat=True))
|
||||||
|
frontier = set(category_ids)
|
||||||
|
while include_subfolders and frontier:
|
||||||
|
children = set(DocumentCategory.objects.filter(parent_id__in=frontier).values_list("pk", flat=True))
|
||||||
|
frontier = children - category_ids
|
||||||
|
category_ids.update(children)
|
||||||
|
documents = Document.objects.filter(category_id__in=category_ids).distinct().order_by("title")
|
||||||
|
if user is not None:
|
||||||
|
documents = documents.restrict(user, "view")
|
||||||
|
return documents
|
||||||
|
|
||||||
|
|
||||||
def _category_path(category):
|
def _category_path(category):
|
||||||
path, seen = [], set()
|
path, seen = [], set()
|
||||||
while category and category.pk not in seen:
|
while category and category.pk not in seen:
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ from django.conf import settings
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from utilities.forms import get_field_value
|
from utilities.forms import get_field_value
|
||||||
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelChoiceField, SlugField
|
from utilities.forms.fields import (
|
||||||
|
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||||
|
)
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import HTMXSelect
|
from utilities.forms.widgets import HTMXSelect
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
@@ -119,17 +121,22 @@ class ImportForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class ArchiveExportForm(forms.Form):
|
class ArchiveExportForm(forms.Form):
|
||||||
documents = forms.ModelMultipleChoiceField(
|
categories = DynamicModelMultipleChoiceField(
|
||||||
queryset=Document.objects.none(), widget=forms.CheckboxSelectMultiple,
|
queryset=DocumentCategory.objects.all(), selector=True,
|
||||||
label="Dokumentationen",
|
label="Ordner und Kategorien",
|
||||||
|
help_text="Einen oder mehrere Ordner suchen und hinzufügen.",
|
||||||
|
)
|
||||||
|
include_subfolders = forms.BooleanField(
|
||||||
|
required=False, initial=True, label="Unterordner einbeziehen",
|
||||||
|
help_text="Exportiert Dokumentationen aus allen untergeordneten Ordnern.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, user=None, **kwargs):
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
queryset = Document.objects.all().order_by("title")
|
queryset = DocumentCategory.objects.all().order_by("name")
|
||||||
if user is not None:
|
if user is not None:
|
||||||
queryset = queryset.restrict(user, "view")
|
queryset = queryset.restrict(user, "view")
|
||||||
self.fields["documents"].queryset = queryset
|
self.fields["categories"].queryset = queryset
|
||||||
|
|
||||||
|
|
||||||
class ArchiveImportForm(forms.Form):
|
class ArchiveImportForm(forms.Form):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<h5 class="card-header"><i class="mdi mdi-folder-zip"></i> Dokumentationen exportieren</h5>
|
<h5 class="card-header"><i class="mdi mdi-folder-zip"></i> Dokumentationen exportieren</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>Ausgewählte Dokumentationen gemeinsam mit Ordnern, Objektzuordnungen und Anhängen herunterladen.</p>
|
<p>Mehrere Ordner suchen und hinzufügen. Alle enthaltenen Dokumentationen werden gemeinsam mit Objektzuordnungen und Anhängen heruntergeladen.</p>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="export">
|
<input type="hidden" name="action" value="export">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from .forms import (
|
|||||||
ArchiveExportForm, ArchiveImportForm, AssignmentForm, DocumentCategoryForm,
|
ArchiveExportForm, ArchiveImportForm, AssignmentForm, DocumentCategoryForm,
|
||||||
DocumentForm, ImportForm,
|
DocumentForm, ImportForm,
|
||||||
)
|
)
|
||||||
from .archive import ArchiveFailure, export_documents, import_archive
|
from .archive import ArchiveFailure, documents_for_categories, export_documents, import_archive
|
||||||
from .importers import ImportFailure, import_document
|
from .importers import ImportFailure, import_document
|
||||||
from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory
|
from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory
|
||||||
from .tables import DocumentTable, AssignmentTable, DocumentCategoryTable
|
from .tables import DocumentTable, AssignmentTable, DocumentCategoryTable
|
||||||
@@ -189,7 +189,13 @@ class DocumentArchiveView(View):
|
|||||||
form = ArchiveExportForm(request.POST, user=request.user)
|
form = ArchiveExportForm(request.POST, user=request.user)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return self._render(request, export_form=form)
|
return self._render(request, export_form=form)
|
||||||
stream = export_documents(form.cleaned_data["documents"])
|
documents = documents_for_categories(
|
||||||
|
form.cleaned_data["categories"], form.cleaned_data["include_subfolders"], request.user
|
||||||
|
)
|
||||||
|
if not documents.exists():
|
||||||
|
form.add_error("categories", "Die ausgewählten Ordner enthalten keine exportierbaren Dokumentationen.")
|
||||||
|
return self._render(request, export_form=form)
|
||||||
|
stream = export_documents(documents)
|
||||||
response = FileResponse(stream, content_type="application/zip")
|
response = FileResponse(stream, content_type="application/zip")
|
||||||
response["Content-Disposition"] = 'attachment; filename="netbox-documentation-export.zip"'
|
response["Content-Disposition"] = 'attachment; filename="netbox-documentation-export.zip"'
|
||||||
return response
|
return response
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-documentation"
|
name = "netbox-documentation"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
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