feat: strukturierte Excel-Tabellen beim Glätten erhalten
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user