feat: Mandantenzuordnung für Dokumente und Ordner ergänzen

This commit is contained in:
2026-07-29 10:17:18 +02:00
parent d20baa3ea3
commit 791b40b3c4
13 changed files with 161 additions and 23 deletions
+3 -2
View File
@@ -7,7 +7,8 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
- Dokumentationen direkt in NetBox schreiben und als sicher bereinigten Rich Text anzeigen - Dokumentationen direkt in NetBox schreiben und als sicher bereinigten Rich Text anzeigen
- Word-ähnlicher WYSIWYG-Editor mit Schriftarten, Schriftgrößen, Farben, Ausrichtung und Tabellen - Word-ähnlicher WYSIWYG-Editor mit Schriftarten, Schriftgrößen, Farben, Ausrichtung und Tabellen
- Hierarchische Ordner und Unterordner für eine BookStack-ähnliche Struktur - Hierarchische Ordner und Unterordner für eine BookStack-ähnliche Struktur
- Breite Editoransicht und Fokusmodus innerhalb des NetBox-Layouts - Breite Editoransicht und TinyMCE-Vollbildmodus
- Dokumentationen und Ordner optional Mandanten und Mandantengruppen zuweisen
- Bilder direkt vom eigenen Gerät in die Dokumentation hochladen - Bilder direkt vom eigenen Gerät in die Dokumentation hochladen
- Formatierte Word-Inhalte inklusive Tabellen und unterstützten Zwischenablage-Bildern einfügen - Formatierte Word-Inhalte inklusive Tabellen und unterstützten Zwischenablage-Bildern einfügen
- Mehrere Dokumentationen mit Ordnern, Zuordnungen und Anhängen als ZIP exportieren und wieder importieren - Mehrere Dokumentationen mit Ordnern, Zuordnungen und Anhängen als ZIP exportieren und wieder importieren
@@ -28,7 +29,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
## Kompatibilität ## Kompatibilität
Die Version `0.7.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.9.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.
## Installation ## Installation
+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.14" version = "0.9.0"
author = "LKE" author = "LKE"
base_url = "documentation" base_url = "documentation"
min_version = "4.0.0" min_version = "4.0.0"
+2 -2
View File
@@ -8,7 +8,7 @@ class DocumentSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Document model = Document
fields = ("id", "url", "display", "title", "slug", "category", "summary", "body", "body_format", "is_published", "tags", "created", "last_updated") fields = ("id", "url", "display", "title", "slug", "category", "tenant_group", "tenant", "summary", "body", "body_format", "is_published", "tags", "created", "last_updated")
class DocumentAssignmentSerializer(NetBoxModelSerializer): class DocumentAssignmentSerializer(NetBoxModelSerializer):
@@ -24,7 +24,7 @@ class DocumentCategorySerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = DocumentCategory model = DocumentCategory
fields = ("id", "url", "display", "name", "slug", "parent", "description", "tags", "created", "last_updated") fields = ("id", "url", "display", "name", "slug", "parent", "tenant_group", "tenant", "description", "tags", "created", "last_updated")
class DocumentAttachmentSerializer(NetBoxModelSerializer): class DocumentAttachmentSerializer(NetBoxModelSerializer):
+32 -4
View File
@@ -7,6 +7,7 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import transaction from django.db import transaction
from django.utils.text import slugify from django.utils.text import slugify
from tenancy.models import Tenant, TenantGroup
from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory
@@ -43,11 +44,25 @@ def _category_path(category):
return list(reversed(path)) return list(reversed(path))
def _category_tenancy_path(category):
path, seen = [], set()
while category and category.pk not in seen:
path.append({
"tenant": category.tenant.slug if category.tenant else None,
"tenant_group": category.tenant_group.slug if category.tenant_group else None,
})
seen.add(category.pk)
category = category.parent
return list(reversed(path))
def export_documents(documents): def export_documents(documents):
stream = SpooledTemporaryFile(max_size=10 * 1024 * 1024, mode="w+b") stream = SpooledTemporaryFile(max_size=10 * 1024 * 1024, mode="w+b")
manifest = {"format": ARCHIVE_FORMAT, "version": ARCHIVE_VERSION, "documents": []} manifest = {"format": ARCHIVE_FORMAT, "version": ARCHIVE_VERSION, "documents": []}
with ZipFile(stream, "w", compression=ZIP_DEFLATED, compresslevel=6) as archive: with ZipFile(stream, "w", compression=ZIP_DEFLATED, compresslevel=6) as archive:
for document in documents.prefetch_related("assignments__assigned_object_type", "attachments").select_related("category"): for document in documents.prefetch_related("assignments__assigned_object_type", "attachments").select_related(
"category", "tenant", "tenant_group", "category__tenant", "category__tenant_group",
):
root = f"documents/{document.pk}" root = f"documents/{document.pk}"
extension = "html" if document.body_format == "html" else "md" extension = "html" if document.body_format == "html" else "md"
body_path = f"{root}/content.{extension}" body_path = f"{root}/content.{extension}"
@@ -76,6 +91,9 @@ def export_documents(documents):
"title": document.title, "slug": document.slug, "summary": document.summary, "title": document.title, "slug": document.slug, "summary": document.summary,
"body_format": document.body_format, "is_published": document.is_published, "body_format": document.body_format, "is_published": document.is_published,
"category_path": _category_path(document.category), "body_path": body_path, "category_path": _category_path(document.category), "body_path": body_path,
"category_tenancy": _category_tenancy_path(document.category),
"tenant": document.tenant.slug if document.tenant else None,
"tenant_group": document.tenant_group.slug if document.tenant_group else None,
"assignments": assignments, "attachments": attachments, "assignments": assignments, "attachments": attachments,
}) })
archive.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8")) archive.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8"))
@@ -105,9 +123,10 @@ 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 _category_from_path(names): def _category_from_path(names, tenancy_path=None):
parent = None parent = None
for name in names: tenancy_path = tenancy_path or []
for index, name in enumerate(names):
name = str(name).strip()[:100] name = str(name).strip()[:100]
if not name: if not name:
continue continue
@@ -115,6 +134,11 @@ def _category_from_path(names):
category = DocumentCategory.objects.filter(parent=parent, slug=slug).first() category = DocumentCategory.objects.filter(parent=parent, slug=slug).first()
if not category: if not category:
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 {}
if tenancy:
category.tenant_group = TenantGroup.objects.filter(slug=tenancy.get("tenant_group")).first()
category.tenant = Tenant.objects.filter(slug=tenancy.get("tenant")).first()
category.save(update_fields=("tenant_group", "tenant", "last_updated"))
parent = category parent = category
return parent return parent
@@ -153,7 +177,9 @@ def import_archive(upload, update_existing=False):
raise ArchiveFailure("Ein Dokumentinhalt fehlt oder ist nicht UTF-8-kodiert.") from exc raise ArchiveFailure("Ein Dokumentinhalt fehlt oder ist nicht UTF-8-kodiert.") from exc
source_slug = str(record.get("slug") or record["title"]) source_slug = str(record.get("slug") or record["title"])
document = Document.objects.filter(slug=source_slug).first() if update_existing else None document = Document.objects.filter(slug=source_slug).first() if update_existing else None
category = _category_from_path(record.get("category_path") or []) category = _category_from_path(
record.get("category_path") or [], record.get("category_tenancy") or [],
)
if document: if document:
updated += 1 updated += 1
else: else:
@@ -165,6 +191,8 @@ 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()
document.tenant = Tenant.objects.filter(slug=record.get("tenant")).first()
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')}"
+2 -2
View File
@@ -5,7 +5,7 @@ from .models import Document, DocumentAssignment, DocumentCategory
class DocumentFilterSet(NetBoxModelFilterSet): class DocumentFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = Document model = Document
fields = ("id", "title", "slug", "category_id", "is_published") fields = ("id", "title", "slug", "category_id", "tenant_group_id", "tenant_id", "is_published")
def search(self, queryset, name, value): def search(self, queryset, name, value):
from django.db.models import Q from django.db.models import Q
@@ -21,7 +21,7 @@ class AssignmentFilterSet(NetBoxModelFilterSet):
class DocumentCategoryFilterSet(NetBoxModelFilterSet): class DocumentCategoryFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = DocumentCategory model = DocumentCategory
fields = ("id", "name", "slug", "parent_id") fields = ("id", "name", "slug", "parent_id", "tenant_group_id", "tenant_id")
def search(self, queryset, name, value): def search(self, queryset, name, value):
return queryset.filter(name__icontains=value) return queryset.filter(name__icontains=value)
+31 -4
View File
@@ -12,6 +12,7 @@ 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
from dcim.models import Device from dcim.models import Device
from tenancy.models import Tenant, TenantGroup
from .embedded_images import EmbeddedImageError, store_embedded_images, validate_embedded_images from .embedded_images import EmbeddedImageError, store_embedded_images, validate_embedded_images
from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory
@@ -42,15 +43,21 @@ class DocumentForm(NetBoxModelForm):
}), help_text="Formatierter Text mit Tabellen, Farben, Schriftarten und Größen.") }), help_text="Formatierter Text mit Tabellen, Farben, Schriftarten und Größen.")
body_format = forms.CharField(widget=forms.HiddenInput(), initial="html") body_format = forms.CharField(widget=forms.HiddenInput(), initial="html")
category = DynamicModelChoiceField(queryset=DocumentCategory.objects.all(), required=False, label="Ordner") category = DynamicModelChoiceField(queryset=DocumentCategory.objects.all(), required=False, label="Ordner")
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), required=False, selector=True, label="Mandantengruppe",
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), required=False, selector=True, label="Mandant",
)
fieldsets = ( fieldsets = (
FieldSet("title", "slug", "category", "summary", "is_published", "tags", name="Dokumentation"), FieldSet("title", "slug", "category", "tenant_group", "tenant", "summary", "is_published", "tags", name="Dokumentation"),
FieldSet("body", "body_format", name="Inhalt"), FieldSet("body", "body_format", name="Inhalt"),
) )
class Meta: class Meta:
model = Document model = Document
fields = ("title", "slug", "category", "summary", "body", "body_format", "is_published", "tags") fields = ("title", "slug", "category", "tenant_group", "tenant", "summary", "body", "body_format", "is_published", "tags")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -70,6 +77,13 @@ class DocumentForm(NetBoxModelForm):
raise forms.ValidationError(str(exc)) from exc raise forms.ValidationError(str(exc)) from exc
return body return body
def clean(self):
cleaned = super().clean()
tenant, group = cleaned.get("tenant"), cleaned.get("tenant_group")
if tenant and group and tenant.group_id != group.pk:
self.add_error("tenant", "Der Mandant gehört nicht zur ausgewählten Mandantengruppe.")
return cleaned
def save(self, commit=True): def save(self, commit=True):
document = super().save(commit=commit) document = super().save(commit=commit)
if not commit or not document.pk or "data:image/" not in (document.body or "").lower(): if not commit or not document.pk or "data:image/" not in (document.body or "").lower():
@@ -134,18 +148,31 @@ class AssignmentForm(NetBoxModelForm):
class DocumentCategoryForm(NetBoxModelForm): class DocumentCategoryForm(NetBoxModelForm):
slug = SlugField(slug_source="name") slug = SlugField(slug_source="name")
parent = DynamicModelChoiceField(queryset=DocumentCategory.objects.all(), required=False, label="Übergeordneter Ordner") parent = DynamicModelChoiceField(queryset=DocumentCategory.objects.all(), required=False, label="Übergeordneter Ordner")
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), required=False, selector=True, label="Mandantengruppe",
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), required=False, selector=True, label="Mandant",
)
fieldsets = (FieldSet("name", "slug", "parent", "description", "tags", name="Ordner"),) fieldsets = (FieldSet("name", "slug", "parent", "tenant_group", "tenant", "description", "tags", name="Ordner"),)
class Meta: class Meta:
model = DocumentCategory model = DocumentCategory
fields = ("name", "slug", "parent", "description", "tags") fields = ("name", "slug", "parent", "tenant_group", "tenant", "description", "tags")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.instance.pk: if self.instance.pk:
self.fields["parent"].queryset = DocumentCategory.objects.exclude(pk=self.instance.pk) self.fields["parent"].queryset = DocumentCategory.objects.exclude(pk=self.instance.pk)
def clean(self):
cleaned = super().clean()
tenant, group = cleaned.get("tenant"), cleaned.get("tenant_group")
if tenant and group and tenant.group_id != group.pk:
self.add_error("tenant", "Der Mandant gehört nicht zur ausgewählten Mandantengruppe.")
return cleaned
class ImportForm(forms.Form): class ImportForm(forms.Form):
file = forms.FileField(label="Word-, Excel- oder PDF-Datei") file = forms.FileField(label="Word-, Excel- oder PDF-Datei")
@@ -0,0 +1,44 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("netbox_documentation", "0004_excelimportpreview"),
("tenancy", "0001_squashed_0012"),
]
operations = [
migrations.AddField(
model_name="document",
name="tenant_group",
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name="documentation_documents", to="tenancy.tenantgroup",
),
),
migrations.AddField(
model_name="document",
name="tenant",
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name="documentation_documents", to="tenancy.tenant",
),
),
migrations.AddField(
model_name="documentcategory",
name="tenant_group",
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name="documentation_categories", to="tenancy.tenantgroup",
),
),
migrations.AddField(
model_name="documentcategory",
name="tenant",
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name="documentation_categories", to="tenancy.tenant",
),
),
]
+16
View File
@@ -19,6 +19,14 @@ class Document(NetBoxModel):
summary = models.CharField(max_length=500, blank=True) summary = models.CharField(max_length=500, blank=True)
is_published = models.BooleanField(default=True) is_published = models.BooleanField(default=True)
category = models.ForeignKey("DocumentCategory", on_delete=models.SET_NULL, null=True, blank=True, related_name="documents") category = models.ForeignKey("DocumentCategory", on_delete=models.SET_NULL, null=True, blank=True, related_name="documents")
tenant_group = models.ForeignKey(
"tenancy.TenantGroup", on_delete=models.SET_NULL, null=True, blank=True,
related_name="documentation_documents",
)
tenant = models.ForeignKey(
"tenancy.Tenant", on_delete=models.SET_NULL, null=True, blank=True,
related_name="documentation_documents",
)
class Meta: class Meta:
ordering = ("title",) ordering = ("title",)
@@ -68,6 +76,14 @@ class DocumentCategory(NetBoxModel):
slug = models.SlugField(max_length=100) slug = models.SlugField(max_length=100)
parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children") parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
description = models.CharField(max_length=500, blank=True) description = models.CharField(max_length=500, blank=True)
tenant_group = models.ForeignKey(
"tenancy.TenantGroup", on_delete=models.SET_NULL, null=True, blank=True,
related_name="documentation_categories",
)
tenant = models.ForeignKey(
"tenancy.Tenant", on_delete=models.SET_NULL, null=True, blank=True,
related_name="documentation_categories",
)
class Meta: class Meta:
ordering = ("name",) ordering = ("name",)
+2 -2
View File
@@ -10,7 +10,7 @@ class DocumentTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Document model = Document
fields = ("pk", "title", "category", "summary", "is_published", "assignments", "last_updated", "actions") fields = ("pk", "title", "category", "tenant_group", "tenant", "summary", "is_published", "assignments", "last_updated", "actions")
class DocumentCategoryTable(NetBoxTable): class DocumentCategoryTable(NetBoxTable):
@@ -20,7 +20,7 @@ class DocumentCategoryTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = DocumentCategory model = DocumentCategory
fields = ("pk", "name", "parent", "description", "document_count", "actions") fields = ("pk", "name", "parent", "tenant_group", "tenant", "description", "document_count", "actions")
class AssignmentTable(NetBoxTable): class AssignmentTable(NetBoxTable):
@@ -78,6 +78,10 @@
<div class="card"><div class="card-body rendered-markdown documentation-content">{{ object.rendered_body|safe }}</div></div> <div class="card"><div class="card-body rendered-markdown documentation-content">{{ object.rendered_body|safe }}</div></div>
</div> </div>
<div class="col col-md-3"> <div class="col col-md-3">
{% if object.tenant_group or object.tenant %}<div class="card mb-3"><h5 class="card-header">Mandantenzuordnung</h5><div class="list-group list-group-flush">
{% if object.tenant_group %}<a class="list-group-item list-group-item-action" href="{{ object.tenant_group.get_absolute_url }}"><i class="mdi mdi-account-group"></i> {{ object.tenant_group }}</a>{% endif %}
{% if object.tenant %}<a class="list-group-item list-group-item-action" href="{{ object.tenant.get_absolute_url }}"><i class="mdi mdi-domain"></i> {{ object.tenant }}</a>{% endif %}
</div></div>{% endif %}
{% if object.category %}<div class="card mb-3"><h5 class="card-header">Ordner</h5><a class="list-group-item list-group-item-action" href="{{ object.category.get_absolute_url }}"><i class="mdi mdi-folder"></i> {{ object.category }}</a></div>{% endif %} {% if object.category %}<div class="card mb-3"><h5 class="card-header">Ordner</h5><a class="list-group-item list-group-item-action" href="{{ object.category.get_absolute_url }}"><i class="mdi mdi-folder"></i> {{ object.category }}</a></div>{% endif %}
<div class="card"><h5 class="card-header">Zuordnungen</h5><div class="list-group list-group-flush"> <div class="card"><h5 class="card-header">Zuordnungen</h5><div class="list-group list-group-flush">
{% for assignment in object.assignments.all %}<a class="list-group-item" href="{{ assignment.assigned_object.get_absolute_url }}">{{ assignment.assigned_object_type }}: {{ assignment.assigned_object }}</a>{% empty %}<div class="list-group-item text-muted">Noch nicht zugeordnet</div>{% endfor %} {% for assignment in object.assignments.all %}<a class="list-group-item" href="{{ assignment.assigned_object.get_absolute_url }}">{{ assignment.assigned_object_type }}: {{ assignment.assigned_object }}</a>{% empty %}<div class="list-group-item text-muted">Noch nicht zugeordnet</div>{% endfor %}
@@ -3,6 +3,15 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-4"> <div class="col col-md-4">
{% if object.tenant_group or object.tenant %}
<div class="card mb-3">
<h5 class="card-header">Mandantenzuordnung</h5>
<div class="list-group list-group-flush">
{% if object.tenant_group %}<a class="list-group-item list-group-item-action" href="{{ object.tenant_group.get_absolute_url }}"><i class="mdi mdi-account-group"></i> {{ object.tenant_group }}</a>{% endif %}
{% if object.tenant %}<a class="list-group-item list-group-item-action" href="{{ object.tenant.get_absolute_url }}"><i class="mdi mdi-domain"></i> {{ object.tenant }}</a>{% endif %}
</div>
</div>
{% endif %}
<div class="card"> <div class="card">
<h5 class="card-header">Unterordner</h5> <h5 class="card-header">Unterordner</h5>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
+14 -5
View File
@@ -46,14 +46,14 @@ class EditorAssetView(View):
class DocumentListView(generic.ObjectListView): class DocumentListView(generic.ObjectListView):
queryset = Document.objects.prefetch_related("assignments") queryset = Document.objects.select_related("category", "tenant_group", "tenant").prefetch_related("assignments")
table = DocumentTable table = DocumentTable
filterset = DocumentFilterSet filterset = DocumentFilterSet
actions = (AddObject, BulkImport, BulkDelete) actions = (AddObject, BulkImport, BulkDelete)
class DocumentView(generic.ObjectView): class DocumentView(generic.ObjectView):
queryset = Document.objects.prefetch_related("assignments", "attachments") queryset = Document.objects.select_related("category", "tenant_group", "tenant").prefetch_related("assignments", "attachments")
class DocumentPrintView(PermissionRequiredMixin, View): class DocumentPrintView(PermissionRequiredMixin, View):
@@ -85,14 +85,14 @@ class DocumentBulkDeleteView(generic.BulkDeleteView):
class DocumentCategoryListView(generic.ObjectListView): class DocumentCategoryListView(generic.ObjectListView):
queryset = DocumentCategory.objects.select_related("parent").prefetch_related("documents") queryset = DocumentCategory.objects.select_related("parent", "tenant_group", "tenant").prefetch_related("documents")
table = DocumentCategoryTable table = DocumentCategoryTable
filterset = DocumentCategoryFilterSet filterset = DocumentCategoryFilterSet
actions = (AddObject, BulkImport, BulkDelete) actions = (AddObject, BulkImport, BulkDelete)
class DocumentCategoryView(generic.ObjectView): class DocumentCategoryView(generic.ObjectView):
queryset = DocumentCategory.objects.prefetch_related("children", "documents") queryset = DocumentCategory.objects.select_related("tenant_group", "tenant").prefetch_related("children", "documents")
class DocumentCategoryEditView(generic.ObjectEditView): class DocumentCategoryEditView(generic.ObjectEditView):
@@ -170,8 +170,12 @@ class DocumentImportView(PermissionRequiredMixin, View):
while Document.objects.filter(slug=slug).exists(): while Document.objects.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}" slug = f"{base_slug}-{counter}"
counter += 1 counter += 1
category = form.cleaned_data.get("category")
document = Document.objects.create( document = Document.objects.create(
title=title, slug=slug, body=result.markdown, body_format=result.body_format, title=title, slug=slug, body=result.markdown, body_format=result.body_format,
category=category,
tenant_group=category.tenant_group if category else None,
tenant=category.tenant if category else None,
) )
keep = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get("keep_imported_file", True) keep = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get("keep_imported_file", True)
if keep: if keep:
@@ -351,7 +355,11 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
document.body = sheet.markdown document.body = sheet.markdown
document.body_format = sheet.body_format document.body_format = sheet.body_format
document.summary = f"Importiert aus {preview.original_name}" document.summary = f"Importiert aus {preview.original_name}"
document.save(update_fields=("title", "body", "body_format", "summary", "last_updated")) document.tenant_group = preview.category.tenant_group
document.tenant = preview.category.tenant
document.save(update_fields=(
"title", "body", "body_format", "summary", "tenant_group", "tenant", "last_updated",
))
updated_count += 1 updated_count += 1
else: else:
base_slug = slugify(title)[:180] or "arbeitsblatt" base_slug = slugify(title)[:180] or "arbeitsblatt"
@@ -362,6 +370,7 @@ class ExcelImportPreviewView(PermissionRequiredMixin, View):
document = Document.objects.create( document = Document.objects.create(
title=title[:200], slug=document_slug, body=sheet.markdown, title=title[:200], slug=document_slug, body=sheet.markdown,
body_format=sheet.body_format, category=preview.category, body_format=sheet.body_format, category=preview.category,
tenant_group=preview.category.tenant_group, tenant=preview.category.tenant,
summary=f"Importiert aus {preview.original_name}", summary=f"Importiert aus {preview.original_name}",
) )
created_count += 1 created_count += 1
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netbox-documentation" name = "netbox-documentation"
version = "0.7.14" version = "0.9.0"
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"