diff --git a/netbox_documentation/__init__.py b/netbox_documentation/__init__.py
index 2dbee2b..d69c23f 100644
--- a/netbox_documentation/__init__.py
+++ b/netbox_documentation/__init__.py
@@ -5,7 +5,7 @@ class DocumentationConfig(PluginConfig):
name = "netbox_documentation"
verbose_name = "NetBox Dokumentation"
description = "Wiki und Office-Dokumentation direkt in NetBox"
- version = "0.9.3"
+ version = "0.10.0"
author = "LKE"
base_url = "documentation"
min_version = "4.0.0"
diff --git a/netbox_documentation/models.py b/netbox_documentation/models.py
index 4d8da1a..f5d9ac4 100644
--- a/netbox_documentation/models.py
+++ b/netbox_documentation/models.py
@@ -103,6 +103,18 @@ class DocumentCategory(NetBoxModel):
def get_absolute_url(self):
return reverse("plugins:netbox_documentation:documentcategory", args=[self.pk])
+ @property
+ def ancestors(self):
+ """Ancestor folders from the root down to (excluding) this one, for breadcrumbs."""
+ result = []
+ parent = self.parent
+ seen = {self.pk}
+ while parent and parent.pk not in seen:
+ result.append(parent)
+ seen.add(parent.pk)
+ parent = parent.parent
+ return list(reversed(result))
+
def clean(self):
super().clean()
from django.core.exceptions import ValidationError
diff --git a/netbox_documentation/navigation.py b/netbox_documentation/navigation.py
index 575d071..2d32e61 100644
--- a/netbox_documentation/navigation.py
+++ b/netbox_documentation/navigation.py
@@ -11,7 +11,7 @@ menu = PluginMenu(
PluginMenuItem(link="plugins:netbox_documentation:documentassignment_list", link_text="Zuordnungen", buttons=(
PluginMenuButton(link="plugins:netbox_documentation:documentassignment_add", title="Zuordnen", icon_class="mdi mdi-link-plus", color=ButtonColorChoices.GREEN),
)),
- PluginMenuItem(link="plugins:netbox_documentation:documentcategory_list", link_text="Ordner & Kategorien", buttons=(
+ PluginMenuItem(link="plugins:netbox_documentation:documentcategory_tree", link_text="Ordner & Kategorien", buttons=(
PluginMenuButton(link="plugins:netbox_documentation:documentcategory_add", title="Ordner erstellen", icon_class="mdi mdi-folder-plus", color=ButtonColorChoices.GREEN),
)),
PluginMenuItem(link="plugins:netbox_documentation:document_archive", link_text="ZIP Export & Import"),
diff --git a/netbox_documentation/templates/netbox_documentation/documentcategory.html b/netbox_documentation/templates/netbox_documentation/documentcategory.html
index 3c10a8f..202600e 100644
--- a/netbox_documentation/templates/netbox_documentation/documentcategory.html
+++ b/netbox_documentation/templates/netbox_documentation/documentcategory.html
@@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% block content %}
+
{% if object.tenant_group or object.tenant %}
diff --git a/netbox_documentation/templates/netbox_documentation/documentcategory_tree.html b/netbox_documentation/templates/netbox_documentation/documentcategory_tree.html
new file mode 100644
index 0000000..7e34619
--- /dev/null
+++ b/netbox_documentation/templates/netbox_documentation/documentcategory_tree.html
@@ -0,0 +1,66 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}Ordner & Kategorien{% endblock %}
+
+{% block content %}
+
+
+
+ {% if tree %}
+
+ {% for node in tree %}
+ {% include 'netbox_documentation/inc/category_tree_node.html' with node=node %}
+ {% endfor %}
+
+ {% else %}
+
Noch keine Ordner vorhanden.
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/netbox_documentation/templates/netbox_documentation/inc/category_tree_node.html b/netbox_documentation/templates/netbox_documentation/inc/category_tree_node.html
new file mode 100644
index 0000000..dc4fab5
--- /dev/null
+++ b/netbox_documentation/templates/netbox_documentation/inc/category_tree_node.html
@@ -0,0 +1,30 @@
+
+ {% if node.children %}
+
+
+
+ {{ node.object.name }}
+ {{ node.object.document_total }}
+ {% if node.object.tenant %} {{ node.object.tenant }}{% endif %}
+ {% if perms.netbox_documentation.change_documentcategory %}
+
+ {% endif %}
+
+
+ {% for child in node.children %}
+ {% include 'netbox_documentation/inc/category_tree_node.html' with node=child %}
+ {% endfor %}
+
+
+ {% else %}
+
+
+
{{ node.object.name }}
+
{{ node.object.document_total }}
+ {% if node.object.tenant %}
{{ node.object.tenant }}{% endif %}
+ {% if perms.netbox_documentation.change_documentcategory %}
+
+ {% endif %}
+
+ {% endif %}
+
diff --git a/netbox_documentation/urls.py b/netbox_documentation/urls.py
index 4bf154e..6033891 100644
--- a/netbox_documentation/urls.py
+++ b/netbox_documentation/urls.py
@@ -24,6 +24,7 @@ urlpatterns = (
path("assignments/
/edit/", views.AssignmentEditView.as_view(), name="documentassignment_edit"),
path("assignments//delete/", views.AssignmentDeleteView.as_view(), name="documentassignment_delete"),
path("folders/", views.DocumentCategoryListView.as_view(), name="documentcategory_list"),
+ path("folders/tree/", views.DocumentCategoryTreeView.as_view(), name="documentcategory_tree"),
path("folders/add/", views.DocumentCategoryEditView.as_view(), name="documentcategory_add"),
path("folders/import/", views.DocumentImportView.as_view(), name="documentcategory_bulk_import"),
path("folders/delete/", views.DocumentCategoryBulkDeleteView.as_view(), name="documentcategory_bulk_delete"),
diff --git a/netbox_documentation/views.py b/netbox_documentation/views.py
index fa6b5cf..c7d0855 100644
--- a/netbox_documentation/views.py
+++ b/netbox_documentation/views.py
@@ -8,7 +8,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db import transaction
-from django.db.models import Q
+from django.db.models import Count, Q
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.text import slugify
@@ -91,8 +91,32 @@ class DocumentCategoryListView(generic.ObjectListView):
actions = (AddObject, BulkImport, BulkDelete)
+class DocumentCategoryTreeView(PermissionRequiredMixin, View):
+ """Nextcloud/Bookstack-style nested folder browser for document categories."""
+
+ permission_required = "netbox_documentation.view_documentcategory"
+ template_name = "netbox_documentation/documentcategory_tree.html"
+
+ def get(self, request):
+ categories = list(
+ DocumentCategory.objects.restrict(request.user, "view")
+ .select_related("tenant_group", "tenant")
+ .annotate(document_total=Count("documents", distinct=True))
+ .order_by("name")
+ )
+ nodes = {category.pk: {"object": category, "children": []} for category in categories}
+ roots = []
+ for category in categories:
+ node = nodes[category.pk]
+ parent_node = nodes.get(category.parent_id)
+ (parent_node["children"] if parent_node else roots).append(node)
+ return render(request, self.template_name, {"tree": roots, "category_count": len(categories)})
+
+
class DocumentCategoryView(generic.ObjectView):
- queryset = DocumentCategory.objects.select_related("tenant_group", "tenant").prefetch_related("children", "documents")
+ queryset = DocumentCategory.objects.select_related("parent", "tenant_group", "tenant").prefetch_related(
+ "parent__parent", "children", "documents",
+ )
class DocumentCategoryEditView(generic.ObjectEditView):
diff --git a/pyproject.toml b/pyproject.toml
index 477b922..a4b03e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "netbox-documentation"
-version = "0.9.3"
+version = "0.10.0"
description = "Integrated Markdown wiki and office document importer for NetBox"
readme = "README.md"
requires-python = ">=3.10"