From 0c6bebe2e3ad818361f5160eab6a811579c4803c Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 23 Sep 2026 13:17:09 +0200 Subject: [PATCH] feat: redesign export form Two-column layout with format toggle buttons, section tiles with short descriptions and "Alle/Keine" per group, device roles/columns and rack faces as toggle pills, option switches, device/rack cards dimmed when their section is deselected and a sticky action bar. Bump version to 0.1.1 (includes the rack elevation fix). Co-Authored-By: Claude Opus 5.5 --- netbox_customer_export/__init__.py | 2 +- netbox_customer_export/forms.py | 4 + netbox_customer_export/sections.py | 31 ++-- .../netbox_customer_export/_checkboxes.html | 7 - .../netbox_customer_export/_pills.html | 6 + .../netbox_customer_export/export.html | 173 +++++++++++++----- pyproject.toml | 2 +- 7 files changed, 157 insertions(+), 68 deletions(-) delete mode 100644 netbox_customer_export/templates/netbox_customer_export/_checkboxes.html create mode 100644 netbox_customer_export/templates/netbox_customer_export/_pills.html diff --git a/netbox_customer_export/__init__.py b/netbox_customer_export/__init__.py index 94cf8ef..b0910bf 100644 --- a/netbox_customer_export/__init__.py +++ b/netbox_customer_export/__init__.py @@ -5,7 +5,7 @@ class CustomerExportConfig(PluginConfig): name = "netbox_customer_export" verbose_name = "Kunden-Export" description = "Kundendokumentation als PDF oder Word exportieren" - version = "0.1.0" + version = "0.1.1" author = "Claude" base_url = "customer-export" min_version = "4.6.8" diff --git a/netbox_customer_export/forms.py b/netbox_customer_export/forms.py index 73db24f..bdd82e0 100644 --- a/netbox_customer_export/forms.py +++ b/netbox_customer_export/forms.py @@ -71,6 +71,10 @@ class ExportForm(forms.Form): for field in self.fields.values(): if isinstance(field.widget, (forms.CheckboxInput, forms.CheckboxSelectMultiple)): field.widget.attrs["class"] = "form-check-input" + elif isinstance(field.widget, forms.Select): + field.widget.attrs["class"] = "form-select" + elif isinstance(field.widget, forms.TextInput): + field.widget.attrs["class"] = "form-control" self.fields["branding"].choices = [(k, b["name"]) for k, b in get_brandings().items()] if scope is not None: role_ids = scope.devices().values("role") diff --git a/netbox_customer_export/sections.py b/netbox_customer_export/sections.py index a2800ed..37733e8 100644 --- a/netbox_customer_export/sections.py +++ b/netbox_customer_export/sections.py @@ -19,14 +19,15 @@ class Section: group: str default: bool func: callable + hint: str = "" SECTIONS = OrderedDict() -def section(key, label, group, default=True): +def section(key, label, group, default=True, hint=""): def deco(func): - SECTIONS[key] = Section(key, label, group, default, func) + SECTIONS[key] = Section(key, label, group, default, func, hint) return func return deco @@ -75,7 +76,7 @@ def _empty(opts, blocks, text="Keine Einträge vorhanden."): # --- Abschnitte ------------------------------------------------------------- -@section("overview", "Übersicht & Standortdaten", "Allgemein") +@section("overview", "Übersicht & Standortdaten", "Allgemein", hint="Kennzahlen, Adressen, Lokationsbaum") def overview(scope, opts): blocks = [H(1, "Übersicht")] counts = [ @@ -118,7 +119,7 @@ def overview(scope, opts): return blocks -@section("contacts", "Ansprechpartner", "Allgemein") +@section("contacts", "Ansprechpartner", "Allgemein", hint="Kontakte an Standorten, Lokationen, Mandanten") def contacts(scope, opts): ContactAssignment = apps.get_model("tenancy", "ContactAssignment") targets = [ @@ -143,7 +144,7 @@ def contacts(scope, opts): return blocks -@section("racks", "Racks (nach Standort & Lokation, inkl. Ansichten)", "Infrastruktur") +@section("racks", "Racks", "Infrastruktur", hint="Nach Standort & Lokation, mit Rack-Ansicht") def racks(scope, opts): qs = ( scope.racks() @@ -211,7 +212,7 @@ def racks(scope, opts): return blocks -@section("devices", "Geräte (nach Geräterolle – Auswahl unten)", "Infrastruktur") +@section("devices", "Geräte", "Infrastruktur", hint="Tabellen je Geräterolle") def devices(scope, opts): blocks = [H(1, "Geräte")] base = scope.devices().select_related( @@ -234,7 +235,7 @@ def devices(scope, opts): return blocks if found else _empty(opts, blocks) -@section("power", "Stromversorgung (Power Feeds)", "Infrastruktur", default=False) +@section("power", "Stromversorgung", "Infrastruktur", default=False, hint="Power Panels & Feeds") def power(scope, opts): qs = scope.power_feeds().select_related("power_panel", "rack").order_by("power_panel__name", "name") blocks = [H(1, "Stromversorgung")] @@ -249,7 +250,7 @@ def power(scope, opts): return blocks -@section("vms", "Virtuelle Maschinen", "Infrastruktur", default=False) +@section("vms", "Virtuelle Maschinen", "Infrastruktur", default=False, hint="Cluster, Ressourcen, IP") def vms(scope, opts): qs = scope.virtual_machines().select_related("cluster", "role", "platform", "primary_ip4", "primary_ip6").order_by("cluster__name", "name") blocks = [H(1, "Virtuelle Maschinen")] @@ -264,7 +265,7 @@ def vms(scope, opts): return blocks -@section("vrfs", "VRFs", "Netzwerk") +@section("vrfs", "VRFs", "Netzwerk", hint="Inkl. Route Targets") def vrfs(scope, opts): qs = scope.vrfs().select_related("tenant").prefetch_related("import_targets", "export_targets").order_by("name") blocks = [H(1, "VRFs")] @@ -279,7 +280,7 @@ def vrfs(scope, opts): return blocks -@section("prefixes", "IP-Netze / Präfixe (nach VRF)", "Netzwerk") +@section("prefixes", "IP-Netze", "Netzwerk", hint="Präfixe gruppiert nach VRF") def prefixes(scope, opts): qs = scope.prefixes().select_related("vrf", "vlan", "role", "tenant").order_by("vrf__name", "prefix") blocks = [H(1, "IP-Netze")] @@ -296,7 +297,7 @@ def prefixes(scope, opts): return blocks -@section("vlans", "VLANs", "Netzwerk") +@section("vlans", "VLANs", "Netzwerk", hint="VID, Name, Gruppe, Rolle") def vlans(scope, opts): qs = scope.vlans().select_related("group", "site", "role", "tenant").order_by("group__name", "vid") blocks = [H(1, "VLANs")] @@ -311,7 +312,7 @@ def vlans(scope, opts): return blocks -@section("ips", "IP-Adressen (ausführlich)", "Netzwerk", default=False) +@section("ips", "IP-Adressen", "Netzwerk", default=False, hint="Ausführlich – kann lang werden") def ips(scope, opts): qs = scope.ip_addresses().select_related("vrf", "tenant").prefetch_related("assigned_object").order_by("vrf__name", "address") blocks = [H(1, "IP-Adressen")] @@ -328,7 +329,7 @@ def ips(scope, opts): return blocks -@section("wireless", "WLANs / SSIDs", "Netzwerk", default=False) +@section("wireless", "WLANs", "Netzwerk", default=False, hint="SSIDs, Authentifizierung") def wireless(scope, opts): qs = scope.wireless_lans().select_related("group", "vlan").order_by("ssid") blocks = [H(1, "WLANs")] @@ -348,7 +349,7 @@ def wireless(scope, opts): return blocks -@section("circuits", "Leitungen / Provider", "Netzwerk", default=False) +@section("circuits", "Leitungen", "Netzwerk", default=False, hint="Provider, Leitungs-ID, Bandbreite") def circuits(scope, opts): qs = scope.circuits().select_related("provider", "type", "tenant").order_by("provider__name", "cid") blocks = [H(1, "Leitungen")] @@ -381,7 +382,7 @@ DEFAULT_LICENSE_SOURCES = [ ] -@section("licenses", "Lizenzen", "Verträge & Lizenzen") +@section("licenses", "Lizenzen", "Verträge & Lizenzen", hint="Aus Lizenz-Plugin (z.B. netbox-lifecycle)") def licenses(scope, opts): sources = get_plugin_config("netbox_customer_export", "license_sources") or DEFAULT_LICENSE_SOURCES blocks = [H(1, "Lizenzen")] diff --git a/netbox_customer_export/templates/netbox_customer_export/_checkboxes.html b/netbox_customer_export/templates/netbox_customer_export/_checkboxes.html deleted file mode 100644 index 2d0cfa4..0000000 --- a/netbox_customer_export/templates/netbox_customer_export/_checkboxes.html +++ /dev/null @@ -1,7 +0,0 @@ -
- {% for cb in field %} -
-
{{ cb.tag }}
-
- {% empty %}
Keine Einträge im Scope.
{% endfor %} -
diff --git a/netbox_customer_export/templates/netbox_customer_export/_pills.html b/netbox_customer_export/templates/netbox_customer_export/_pills.html new file mode 100644 index 0000000..9ba678a --- /dev/null +++ b/netbox_customer_export/templates/netbox_customer_export/_pills.html @@ -0,0 +1,6 @@ +
+ {% for cb in field %} + + + {% empty %}Keine Einträge im Scope.{% endfor %} +
diff --git a/netbox_customer_export/templates/netbox_customer_export/export.html b/netbox_customer_export/templates/netbox_customer_export/export.html index a94aec9..3358f41 100644 --- a/netbox_customer_export/templates/netbox_customer_export/export.html +++ b/netbox_customer_export/templates/netbox_customer_export/export.html @@ -1,66 +1,151 @@ {% extends 'generic/_base.html' %} {% load form_helpers %} -{% block title %}Kundendoku: {{ object }}{% endblock %} -{% block subtitle %}
{{ scope.label }}
{% endblock %} +{% block title %}Kundendokumentation: {{ object }}{% endblock %} +{% block subtitle %} +
{{ scope.label }} {{ object }}
+{% endblock %} {% block content %} -
+ + + {% csrf_token %} {% if form.errors %}
{{ form.errors }}
{% endif %} -
-
-
-

