Files
NetBox-Customer-Export/netbox_customer_export/views.py
T
MrBlakeandClaude Opus 5.5 e477aa9b49 feat: network topology diagram with Packet-Tracer-style symbols
- New section "Netzwerk-Topologie": one diagram per site built from
  NetBox cable paths (traced through patch panels), layered top-down by
  cabling (provider > firewall/router > core > access > endpoints),
  barycenter ordering, "2x" for parallel links, provider circuits as
  clouds and devices outside the scope dashed. Labels: management IP,
  model, ports.
- New export format "draw.io": the topology as an editable .drawio file
  (one page per site, symbols embedded).
- 14 built-in symbols drawn with Pillow; automatic mapping from role,
  model and manufacturer, passive gear (patch panels, PDUs, UPS) hidden.
- New TopologySymbol model (migration 0002) with list/edit/delete UI,
  symbol gallery, custom icon upload and a "Topologie-Symbol" button on
  device type and device role pages.

Bump version to 0.4.0 - requires "manage.py migrate".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:12:55 +02:00

98 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.text import slugify
from django.views import View
from .blocks import Document
from .branding import get_branding
from .forms import ExportForm, ScopeSelectForm
from .renderers import MissingDependency, render as render_document
from .scope import SCOPE_MODELS, Scope
from .sections import SECTIONS
class ScopeSelectView(LoginRequiredMixin, View):
template_name = "netbox_customer_export/select.html"
def get(self, request):
return render(request, self.template_name, {"form": ScopeSelectForm()})
def post(self, request):
form = ScopeSelectForm(request.POST)
if form.is_valid():
return redirect("plugins:netbox_customer_export:export", kind=form.cleaned_data["kind"], pk=form.cleaned_data["obj"].pk)
return render(request, self.template_name, {"form": form})
class ExportView(LoginRequiredMixin, View):
def _drawio(self, request, scope, form, opts):
"""Nur die Topologie – als editierbare draw.io-Datei (eine Seite je Standort)."""
from .topology import build_graphs, render_drawio
graphs = build_graphs(scope, opts)
if not graphs:
messages.warning(request, "Keine verkabelten Geräte gefunden – es gibt keine Topologie zu exportieren.")
return self._render_form(request, scope, form)
content = render_drawio(graphs, show_ports="ports" in (opts.get("topo_details") or []))
response = HttpResponse(content, content_type="application/xml")
filename = f"topologie_{slugify(str(scope.obj)) or 'export'}_{timezone.localdate():%Y%m%d}.drawio"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
template_name = "netbox_customer_export/export.html"
def _scope(self, request, kind, pk):
model = SCOPE_MODELS.get(kind)
if model is None:
raise Http404
obj = get_object_or_404(model.objects.restrict(request.user, "view"), pk=pk)
return Scope(kind, obj, request.user)
def _render_form(self, request, scope, form):
grouped = {}
for s in SECTIONS.values():
grouped.setdefault(s.group, []).append(s)
return render(request, self.template_name, {"object": scope.obj, "scope": scope, "form": form, "section_groups": grouped})
def get(self, request, kind, pk):
scope = self._scope(request, kind, pk)
return self._render_form(request, scope, ExportForm(scope=scope))
def post(self, request, kind, pk):
scope = self._scope(request, kind, pk)
form = ExportForm(request.POST, scope=scope)
if not form.is_valid():
return self._render_form(request, scope, form)
opts = form.cleaned_data
if opts["format"] == "drawio":
return self._drawio(request, scope, form, opts)
blocks = []
for key in opts["sections"]:
blocks += SECTIONS[key].func(scope, opts)
doc = Document(
title=opts["title"],
subtitle=opts.get("subtitle", ""),
blocks=blocks,
meta={
"author": opts.get("author") or request.user.get_full_name() or request.user.username,
"date": timezone.localtime().strftime("%d.%m.%Y"),
},
branding=get_branding(opts["branding"]),
)
try:
content, content_type, ext = render_document(doc, opts["format"], request)
except MissingDependency as e:
messages.error(request, str(e))
return self._render_form(request, scope, form)
response = HttpResponse(content, content_type=content_type)
if ext != "html":
filename = f"{slugify(opts['title']) or 'dokumentation'}_{timezone.localdate():%Y%m%d}.{ext}"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response