diff --git a/README.md b/README.md index 1407d84..e06f2a2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit - 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 - Druckoptimierte A4-Ansicht zum Drucken oder Speichern als PDF +- Excel-Mehrblattimport: je Arbeitsblatt eine Dokumentation in einem gewählten Zielordner - 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 @@ -22,7 +23,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit ## Kompatibilität -Die Version `0.4.2` 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.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 @@ -121,6 +122,8 @@ 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. + ## REST-API Nach Aktivierung stehen die üblichen NetBox-Plugin-Endpunkte bereit: diff --git a/netbox_documentation/__init__.py b/netbox_documentation/__init__.py index 6949f3c..8a4df34 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.4.2" + version = "0.5.0" author = "LKE" base_url = "documentation" min_version = "4.0.0" diff --git a/netbox_documentation/forms.py b/netbox_documentation/forms.py index 78f3793..0e7f8c4 100644 --- a/netbox_documentation/forms.py +++ b/netbox_documentation/forms.py @@ -108,12 +108,34 @@ class ImportForm(forms.Form): 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") + excel_sheets_as_documents = forms.BooleanField( + required=False, + label="Excel mit mehreren Arbeitsblättern – je Blatt eine Dokumentation", + ) + category = DynamicModelChoiceField( + queryset=DocumentCategory.objects.all(), required=False, + label="Zielordner / Kategorie", + help_text="Für den Mehrblattimport verpflichtend.", + ) + + def __init__(self, *args, user=None, **kwargs): + super().__init__(*args, **kwargs) + if user is not None: + self.fields["document"].queryset = Document.objects.restrict(user, "change") + self.fields["category"].queryset = DocumentCategory.objects.restrict(user, "view") def clean(self): data = super().clean() if data.get("append") and not data.get("document"): self.add_error("document", "Zum Anhängen muss eine Dokumentation gewählt werden.") upload = data.get("file") + if data.get("excel_sheets_as_documents"): + if upload and not upload.name.lower().endswith((".xlsx", ".xlsm")): + self.add_error("file", "Der Mehrblattimport benötigt eine XLSX- oder XLSM-Datei.") + if not data.get("category"): + self.add_error("category", "Bitte einen Zielordner für die erzeugten Dokumentationen auswählen.") + if data.get("append"): + self.add_error("append", "Mehrblattimport und Anhängen können nicht kombiniert werden.") limit = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get("max_import_size_mb", 25) if upload and upload.size > limit * 1024 * 1024: self.add_error("file", f"Die Datei ist größer als {limit} MB.") diff --git a/netbox_documentation/importers.py b/netbox_documentation/importers.py index 7168370..a75b73b 100644 --- a/netbox_documentation/importers.py +++ b/netbox_documentation/importers.py @@ -14,6 +14,21 @@ class ImportResult: warnings: list[str] = field(default_factory=list) +@dataclass +class ImportedImage: + name: str + content: bytes + content_type: str + cell: str = "" + + +@dataclass +class ExcelSheetResult: + title: str + markdown: str + images: list[ImportedImage] = field(default_factory=list) + + def import_document(upload) -> ImportResult: suffix = Path(upload.name).suffix.lower() content = upload.read() @@ -58,6 +73,60 @@ def _xlsx(content): return ImportResult("\n\n".join(sections)) +def import_excel_sheets(upload) -> 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"}: + raise ImportFailure("Der Mehrblattimport unterstützt XLSX- und XLSM-Dateien.") + content = upload.read() + upload.seek(0) + workbook = load_workbook(BytesIO(content), read_only=False, data_only=True) + results = [] + 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)] + while rows and not any(value for value in rows[-1]): + rows.pop() + images = [] + for number, image in enumerate(getattr(sheet, "_images", ()), 1): + image_format = (getattr(image, "format", None) or "png").lower() + if image_format == "jpg": + image_format = "jpeg" + if image_format not in {"png", "jpeg", "gif", "webp"}: + continue + anchor = getattr(image, "anchor", None) + marker = getattr(anchor, "_from", None) + cell = f"{get_column_letter(marker.col + 1)}{marker.row + 1}" if marker else "" + try: + image_content = image._data() + except (AttributeError, OSError, ValueError): + continue + images.append(ImportedImage( + name=f"{slugify_filename(sheet.title)}-{number}.{image_format if image_format != 'jpeg' else 'jpg'}", + content=image_content, content_type=f"image/{image_format}", cell=cell, + )) + 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") + else: + markdown = "" + results.append(ExcelSheetResult(title=sheet.title, markdown=markdown, images=images)) + if not results: + raise ImportFailure("Die Arbeitsmappe enthält keine Daten oder unterstützten Bilder.") + return results + + +def slugify_filename(value): + value = re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip("-.") + return value[:80] or "arbeitsblatt" + + def _pdf(content): from pypdf import PdfReader reader = PdfReader(BytesIO(content)) @@ -70,4 +139,3 @@ def _pdf(content): if not pages: raise ImportFailure("Das PDF enthält keinen extrahierbaren Text. Für Scans ist OCR erforderlich.") return ImportResult("\n\n".join(pages), ["PDF-Layout und Bilder können nicht vollständig übernommen werden."]) - diff --git a/netbox_documentation/templates/netbox_documentation/document_import.html b/netbox_documentation/templates/netbox_documentation/document_import.html index e2b443d..e528ddc 100644 --- a/netbox_documentation/templates/netbox_documentation/document_import.html +++ b/netbox_documentation/templates/netbox_documentation/document_import.html @@ -5,9 +5,12 @@