feat: optional device images in rack elevations, HTML preview in new tab
- New export option "Gerätebilder anzeigen": the SVG is rendered with include_images and the /media/ image links are replaced by embedded data URIs, since Word, WeasyPrint and SVG-as-image cannot load login-protected files. The Pillow PNG renderer draws the front/rear images too, with the device name on a dark label. - Custom rack_svg_renderer adapters receive images=..., older adapters without that parameter keep working. - The HTML preview now opens in a new browser tab. Bump version to 0.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"""Rack-Ansichten offline (ohne HTTP-Aufruf der API) als SVG erzeugen."""
|
||||
import base64
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
from django.utils.module_loading import import_string
|
||||
from netbox.plugins import get_plugin_config
|
||||
@@ -13,17 +15,52 @@ LEGEND_WIDTH = 30
|
||||
MARGIN_WIDTH = 15
|
||||
|
||||
|
||||
def render_rack_svg(rack, face, user):
|
||||
def device_image(device_type, face):
|
||||
"""Bilddatei (FieldFile) des Gerätetyps für die Seite oder None."""
|
||||
image = getattr(device_type, "front_image" if face == "front" else "rear_image", None)
|
||||
return image if image else None
|
||||
|
||||
|
||||
def _data_uri(image):
|
||||
mime = mimetypes.guess_type(image.name)[0] or "image/png"
|
||||
with image.open("rb") as f:
|
||||
return f"data:{mime};base64,{base64.b64encode(f.read()).decode('ascii')}"
|
||||
|
||||
|
||||
def _inline_images(svg, devices):
|
||||
"""Ersetzt die /media/…-Links der Gerätebilder durch eingebettete Daten.
|
||||
|
||||
Word, WeasyPrint und <img src="data:image/svg+xml…"> laden keine externen (login-geschützten) Dateien nach.
|
||||
"""
|
||||
seen = set()
|
||||
for d in devices:
|
||||
for face in ("front", "rear"):
|
||||
image = device_image(d.device_type, face)
|
||||
if not image or image.name in seen:
|
||||
continue
|
||||
seen.add(image.name)
|
||||
try:
|
||||
svg = svg.replace(f'"{image.url}"', f'"{_data_uri(image)}"')
|
||||
except OSError:
|
||||
logger.warning("Gerätebild %s nicht lesbar", image.name)
|
||||
return svg
|
||||
|
||||
|
||||
def render_rack_svg(rack, face, user, images=False, devices=()):
|
||||
"""Gibt (svg, fehler) zurück – genau einer der beiden Werte ist None.
|
||||
|
||||
Ist ``rack_svg_renderer`` gesetzt (z.B. der Offline-Renderer aus Netbox-Utilities, der mehrere
|
||||
Geräte pro HE korrekt darstellt), wird dieser aufgerufen: ``fn(rack=..., face=..., user=...)``.
|
||||
Geräte pro HE korrekt darstellt), wird dieser aufgerufen: ``fn(rack=..., face=..., user=..., images=...)``.
|
||||
Andernfalls wird NetBox' eigener RackElevationSVG direkt in-process genutzt.
|
||||
"""
|
||||
try:
|
||||
renderer = get_plugin_config("netbox_customer_export", "rack_svg_renderer")
|
||||
if renderer:
|
||||
svg = import_string(renderer)(rack=rack, face=face, user=user)
|
||||
fn = import_string(renderer)
|
||||
try:
|
||||
svg = fn(rack=rack, face=face, user=user, images=images)
|
||||
except TypeError: # ältere Adapter ohne images-Parameter
|
||||
svg = fn(rack=rack, face=face, user=user)
|
||||
else:
|
||||
from dcim.svg import RackElevationSVG
|
||||
|
||||
@@ -36,10 +73,13 @@ def render_rack_svg(rack, face, user):
|
||||
legend_width=LEGEND_WIDTH,
|
||||
margin_width=MARGIN_WIDTH,
|
||||
user=user,
|
||||
include_images=False,
|
||||
include_images=images,
|
||||
base_url="",
|
||||
).render(face)
|
||||
return (svg.tostring() if hasattr(svg, "tostring") else str(svg)), None
|
||||
svg = svg.tostring() if hasattr(svg, "tostring") else str(svg)
|
||||
if images:
|
||||
svg = _inline_images(svg, devices)
|
||||
return svg, None
|
||||
except Exception as e: # noqa: BLE001 – ein defektes Rack soll nicht den ganzen Export abbrechen
|
||||
logger.exception("Rack-SVG für %s (%s) fehlgeschlagen", rack, face)
|
||||
return None, f"{type(e).__name__}: {e}"
|
||||
|
||||
Reference in New Issue
Block a user