Dokument

+ +
+ {# --- links: Dokument --- #} +
+
+

Dokument

- {% render_field form.format %} - {% render_field form.title %} - {% render_field form.subtitle %} - {% render_field form.author %} - {% if form.branding.field.choices|length > 1 %}{% render_field form.branding %}{% else %}{% endif %} -
-
-
-

Inhalte

-
- {% for group, sections in section_groups.items %} -
{{ group }}
- {% for cb in form.sections %}{% for s in sections %}{% if s.key == cb.data.value %} -
{{ cb.tag }}
- {% endif %}{% endfor %}{% endfor %} - {% endfor %} -
-
{{ form.include_empty }}
+
Format
+
+ {% for value, label in form.format.field.choices %} + + + {% endfor %} +
+
{{ form.title }}
+
{{ form.subtitle }}
+
{{ form.author }}
Leer = dein Benutzername
+ {% if form.branding.field.choices|length > 1 %} +
{{ form.branding }}
+ {% else %} + + {% endif %} + +
Optionen
+ {% if form.include_secrets %} -
{{ form.include_secrets }}
+ {% endif %}
-
-
-

Geräte

+ + {# --- rechts: Inhalte + Detailoptionen --- #} +
+
+

Inhalte

-
Geräterollen
- {% include 'netbox_customer_export/_checkboxes.html' with field=form.roles cols=4 %} -
Spalten
- {% include 'netbox_customer_export/_checkboxes.html' with field=form.device_columns cols=4 %} -
{{ form.group_by_site }}
+ {% for group, sections in section_groups.items %} +
+
+
{{ group }}
+ Alle · Keine +
+
+ {% for cb in form.sections %}{% for s in sections %}{% if s.key == cb.data.value %} + + {% endif %}{% endfor %}{% endfor %} +
+
+ {% endfor %}
-
-

Racks

-
-
{{ form.rack_svg }}
-
{{ form.rack_devices }}
-
{{ form.rack_pagebreak }}
-
Seiten
- {% include 'netbox_customer_export/_checkboxes.html' with field=form.rack_faces cols=6 %} + +
+
+
+

Geräte

+
+
+
Geräterollen
Alle · Keine
+ {% include 'netbox_customer_export/_pills.html' with field=form.roles %} +
+
Spalten
+ {% include 'netbox_customer_export/_pills.html' with field=form.device_columns %} + +
+
+
+
+
+

Racks

+
+ +
Seiten
+ {% include 'netbox_customer_export/_pills.html' with field=form.rack_faces %} +
Weiteres
+ + +
+
-
+ +
Abbrechen - +
+ + {% endblock %} diff --git a/pyproject.toml b/pyproject.toml index feb4532..7decbc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netbox-customer-export" -version = "0.1.0" +version = "0.1.1" description = "NetBox-Plugin: Kundendokumentation (PDF/Word) für Standorte, Lokationen, Mandanten und Mandantengruppen" readme = "README.md" requires-python = ">=3.10"