- 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>
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""Verwaltung der Topologie-Symbole je Gerätetyp / Geräterolle."""
|
||
from django.contrib import messages
|
||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||
from django.http import Http404, HttpResponse
|
||
from django.shortcuts import get_object_or_404, redirect, render
|
||
from django.urls import reverse
|
||
from django.views import View
|
||
|
||
from .forms import TopologySymbolForm
|
||
from .models import TopologySymbol
|
||
from .symbols import HIDDEN, KINDS, builtin_icon
|
||
|
||
APP = "netbox_customer_export"
|
||
|
||
|
||
def _builtin_kinds():
|
||
return [(k, label, reverse(f"plugins:{APP}:symbol_builtin", kwargs={"kind": k})) for k, (label, _) in KINDS.items()]
|
||
|
||
|
||
class BuiltinIconView(LoginRequiredMixin, View):
|
||
"""PNG eines eingebauten Symbols (für Galerie und Vorschau)."""
|
||
|
||
def get(self, request, kind):
|
||
if kind not in KINDS:
|
||
raise Http404
|
||
response = HttpResponse(builtin_icon(kind), content_type="image/png")
|
||
response["Cache-Control"] = "max-age=86400"
|
||
return response
|
||
|
||
|
||
class SymbolListView(PermissionRequiredMixin, View):
|
||
permission_required = f"{APP}.view_topologysymbol"
|
||
|
||
def get(self, request):
|
||
symbols = TopologySymbol.objects.select_related("device_type__manufacturer", "role")
|
||
builtin = {k: url for k, _, url in _builtin_kinds()}
|
||
rows = []
|
||
for s in symbols:
|
||
if s.icon:
|
||
preview = s.icon.url
|
||
elif s.kind and s.kind != HIDDEN:
|
||
preview = builtin[s.kind]
|
||
else:
|
||
preview = None
|
||
rows.append((s, preview))
|
||
return render(
|
||
request,
|
||
f"{APP}/symbol_list.html",
|
||
{"rows": rows, "builtin": _builtin_kinds(), "types": [r for r in rows if r[0].device_type_id], "roles": [r for r in rows if r[0].role_id]},
|
||
)
|
||
|
||
|
||
class SymbolEditView(PermissionRequiredMixin, View):
|
||
template_name = f"{APP}/symbol_edit.html"
|
||
|
||
def get_permission_required(self):
|
||
return [f"{APP}.{'change' if self.kwargs.get('pk') else 'add'}_topologysymbol"]
|
||
|
||
def _obj(self, pk):
|
||
return get_object_or_404(TopologySymbol, pk=pk) if pk else TopologySymbol()
|
||
|
||
def _render(self, request, obj, form):
|
||
return render(request, self.template_name, {"object": obj, "form": form, "builtin": _builtin_kinds()})
|
||
|
||
def get(self, request, pk=None):
|
||
obj = self._obj(pk)
|
||
initial = {}
|
||
for key in ("device_type", "role"): # Vorbelegung z.B. aus dem Link auf der Gerätetyp-Seite
|
||
if request.GET.get(key):
|
||
initial[key] = request.GET[key]
|
||
return self._render(request, obj, TopologySymbolForm(instance=obj, initial=initial))
|
||
|
||
def post(self, request, pk=None):
|
||
obj = self._obj(pk)
|
||
form = TopologySymbolForm(request.POST, request.FILES, instance=obj)
|
||
if form.is_valid():
|
||
obj = form.save()
|
||
messages.success(request, f"Topologie-Symbol für „{obj}“ gespeichert.")
|
||
return redirect(f"plugins:{APP}:symbol_list")
|
||
return self._render(request, obj, form)
|
||
|
||
|
||
class SymbolDeleteView(PermissionRequiredMixin, View):
|
||
permission_required = f"{APP}.delete_topologysymbol"
|
||
|
||
def get(self, request, pk):
|
||
return render(request, f"{APP}/symbol_delete.html", {"object": get_object_or_404(TopologySymbol, pk=pk)})
|
||
|
||
def post(self, request, pk):
|
||
obj = get_object_or_404(TopologySymbol, pk=pk)
|
||
name = str(obj)
|
||
if obj.icon:
|
||
obj.icon.delete(save=False)
|
||
obj.delete()
|
||
messages.success(request, f"Topologie-Symbol für „{name}“ gelöscht – es gilt wieder die automatische Erkennung.")
|
||
return redirect(f"plugins:{APP}:symbol_list")
|