feat: Mandantenzuordnung für Dokumente und Ordner ergänzen
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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')}"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user