Topology: - Orthogonal routing: each parent drops to its own bus lane in the gap below it (interval coloring prevents overlaps), color-coded per parent; parallel links drawn thicker. Adjacent same-row links are straight, other same-row links use a lane above the row. - Port labels sit at the target device, stacked per uplink, instead of piling up at line midpoints. - Crossing reduction via repeated down/up barycenter sweeps; rows wrap at 12 devices. - Wide diagrams scale to page width in HTML/PDF instead of being cut off. Symbols: - New built-in symbols: modem/NTU (RAD), radio link, PoE injector, NAS, tape library, UPS/ATS and PBX. - Detection checks the device role first, then model/manufacturer, and covers the common typos "Acces Point", "Controler" and "Libary". Rooms and cable management are hidden; UPS devices are now shown. Removed the draw.io export format. Bump version to 0.5.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
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):
|
|
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
|
|
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
|