Files
NetBox-Customer-Export/netbox_customer_export/blocks.py
T
MrBlakeandClaude Opus 5.5 c37626aff1 fix: rack elevations missing in all export formats
RackElevationSVG falls back to RACK_ELEVATION_DEFAULT_* config
parameters, which raise AttributeError on the target NetBox version.
Pass the dimensions explicitly instead.

Add a Pillow-based PNG rack renderer (role colors, unit legend,
full-depth devices shown dimmed from the other side). It is used for
Word when cairosvg is unavailable and replaces a failed SVG in all
formats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:15:34 +02:00

92 lines
2.5 KiB
Python

"""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 oder None, caption, png_bytes_oder_None)."""
out = []
for svg, caption, png in images:
if svg:
data_uri = "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode("ascii")
elif png:
data_uri = "data:image/png;base64," + base64.b64encode(png).decode("ascii")
else:
continue
out.append({"svg": svg, "png": png, "caption": caption, "data_uri": data_uri})
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