NetBox Customer Export plugin with configurable branding
Plugin for NetBox 4.6.8–4.6.99 that exports customer documentation (PDF, Word, HTML preview) for a site, location, tenant or tenant group. Technicians pick sections, device roles and columns before exporting. Branding profiles (colors, font, logo, footer, Word template incl. .dotx, table style, extra CSS) are configurable via PLUGINS_CONFIG and selectable per export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Neutrales Dokumentmodell, das sowohl der HTML/PDF- als auch der Word-Renderer versteht."""
|
||||
import base64
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from django.utils.text import slugify
|
||||
|
||||
EMPTY = "—"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Block:
|
||||
kind: str # heading | paragraph | table | kv | images | pagebreak
|
||||
level: int = 0
|
||||
text: str = ""
|
||||
headers: list = field(default_factory=list)
|
||||
rows: list = field(default_factory=list)
|
||||
images: list = field(default_factory=list)
|
||||
anchor: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Document:
|
||||
title: str
|
||||
subtitle: str
|
||||
blocks: list
|
||||
meta: dict
|
||||
branding: dict = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def toc(self):
|
||||
return [b for b in self.blocks if b.kind == "heading" and b.level <= 2]
|
||||
|
||||
|
||||
_anchor_counter = {"n": 0}
|
||||
|
||||
|
||||
def H(level, text):
|
||||
_anchor_counter["n"] += 1
|
||||
return Block("heading", level=level, text=str(text), anchor=f"s{_anchor_counter['n']}-{slugify(str(text))[:40]}")
|
||||
|
||||
|
||||
def P(text):
|
||||
return Block("paragraph", text=str(text))
|
||||
|
||||
|
||||
def T(headers, rows):
|
||||
return Block("table", headers=list(headers), rows=[[fmt(c) for c in r] for r in rows])
|
||||
|
||||
|
||||
def KV(pairs, skip_empty=True):
|
||||
rows = [[k, fmt(v)] for k, v in pairs if not (skip_empty and v in (None, "", [], EMPTY))]
|
||||
return Block("kv", rows=rows)
|
||||
|
||||
|
||||
def IMAGES(images):
|
||||
"""images: Liste von (svg_string, caption)."""
|
||||
out = []
|
||||
for svg, caption in images:
|
||||
data = base64.b64encode(svg.encode("utf-8")).decode("ascii")
|
||||
out.append({"svg": svg, "caption": caption, "data_uri": f"data:image/svg+xml;base64,{data}"})
|
||||
return Block("images", images=out)
|
||||
|
||||
|
||||
def PAGEBREAK():
|
||||
return Block("pagebreak")
|
||||
|
||||
|
||||
def fmt(value):
|
||||
if value is None or value == "":
|
||||
return EMPTY
|
||||
if isinstance(value, bool):
|
||||
return "Ja" if value else "Nein"
|
||||
if isinstance(value, (list, tuple, set)) or hasattr(value, "all"):
|
||||
items = value.all() if hasattr(value, "all") else value
|
||||
return ", ".join(str(i) for i in items) or EMPTY
|
||||
return str(value)
|
||||
|
||||
|
||||
def get(obj, path):
|
||||
"""Dotted-getattr, bevorzugt get_<feld>_display() für Choice-Felder."""
|
||||
for part in path.split("."):
|
||||
if obj is None:
|
||||
return None
|
||||
display = getattr(obj, f"get_{part}_display", None)
|
||||
obj = display() if callable(display) else getattr(obj, part, None)
|
||||
return obj
|
||||
Reference in New Issue
Block a user