feat: Excel-Import glätten und Dokumentlistenaktionen korrigieren

This commit is contained in:
2026-07-22 14:34:14 +02:00
parent 6df5c101c5
commit eb503df94b
11 changed files with 129 additions and 36 deletions
+5 -2
View File
@@ -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 - Mehrere Dokumentationen mit Ordnern, Zuordnungen und Anhängen als ZIP exportieren und wieder importieren
- Druckoptimierte A4-Ansicht zum Drucken oder Speichern als PDF - Druckoptimierte A4-Ansicht zum Drucken oder Speichern als PDF
- Excel-Mehrblattimport: je Arbeitsblatt eine Dokumentation in einem gewählten Zielordner - 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 - Eine Dokumentation mehreren Objekten zuordnen und umgekehrt
- Unbegrenzt viele Objektzuordnungen pro Dokumentation; nur identische Doppelzuordnungen werden verhindert - 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 - 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 ## 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 ## 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. 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 ## REST-API
+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.5.0" version = "0.5.1"
author = "LKE" author = "LKE"
base_url = "documentation" base_url = "documentation"
min_version = "4.0.0" min_version = "4.0.0"
+30 -6
View File
@@ -2,6 +2,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.utils.html import format_html
from utilities.forms import get_field_value from utilities.forms import get_field_value
from utilities.forms.fields import ( from utilities.forms.fields import (
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
@@ -13,6 +14,14 @@ from dcim.models import Device
from .models import Document, DocumentAssignment, DocumentCategory from .models import Document, DocumentAssignment, DocumentCategory
def help_label(label, description):
return format_html(
'{} <span class="mdi mdi-help-circle-outline text-muted" title="{}" '
'data-bs-toggle="tooltip" aria-label="{}"></span>',
label, description, description,
)
def allowed_content_types(): def allowed_content_types():
labels = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get( labels = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get(
"allowed_object_types", [] "allowed_object_types", []
@@ -105,17 +114,32 @@ class DocumentCategoryForm(NetBoxModelForm):
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")
title = forms.CharField(max_length=200, required=False, help_text="Leer lassen, um den Dateinamen zu verwenden") title = forms.CharField(max_length=200, required=False, label=help_label(
append = forms.BooleanField(required=False, initial=False, label="An bestehende Dokumentation anhängen") "Titel", "Leer lassen, um den Dateinamen als Titel zu verwenden. Beim Mehrblattimport werden die Blattnamen verwendet."
document = forms.ModelChoiceField(queryset=Document.objects.all(), required=False, label="Bestehende Dokumentation") ))
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( excel_sheets_as_documents = forms.BooleanField(
required=False, 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( category = DynamicModelChoiceField(
queryset=DocumentCategory.objects.all(), required=False, queryset=DocumentCategory.objects.all(), required=False,
label="Zielordner / Kategorie", label=help_label("Zielordner / Kategorie", "Für den Excel-Mehrblattimport verpflichtend."),
help_text="Für den Mehrblattimport verpflichtend.",
) )
def __init__(self, *args, user=None, **kwargs): def __init__(self, *args, user=None, **kwargs):
+26 -13
View File
@@ -29,14 +29,14 @@ class ExcelSheetResult:
images: list[ImportedImage] = field(default_factory=list) 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() suffix = Path(upload.name).suffix.lower()
content = upload.read() content = upload.read()
upload.seek(0) upload.seek(0)
if suffix == ".docx": if suffix == ".docx":
return _docx(content) return _docx(content)
if suffix in {".xlsx", ".xlsm"}: if suffix in {".xlsx", ".xlsm"}:
return _xlsx(content) return _xlsx(content, flatten=flatten_excel)
if suffix == ".pdf": if suffix == ".pdf":
return _pdf(content) return _pdf(content)
if suffix in {".md", ".txt"}: if suffix in {".md", ".txt"}:
@@ -53,9 +53,28 @@ def _docx(content):
return ImportResult(result.value.strip(), warnings) return ImportResult(result.value.strip(), warnings)
def _xlsx(content): def _render_excel_rows(rows, flatten=False):
from openpyxl import load_workbook
from tabulate import tabulate 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) workbook = load_workbook(BytesIO(content), read_only=True, data_only=True)
sections = [] sections = []
for sheet in workbook.worksheets: for sheet in workbook.worksheets:
@@ -64,20 +83,16 @@ def _xlsx(content):
rows.pop() rows.pop()
if not rows: if not rows:
continue continue
width = max(len(row) for row in rows) sections.append(f"## {sheet.title}\n\n{_render_excel_rows(rows, flatten)}")
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')}")
if not sections: if not sections:
raise ImportFailure("Die Arbeitsmappe enthält keine Daten.") raise ImportFailure("Die Arbeitsmappe enthält keine Daten.")
return ImportResult("\n\n".join(sections)) 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.""" """Convert each non-empty worksheet into an individual document payload."""
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
from tabulate import tabulate
suffix = Path(upload.name).suffix.lower() suffix = Path(upload.name).suffix.lower()
if suffix not in {".xlsx", ".xlsm"}: if suffix not in {".xlsx", ".xlsm"}:
@@ -111,9 +126,7 @@ def import_excel_sheets(upload) -> list[ExcelSheetResult]:
if not rows and not images: if not rows and not images:
continue continue
if rows: if rows:
width = max(len(row) for row in rows) markdown = _render_excel_rows(rows, flatten)
rows = [row + [""] * (width - len(row)) for row in rows]
markdown = tabulate(rows[1:], headers=rows[0], tablefmt="github")
else: else:
markdown = "" markdown = ""
results.append(ExcelSheetResult(title=sheet.title, markdown=markdown, images=images)) results.append(ExcelSheetResult(title=sheet.title, markdown=markdown, images=images))
@@ -1,5 +1,28 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% block head %}
{{ block.super }}
<style>
.documentation-content { overflow-x: auto; }
.documentation-content table {
width: auto !important;
max-width: 100%;
table-layout: auto;
font-size: .875rem;
line-height: 1.25;
}
.documentation-content th,
.documentation-content td {
min-width: 3rem;
max-width: 22rem;
padding: .25rem .4rem !important;
overflow-wrap: anywhere;
word-break: normal;
vertical-align: top;
}
.documentation-content p { max-width: 90rem; }
</style>
{% endblock head %}
{% block extra_controls %} {% block extra_controls %}
<a href="{% url 'plugins:netbox_documentation:document_print' pk=object.pk %}" target="_blank" class="btn btn-outline-secondary"> <a href="{% url 'plugins:netbox_documentation:document_print' pk=object.pk %}" target="_blank" class="btn btn-outline-secondary">
<i class="mdi mdi-printer"></i> Drucken / PDF <i class="mdi mdi-printer"></i> Drucken / PDF
@@ -9,7 +32,7 @@
<div class="row"> <div class="row">
<div class="col col-md-9"> <div class="col col-md-9">
{% if object.summary %}<p class="lead">{{ object.summary }}</p>{% endif %} {% if object.summary %}<p class="lead">{{ object.summary }}</p>{% endif %}
<div class="card"><div class="card-body rendered-markdown">{{ 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.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 %}
@@ -6,9 +6,8 @@
<h5 class="card-header">Word, Excel oder PDF importieren</h5> <h5 class="card-header">Word, Excel oder PDF importieren</h5>
<div class="card-body"><form method="post" enctype="multipart/form-data">{% csrf_token %}{% render_form form %} <div class="card-body"><form method="post" enctype="multipart/form-data">{% csrf_token %}{% render_form form %}
<div class="text-muted mb-3"> <div class="text-muted mb-3">
DOCX übernimmt Überschriften, Listen und Tabellen. Beim normalen XLSX-Import werden alle Blätter in einer Dokumentation zusammengeführt. Importhinweise
Mit <strong>„Excel mit mehreren Arbeitsblättern“</strong> entsteht dagegen je nicht leerem Blatt eine eigene Dokumentation im verpflichtend gewählten Zielordner; eingebettete Standardbilder werden soweit möglich übernommen. <span class="mdi mdi-help-circle-outline" data-bs-toggle="tooltip" title="DOCX übernimmt Überschriften, Listen und Tabellen. Beim normalen XLSX-Import werden alle Blätter zusammengeführt. Der Mehrblattimport erzeugt je Blatt eine Dokumentation im Zielordner. PDF benötigt eine echte Textebene; Scan-OCR ist nicht enthalten." aria-label="Importhinweise"></span>
PDF benötigt eine echte Textebene; Scan-OCR ist nicht enthalten.
</div> </div>
<button type="submit" class="btn btn-primary">Importieren</button> <button type="submit" class="btn btn-primary">Importieren</button>
</form></div> </form></div>
@@ -16,7 +16,7 @@
.summary { color: #4b5563; font-size: 13pt; margin: 0 0 1rem; } .summary { color: #4b5563; font-size: 13pt; margin: 0 0 1rem; }
.meta { display: flex; flex-wrap: wrap; gap: .4rem 1.25rem; padding: .65rem 0; margin-bottom: 1.4rem; color: #6b7280; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; font-size: 9pt; } .meta { display: flex; flex-wrap: wrap; gap: .4rem 1.25rem; padding: .65rem 0; margin-bottom: 1.4rem; color: #6b7280; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; font-size: 9pt; }
.content img { max-width: 100%; height: auto; } .content img { max-width: 100%; height: auto; }
.content table { width: 100%; border-collapse: collapse; margin: 1em 0; } .content table { width: auto; max-width: 100%; border-collapse: collapse; margin: 1em 0; font-size: 9pt; line-height: 1.25; }
.content th, .content td { border: 1px solid #777; padding: .35rem .5rem; vertical-align: top; } .content th, .content td { border: 1px solid #777; padding: .35rem .5rem; vertical-align: top; }
.content pre, .content code { white-space: pre-wrap; overflow-wrap: anywhere; font-family: Consolas, monospace; } .content pre, .content code { white-space: pre-wrap; overflow-wrap: anywhere; font-family: Consolas, monospace; }
.content a { color: #174ea6; overflow-wrap: anywhere; } .content a { color: #174ea6; overflow-wrap: anywhere; }
+2
View File
@@ -7,6 +7,8 @@ urlpatterns = (
path("", views.DocumentListView.as_view(), name="document_list"), path("", views.DocumentListView.as_view(), name="document_list"),
path("documents/add/", views.DocumentEditView.as_view(), name="document_add"), path("documents/add/", views.DocumentEditView.as_view(), name="document_add"),
path("documents/import/", views.DocumentImportView.as_view(), name="document_import"), path("documents/import/", views.DocumentImportView.as_view(), name="document_import"),
path("documents/import/", views.DocumentImportView.as_view(), name="document_bulk_import"),
path("documents/delete/", views.DocumentBulkDeleteView.as_view(), name="document_bulk_delete"),
path("archive/", views.DocumentArchiveView.as_view(), name="document_archive"), path("archive/", views.DocumentArchiveView.as_view(), name="document_archive"),
path("documents/<int:pk>/", views.DocumentView.as_view(), name="document"), path("documents/<int:pk>/", views.DocumentView.as_view(), name="document"),
path("documents/<int:pk>/print/", views.DocumentPrintView.as_view(), name="document_print"), path("documents/<int:pk>/print/", views.DocumentPrintView.as_view(), name="document_print"),
+21 -8
View File
@@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils.text import slugify from django.utils.text import slugify
from django.views import View from django.views import View
from netbox.views import generic from netbox.views import generic
from netbox.object_actions import AddObject, BulkDelete, BulkImport
from .filtersets import DocumentFilterSet, AssignmentFilterSet, DocumentCategoryFilterSet from .filtersets import DocumentFilterSet, AssignmentFilterSet, DocumentCategoryFilterSet
from .forms import ( from .forms import (
ArchiveExportForm, ArchiveImportForm, AssignmentForm, DocumentCategoryForm, ArchiveExportForm, ArchiveImportForm, AssignmentForm, DocumentCategoryForm,
@@ -41,6 +42,7 @@ class DocumentListView(generic.ObjectListView):
queryset = Document.objects.prefetch_related("assignments") queryset = Document.objects.prefetch_related("assignments")
table = DocumentTable table = DocumentTable
filterset = DocumentFilterSet filterset = DocumentFilterSet
actions = (AddObject, BulkImport, BulkDelete)
class DocumentView(generic.ObjectView): class DocumentView(generic.ObjectView):
@@ -69,6 +71,12 @@ class DocumentDeleteView(generic.ObjectDeleteView):
queryset = Document.objects.all() queryset = Document.objects.all()
class DocumentBulkDeleteView(generic.BulkDeleteView):
queryset = Document.objects.all()
table = DocumentTable
filterset = DocumentFilterSet
class DocumentCategoryListView(generic.ObjectListView): class DocumentCategoryListView(generic.ObjectListView):
queryset = DocumentCategory.objects.select_related("parent").prefetch_related("documents") queryset = DocumentCategory.objects.select_related("parent").prefetch_related("documents")
table = DocumentCategoryTable table = DocumentCategoryTable
@@ -118,7 +126,7 @@ class DocumentImportView(PermissionRequiredMixin, View):
if form.cleaned_data.get("excel_sheets_as_documents"): if form.cleaned_data.get("excel_sheets_as_documents"):
return self._import_excel_sheets(request, form, upload) return self._import_excel_sheets(request, form, upload)
try: try:
result = import_document(upload) result = import_document(upload, flatten_excel=form.cleaned_data.get("flatten_excel_tables", False))
except (ImportFailure, Exception) as exc: except (ImportFailure, Exception) as exc:
# Known conversion/library errors are presented without exposing a traceback. # Known conversion/library errors are presented without exposing a traceback.
form.add_error("file", f"Import fehlgeschlagen: {exc}") form.add_error("file", f"Import fehlgeschlagen: {exc}")
@@ -155,12 +163,15 @@ class DocumentImportView(PermissionRequiredMixin, View):
def _import_excel_sheets(self, request, form, upload): def _import_excel_sheets(self, request, form, upload):
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
try: try:
sheets = import_excel_sheets(upload) sheets = import_excel_sheets(upload, flatten=form.cleaned_data.get("flatten_excel_tables", False))
except (ImportFailure, Exception) as exc: except (ImportFailure, Exception) as exc:
form.add_error("file", f"Excel-Mehrblattimport fehlgeschlagen: {exc}") form.add_error("file", f"Excel-Mehrblattimport fehlgeschlagen: {exc}")
return render(request, self.template_name, {"form": form}) return render(request, self.template_name, {"form": form})
category = form.cleaned_data["category"] category = form.cleaned_data["category"]
created_documents = [] created_documents = []
upload.seek(0)
original_content = upload.read()
upload.seek(0)
with transaction.atomic(): with transaction.atomic():
for sheet in sheets: for sheet in sheets:
base_slug = slugify(sheet.title)[:180] or "arbeitsblatt" base_slug = slugify(sheet.title)[:180] or "arbeitsblatt"
@@ -188,12 +199,14 @@ class DocumentImportView(PermissionRequiredMixin, View):
document.save(update_fields=("body", "last_updated")) document.save(update_fields=("body", "last_updated"))
created_documents.append(document) created_documents.append(document)
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 and created_documents: if keep:
upload.seek(0) for document in created_documents:
DocumentAttachment.objects.create( attachment = DocumentAttachment(
document=created_documents[0], file=upload, original_name=upload.name, document=document, original_name=upload.name,
content_type=upload.content_type or "", size=upload.size, content_type=upload.content_type or "", size=len(original_content),
) )
attachment.file.save(upload.name, ContentFile(original_content), save=False)
attachment.save()
image_count = sum(len(sheet.images) for sheet in sheets) image_count = sum(len(sheet.images) for sheet in sheets)
messages.success(request, ( messages.success(request, (
f"{len(created_documents)} Arbeitsblätter als Dokumentationen in „{category}“ importiert; " f"{len(created_documents)} Arbeitsblätter als Dokumentationen in „{category}“ importiert; "
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netbox-documentation" name = "netbox-documentation"
version = "0.5.0" version = "0.5.1"
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"
+16
View File
@@ -65,3 +65,19 @@ def test_excel_multi_sheet_import_creates_one_result_per_sheet():
assert [result.title for result in results] == ["Server", "Kontakte"] assert [result.title for result in results] == ["Server", "Kontakte"]
assert "web01" in results[0].markdown assert "web01" in results[0].markdown
assert "Leitstelle" in results[1].markdown assert "Leitstelle" in results[1].markdown
def test_excel_flatten_turns_rows_into_field_value_blocks():
workbook = Workbook()
sheet = workbook.active
sheet.append(["Name", "IP"])
sheet.append(["db01", "10.0.0.2"])
stream = BytesIO()
workbook.save(stream)
results = import_excel_sheets(Upload(stream.getvalue(), "server.xlsx"), flatten=True)
assert "### Datensatz 1" in results[0].markdown
assert "**Name:** db01" in results[0].markdown
assert "**IP:** 10.0.0.2" in results[0].markdown
assert "|" not in results[0].markdown