diff --git a/README.md b/README.md
index e06f2a2..8fe33d5 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
- Mehrere Dokumentationen mit Ordnern, Zuordnungen und Anhängen als ZIP exportieren und wieder importieren
- Druckoptimierte A4-Ansicht zum Drucken oder Speichern als PDF
- Excel-Mehrblattimport: je Arbeitsblatt eine Dokumentation in einem gewählten Zielordner
+- Optionales Glätten von Excel-Tabellen in kompakte Feld-/Wert-Textblöcke
- Eine Dokumentation mehreren Objekten zuordnen und umgekehrt
- Unbegrenzt viele Objektzuordnungen pro Dokumentation; nur identische Doppelzuordnungen werden verhindert
- Unterstützte Standardobjekte: Region, Standort, Location, Rack, Gerät, VM, VM-Cluster und Mandant/Kunde
@@ -23,7 +24,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
## Kompatibilität
-Die Version `0.5.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.5.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
@@ -122,7 +123,9 @@ Objektbezogene NetBox-Constraints sollten zusätzlich passend zu Mandanten und V
Alte binäre `.doc`- und `.xls`-Dateien müssen vorher in `.docx` bzw. `.xlsx` konvertiert werden. Gescannte PDFs benötigen OCR, die in dieser Version bewusst noch nicht enthalten ist. Komplexe Word-/PDF-Layouts, eingebettete Bilder und Excel-Formatierungen können nicht verlustfrei nach Markdown übertragen werden.
-Für große Excel-Dateien kann auf der Importseite **„Excel mit mehreren Arbeitsblättern – je Blatt eine Dokumentation“** aktiviert werden. Dazu ist ein Zielordner verpflichtend. Jedes nicht leere Arbeitsblatt wird als eigene Dokumentation mit dem Blattnamen als Titel angelegt. Normale eingebettete PNG-, JPEG-, GIF- und WebP-Bilder werden dem jeweiligen Dokument als Medienanhang hinzugefügt. Diagramme, SmartArt, Steuerelemente und extern verknüpfte Bilder können nicht zuverlässig übernommen werden.
+Für große Excel-Dateien kann auf der Importseite **„Excel mit mehreren Arbeitsblättern – je Blatt eine Dokumentation“** aktiviert werden. Dazu ist ein Zielordner verpflichtend. Jedes nicht leere Arbeitsblatt wird als eigene Dokumentation mit dem Blattnamen als Titel angelegt und erhält eine eigene Kopie der Original-Exceldatei als Anhang. Normale eingebettete PNG-, JPEG-, GIF- und WebP-Bilder werden dem jeweiligen Dokument als Medienanhang hinzugefügt. Diagramme, SmartArt, Steuerelemente und extern verknüpfte Bilder können nicht zuverlässig übernommen werden.
+
+Mit **„Excel-Tabellen glätten“** wird jede Datenzeile statt als breite Tabelle als kompakter Textblock aus Feldnamen und Wert importiert. Nicht geglättete Tabellen werden in der Dokumentansicht automatisch kompakt dargestellt und bei Bedarf innerhalb des Inhalts horizontal scrollbar, ohne das NetBox-Layout zu verbreitern.
## REST-API
diff --git a/netbox_documentation/__init__.py b/netbox_documentation/__init__.py
index 8a4df34..19cec8b 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.5.0"
+ version = "0.5.1"
author = "LKE"
base_url = "documentation"
min_version = "4.0.0"
diff --git a/netbox_documentation/forms.py b/netbox_documentation/forms.py
index 0e7f8c4..f8e1ace 100644
--- a/netbox_documentation/forms.py
+++ b/netbox_documentation/forms.py
@@ -2,6 +2,7 @@ from django import forms
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
+from django.utils.html import format_html
from utilities.forms import get_field_value
from utilities.forms.fields import (
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
@@ -13,6 +14,14 @@ from dcim.models import Device
from .models import Document, DocumentAssignment, DocumentCategory
+def help_label(label, description):
+ return format_html(
+ '{} ',
+ label, description, description,
+ )
+
+
def allowed_content_types():
labels = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get(
"allowed_object_types", []
@@ -105,17 +114,32 @@ class DocumentCategoryForm(NetBoxModelForm):
class ImportForm(forms.Form):
file = forms.FileField(label="Word-, Excel- oder PDF-Datei")
- title = forms.CharField(max_length=200, required=False, help_text="Leer lassen, um den Dateinamen zu verwenden")
- append = forms.BooleanField(required=False, initial=False, label="An bestehende Dokumentation anhängen")
- document = forms.ModelChoiceField(queryset=Document.objects.all(), required=False, label="Bestehende Dokumentation")
+ title = forms.CharField(max_length=200, required=False, label=help_label(
+ "Titel", "Leer lassen, um den Dateinamen als Titel zu verwenden. Beim Mehrblattimport werden die Blattnamen verwendet."
+ ))
+ append = forms.BooleanField(required=False, initial=False, label=help_label(
+ "An bestehende Dokumentation anhängen", "Fügt den importierten Inhalt am Ende einer vorhandenen Dokumentation an."
+ ))
+ document = forms.ModelChoiceField(queryset=Document.objects.all(), required=False, label=help_label(
+ "Bestehende Dokumentation", "Zieldokumentation für die Funktion Anhängen."
+ ))
excel_sheets_as_documents = forms.BooleanField(
required=False,
- label="Excel mit mehreren Arbeitsblättern – je Blatt eine Dokumentation",
+ label=help_label(
+ "Excel mit mehreren Arbeitsblättern – je Blatt eine Dokumentation",
+ "Erstellt aus jedem nicht leeren Excel-Arbeitsblatt eine eigene Dokumentation im gewählten Zielordner.",
+ ),
+ )
+ flatten_excel_tables = forms.BooleanField(
+ required=False,
+ label=help_label(
+ "Excel-Tabellen glätten",
+ "Wandelt jede Tabellenzeile in einen lesbaren Textblock aus Feldnamen und Wert um, statt eine Tabelle zu erzeugen.",
+ ),
)
category = DynamicModelChoiceField(
queryset=DocumentCategory.objects.all(), required=False,
- label="Zielordner / Kategorie",
- help_text="Für den Mehrblattimport verpflichtend.",
+ label=help_label("Zielordner / Kategorie", "Für den Excel-Mehrblattimport verpflichtend."),
)
def __init__(self, *args, user=None, **kwargs):
diff --git a/netbox_documentation/importers.py b/netbox_documentation/importers.py
index a75b73b..79c9bcb 100644
--- a/netbox_documentation/importers.py
+++ b/netbox_documentation/importers.py
@@ -29,14 +29,14 @@ class ExcelSheetResult:
images: list[ImportedImage] = field(default_factory=list)
-def import_document(upload) -> ImportResult:
+def import_document(upload, flatten_excel=False) -> ImportResult:
suffix = Path(upload.name).suffix.lower()
content = upload.read()
upload.seek(0)
if suffix == ".docx":
return _docx(content)
if suffix in {".xlsx", ".xlsm"}:
- return _xlsx(content)
+ return _xlsx(content, flatten=flatten_excel)
if suffix == ".pdf":
return _pdf(content)
if suffix in {".md", ".txt"}:
@@ -53,9 +53,28 @@ def _docx(content):
return ImportResult(result.value.strip(), warnings)
-def _xlsx(content):
- from openpyxl import load_workbook
+def _render_excel_rows(rows, flatten=False):
from tabulate import tabulate
+ width = max(len(row) for row in rows)
+ rows = [row + [""] * (width - len(row)) for row in rows]
+ header, body = rows[0], rows[1:]
+ if not flatten:
+ return tabulate(body, headers=header, tablefmt="github")
+ blocks = []
+ for number, row in enumerate(body, 1):
+ values = []
+ for column, value in zip(header, row):
+ if value == "":
+ continue
+ label = column or "Feld"
+ values.append(f"**{label}:** {value}")
+ if values:
+ blocks.append(f"### Datensatz {number}\n\n" + " \n".join(values))
+ return "\n\n".join(blocks) or "_Keine Datensätze_"
+
+
+def _xlsx(content, flatten=False):
+ from openpyxl import load_workbook
workbook = load_workbook(BytesIO(content), read_only=True, data_only=True)
sections = []
for sheet in workbook.worksheets:
@@ -64,20 +83,16 @@ def _xlsx(content):
rows.pop()
if not rows:
continue
- width = max(len(row) for row in rows)
- rows = [row + [""] * (width - len(row)) for row in rows]
- header, body = rows[0], rows[1:]
- sections.append(f"## {sheet.title}\n\n{tabulate(body, headers=header, tablefmt='github')}")
+ sections.append(f"## {sheet.title}\n\n{_render_excel_rows(rows, flatten)}")
if not sections:
raise ImportFailure("Die Arbeitsmappe enthält keine Daten.")
return ImportResult("\n\n".join(sections))
-def import_excel_sheets(upload) -> list[ExcelSheetResult]:
+def import_excel_sheets(upload, flatten=False) -> list[ExcelSheetResult]:
"""Convert each non-empty worksheet into an individual document payload."""
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
- from tabulate import tabulate
suffix = Path(upload.name).suffix.lower()
if suffix not in {".xlsx", ".xlsm"}:
@@ -111,9 +126,7 @@ def import_excel_sheets(upload) -> list[ExcelSheetResult]:
if not rows and not images:
continue
if rows:
- width = max(len(row) for row in rows)
- rows = [row + [""] * (width - len(row)) for row in rows]
- markdown = tabulate(rows[1:], headers=rows[0], tablefmt="github")
+ markdown = _render_excel_rows(rows, flatten)
else:
markdown = ""
results.append(ExcelSheetResult(title=sheet.title, markdown=markdown, images=images))
diff --git a/netbox_documentation/templates/netbox_documentation/document.html b/netbox_documentation/templates/netbox_documentation/document.html
index 7886bc4..0b8cc74 100644
--- a/netbox_documentation/templates/netbox_documentation/document.html
+++ b/netbox_documentation/templates/netbox_documentation/document.html
@@ -1,5 +1,28 @@
{% extends 'generic/object.html' %}
{% load helpers %}
+{% block head %}
+ {{ block.super }}
+
+{% endblock head %}
{% block extra_controls %}
Drucken / PDF
@@ -9,7 +32,7 @@