feat: add offline SVG export for rack elevations

Adds a "SVG offline" download button on the rack detail page. It
reuses NetBox's own rack-elevation REST endpoint to render the SVG,
then strips embedded <a>/xlink:href links (which point at internal,
relative NetBox URLs and are useless once the file is saved) so the
result renders identically when opened standalone in any SVG viewer.

Gated by the new offline_svg_export_enabled plugin setting (on by
default).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 14:22:04 +02:00
co-authored by Claude Sonnet 5
parent 35ca661ed0
commit 3768417be6
9 changed files with 186 additions and 6 deletions
+36 -2
View File
@@ -1,10 +1,10 @@
from dcim.models import Device, Module
from dcim.models import Device, Module, Rack
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import SuspiciousFileOperation, ValidationError
from django.db import DatabaseError, transaction
from django.http import Http404, HttpResponseBadRequest, JsonResponse
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
@@ -33,8 +33,10 @@ from .navigation_helpers import (
from .runtime import (
clear_runtime_settings_cache,
navigation_customization_enabled,
offline_svg_export_enabled,
tenant_filter_enabled,
)
from .svg_export import strip_svg_hyperlinks
def _safe_return_url(request, default_name="home"):
@@ -364,3 +366,35 @@ class SelectTenantView(LoginRequiredMixin, View):
return HttpResponseBadRequest("Ungültiger Mandantenfilter.")
return redirect(_safe_return_url(request))
class RackElevationOfflineSVGView(LoginRequiredMixin, View):
"""Serve a rack elevation SVG stripped of hyperlinks, for offline use."""
def get(self, request, pk):
if not offline_svg_export_enabled():
return HttpResponseBadRequest("Der Offline-SVG-Export ist deaktiviert.")
rack = get_object_or_404(Rack.objects.restrict(request.user, "view"), pk=pk)
face = request.GET.get("face", "front")
if face not in ("front", "rear"):
return HttpResponseBadRequest("Ungültige Rack-Seite.")
from dcim.api.views import RackViewSet
from rest_framework.test import APIRequestFactory
factory = APIRequestFactory()
api_request = factory.get(
f"/api/dcim/racks/{rack.pk}/elevation/",
{"render": "svg", "face": face},
)
api_request.user = request.user
drf_view = RackViewSet.as_view({"get": "elevation"})
response = drf_view(api_request, pk=rack.pk)
response.render()
offline_svg = strip_svg_hyperlinks(response.content)
filename = f"{rack.name}-{face}-offline.svg".replace(" ", "_")
http_response = HttpResponse(offline_svg, content_type="image/svg+xml")
http_response["Content-Disposition"] = f'attachment; filename="{filename}"'
return http_response