feat: manage branding profiles in the NetBox UI

New BrandingProfile model (migration 0001) with logo upload, color
pickers, font, Word template upload, table style and extra CSS, plus
list/edit/delete views, an HTML preview and a menu entry with add
button. Access is controlled via the model's Django permissions.

Profiles from the database are offered first in the export form (the
default profile preselected); profiles from PLUGINS_CONFIG still work.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 13:21:11 +02:00
co-authored by Claude Opus 5.5
parent 0c6bebe2e3
commit f24e42a516
15 changed files with 491 additions and 12 deletions
+42
View File
@@ -5,6 +5,7 @@ from tenancy.models import Tenant, TenantGroup
from utilities.forms.fields import DynamicModelChoiceField
from .branding import get_brandings
from .models import BrandingProfile
from .sections import DEFAULT_DEVICE_COLUMNS, DEVICE_COLUMNS, SECTIONS
FORMAT_CHOICES = (("pdf", "PDF"), ("docx", "Word (.docx)"), ("html", "HTML-Vorschau"))
@@ -85,3 +86,44 @@ class ExportForm(forms.Form):
self.fields["subtitle"].initial = f"{scope.label}: {scope.obj}"
if not get_plugin_config("netbox_customer_export", "allow_secrets"):
del self.fields["include_secrets"]
COLOR_FIELDS = ("primary_color", "accent_color", "header_text_color", "label_bg_color", "stripe_color")
class ColorInput(forms.TextInput):
"""<input type="color">: Anzeige mit #, gespeichert wird ohne #."""
input_type = "color"
def format_value(self, value):
return f"#{value.lstrip('#')}" if value else None
def value_from_datadict(self, data, files, name):
value = super().value_from_datadict(data, files, name)
return value.lstrip("#").upper() if value else value
class BrandingProfileForm(forms.ModelForm):
class Meta:
model = BrandingProfile
exclude = ("last_updated",)
widgets = {
**{f: ColorInput(attrs={"class": "form-control form-control-color"}) for f in COLOR_FIELDS},
"extra_css": forms.Textarea(attrs={"rows": 4, "class": "form-control font-monospace"}),
}
help_texts = {
"is_default": "Wird im Exportformular vorausgewählt.",
"logo": "PNG oder JPG, erscheint auf dem Deckblatt.",
"docx_template": ".docx oder .dotx: Formatvorlagen, Kopf-/Fußzeile und Ränder werden übernommen.",
"docx_table_style": "Name wie in Word angezeigt, z.B. Gitternetztabelle 4 – Akzent 1. Leer = eigene Einfärbung.",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
w = field.widget
if isinstance(w, forms.CheckboxInput):
w.attrs.setdefault("class", "form-check-input")
else:
w.attrs.setdefault("class", "form-control")