feat: strukturierte Excel-Tabellen beim Glätten erhalten

This commit is contained in:
2026-07-22 14:46:48 +02:00
parent e5767add73
commit a3e255352a
6 changed files with 76 additions and 8 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
## Kompatibilität
Die Version `0.5.3` 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.4` 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
@@ -125,7 +125,7 @@ Alte binäre `.doc`- und `.xls`-Dateien müssen vorher in `.docx` bzw. `.xlsx` k
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“** werden die Zellen ohne künstliche Datensatz- oder Feldbezeichnungen in ihrer Lesereihenfolge übernommen. Zellen einer Excel-Zeile erscheinen als einzelne Textzeilen eines Absatzes; zwischen Excel-Zeilen wird ein Absatz eingefügt. Das Ergebnis ähnelt damit einer Übertragung nach Word ohne Tabelle. 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.
Mit **„Excel-Tabellen glätten“** werden normale Zellen ohne künstliche Datensatz- oder Feldbezeichnungen in ihrer Lesereihenfolge übernommen. Zellen einer Excel-Zeile erscheinen als einzelne Textzeilen eines Absatzes; zwischen Excel-Zeilen wird ein Absatz eingefügt. Bereiche, die in Excel ausdrücklich über **Einfügen → Tabelle** als strukturierte Tabelle gekennzeichnet wurden, bleiben dagegen als Tabelle erhalten. Rein optisch mit Rahmen oder Farben formatierte Bereiche gelten nicht als Tabelle. 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
+1 -1
View File
@@ -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.3"
version = "0.5.4"
author = "LKE"
base_url = "documentation"
min_version = "4.0.0"
+1 -1
View File
@@ -134,7 +134,7 @@ class ImportForm(forms.Form):
required=False,
label=help_label(
"Excel-Tabellen glätten",
"Übernimmt alle Zellen ohne Tabelle in Lesereihenfolge als dokumentartigen Text. Es werden keine Datensatz- oder Feldbezeichnungen erzeugt.",
"Übernimmt normale Zellen ohne Tabelle als dokumentartigen Text. Nur in Excel ausdrücklich über Einfügen → Tabelle definierte Bereiche bleiben Tabellen.",
),
)
category = forms.ModelChoiceField(
+49 -3
View File
@@ -80,7 +80,7 @@ def _escape_markdown_text(value):
def _xlsx(content, flatten=False):
from openpyxl import load_workbook
workbook = load_workbook(BytesIO(content), read_only=True, data_only=True)
workbook = load_workbook(BytesIO(content), read_only=not flatten, data_only=True)
sections = []
for sheet in workbook.worksheets:
rows = [["" if cell is None else str(cell) for cell in row] for row in sheet.iter_rows(values_only=True)]
@@ -88,7 +88,8 @@ def _xlsx(content, flatten=False):
rows.pop()
if not rows:
continue
sections.append(f"## {sheet.title}\n\n{_render_excel_rows(rows, flatten)}")
rendered = _render_flattened_worksheet(sheet) if flatten else _render_excel_rows(rows)
sections.append(f"## {sheet.title}\n\n{rendered}")
if not sections:
raise ImportFailure("Die Arbeitsmappe enthält keine Daten.")
return ImportResult("\n\n".join(sections))
@@ -131,7 +132,7 @@ def import_excel_sheets(upload, flatten=False) -> list[ExcelSheetResult]:
if not rows and not images:
continue
if rows:
markdown = _render_excel_rows(rows, flatten)
markdown = _render_flattened_worksheet(sheet) if flatten else _render_excel_rows(rows)
else:
markdown = ""
results.append(ExcelSheetResult(title=sheet.title, markdown=markdown, images=images))
@@ -145,6 +146,51 @@ def slugify_filename(value):
return value[:80] or "arbeitsblatt"
def _render_flattened_worksheet(sheet):
"""Flatten ordinary cells while preserving explicitly defined Excel tables."""
from openpyxl.utils.cell import range_boundaries
table_ranges = []
for table in sheet.tables.values():
min_col, min_row, max_col, max_row = range_boundaries(table.ref)
table_ranges.append((min_col, min_row, max_col, max_row))
table_ranges.sort(key=lambda item: (item[1], item[0]))
def containing_table(row, column):
for bounds in table_ranges:
min_col, min_row, max_col, max_row = bounds
if min_row <= row <= max_row and min_col <= column <= max_col:
return bounds
return None
blocks = []
rendered_tables = set()
for row_number in range(1, sheet.max_row + 1):
ordinary_values = []
tables_starting_here = []
for column_number in range(1, sheet.max_column + 1):
bounds = containing_table(row_number, column_number)
if bounds:
if bounds[1] == row_number and bounds not in rendered_tables:
tables_starting_here.append(bounds)
rendered_tables.add(bounds)
continue
value = sheet.cell(row=row_number, column=column_number).value
if value not in (None, ""):
ordinary_values.append(_escape_markdown_text(value))
if ordinary_values:
blocks.append(" \n".join(ordinary_values))
for min_col, min_row, max_col, max_row in tables_starting_here:
rows = []
for table_row in sheet.iter_rows(
min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col, values_only=True
):
rows.append(["" if value is None else str(value) for value in table_row])
if rows:
blocks.append(_render_excel_rows(rows, flatten=False))
return "\n\n".join(blocks) or "_Keine Inhalte_"
def _pdf(content):
from pypdf import PdfReader
reader = PdfReader(BytesIO(content))
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "netbox-documentation"
version = "0.5.3"
version = "0.5.4"
description = "Integrated Markdown wiki and office document importer for NetBox"
readme = "README.md"
requires-python = ">=3.10"
+22
View File
@@ -5,6 +5,7 @@ import sys
import pytest
from openpyxl import Workbook
from openpyxl.worksheet.table import Table
spec = importlib.util.spec_from_file_location(
@@ -81,3 +82,24 @@ def test_excel_flatten_turns_cells_into_document_flow():
assert "Datensatz" not in results[0].markdown
assert "Feld" not in results[0].markdown
assert "|" not in results[0].markdown
def test_excel_flatten_preserves_explicit_excel_tables_only():
workbook = Workbook()
sheet = workbook.active
sheet["A1"] = "Name"
sheet["B1"] = "IP"
sheet["A2"] = "web01"
sheet["B2"] = "10.0.0.1"
sheet["D1"] = "Normaler Hinweis"
sheet.add_table(Table(displayName="ServerTable", ref="A1:B2"))
stream = BytesIO()
workbook.save(stream)
results = import_excel_sheets(Upload(stream.getvalue(), "server.xlsx"), flatten=True)
markdown = results[0].markdown
assert "Normaler Hinweis" in markdown
assert "| Name" in markdown
assert "| web01" in markdown
assert "Datensatz" not in markdown