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
+76
View File
@@ -0,0 +1,76 @@
from django.core.validators import FileExtensionValidator, MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models
from django.urls import reverse
hex_color = RegexValidator(r"^[0-9A-Fa-f]{6}$", "Hex-Farbe ohne # erwartet, z.B. 1F4E79")
def _color(default, verbose_name):
return models.CharField(max_length=6, default=default, validators=[hex_color], verbose_name=verbose_name)
class BrandingProfile(models.Model):
"""In NetBox gepflegtes Design-Profil (Alternative/Ergänzung zu PLUGINS_CONFIG['brandings'])."""
name = models.CharField(max_length=100, unique=True, verbose_name="Name")
is_default = models.BooleanField(default=False, verbose_name="Standardprofil")
company_name = models.CharField(max_length=200, blank=True, verbose_name="Firmenname")
footer_text = models.CharField(
max_length=300, blank=True, default="Vertraulich – nur für den internen Gebrauch des Kunden", verbose_name="Fußzeile"
)
logo = models.ImageField(upload_to="customer-export/logos/", blank=True, verbose_name="Logo")
primary_color = _color("1F4E79", "Primärfarbe")
accent_color = _color("2E75B6", "Akzentfarbe")
header_text_color = _color("FFFFFF", "Schrift Tabellenkopf")
label_bg_color = _color("DEEAF6", "Hintergrund Beschriftungen")
stripe_color = _color("F3F7FB", "Zebrastreifen")
font_family = models.CharField(max_length=100, default="Calibri", verbose_name="Schriftart")
font_size = models.PositiveSmallIntegerField(
default=10, validators=[MinValueValidator(7), MaxValueValidator(16)], verbose_name="Schriftgröße (pt)"
)
docx_template = models.FileField(
upload_to="customer-export/templates/",
blank=True,
validators=[FileExtensionValidator(["docx", "dotx"])],
verbose_name="Word-Vorlage",
)
docx_table_style = models.CharField(max_length=100, blank=True, verbose_name="Tabellenformatvorlage (Word)")
extra_css = models.TextField(blank=True, verbose_name="Zusätzliches CSS (PDF/HTML)")
last_updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("-is_default", "name")
verbose_name = "Branding-Profil"
verbose_name_plural = "Branding-Profile"
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("plugins:netbox_customer_export:branding_edit", kwargs={"pk": self.pk})
def save(self, *args, **kwargs):
for field in ("primary_color", "accent_color", "header_text_color", "label_bg_color", "stripe_color"):
setattr(self, field, getattr(self, field).lstrip("#").upper())
super().save(*args, **kwargs)
if self.is_default:
BrandingProfile.objects.exclude(pk=self.pk).filter(is_default=True).update(is_default=False)
def as_branding(self):
"""Dict im Format von branding.DEFAULT_BRANDING."""
return {
"name": self.name,
"company_name": self.company_name,
"footer_text": self.footer_text,
"logo_path": self.logo.path if self.logo else None,
"primary_color": self.primary_color,
"accent_color": self.accent_color,
"header_text_color": self.header_text_color,
"label_bg_color": self.label_bg_color,
"stripe_color": self.stripe_color,
"font_family": self.font_family,
"font_size": self.font_size,
"docx_template": self.docx_template.path if self.docx_template else None,
"docx_table_style": self.docx_table_style,
"extra_css": self.extra_css,
}