feat: add German localization

Internationalize plugin navigation, forms, tables, model labels, detail templates, choices, and validation messages. Ship a compiled German Django message catalog and bump the plugin version to 1.10.0.
This commit is contained in:
2026-07-24 15:41:35 +02:00
parent 7ca670c717
commit 5887129546
20 changed files with 499 additions and 158 deletions
+1
View File
@@ -5,6 +5,7 @@
### Added
* Optional tenant group and tenant assignments for software licenses
* German translation for navigation, forms, tables, detail views, choices, and validation messages
### Changed
+4 -1
View File
@@ -5,6 +5,9 @@ Lizenzen können optional einer NetBox-Mandantengruppe und einem Mandanten zugeo
Diese Variante ist für **NetBox 4.6.5** vorgesehen.
Die Oberfläche ist auf Englisch und Deutsch verfügbar. Die Sprache richtet sich nach der in NetBox für den
Benutzer ausgewählten Sprache.
## Funktionen
- Softwareprodukte und Hersteller verwalten
@@ -54,7 +57,7 @@ Die installierte Version kann anschließend geprüft werden:
/opt/netbox/venv/bin/pip show netbox-slm
```
Für diese Variante muss dort mindestens Version `1.9.1` stehen.
Für diese Variante muss dort mindestens Version `1.10.0` stehen.
### 3. Plugin in NetBox aktivieren
+4 -3
View File
@@ -15,15 +15,16 @@ limitations under the License.
"""
from netbox.plugins import PluginConfig
from django.utils.translation import gettext_lazy as _
__version__ = "1.9.1"
__version__ = "1.10.0"
class SLMConfig(PluginConfig):
name = "netbox_slm"
verbose_name = "Software Lifecycle Management"
verbose_name = _("Software Lifecycle Management")
version = __version__
description = "Software Lifecycle Management Netbox Plugin."
description = _("Software Lifecycle Management NetBox Plugin.")
author = "ICTU"
author_email = "open-source-projects@ictu.nl"
base_url = "slm"
+2 -1
View File
@@ -1,5 +1,6 @@
from rest_framework import serializers
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
from django.utils.translation import gettext_lazy as _
from netbox.api.serializers import NetBoxModelSerializer
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
@@ -46,7 +47,7 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
tenant = attrs.get("tenant", getattr(self.instance, "tenant", None))
if tenant_group and tenant and tenant.group_id != tenant_group.pk:
raise serializers.ValidationError(
{"tenant": "The selected tenant does not belong to the selected tenant group."}
{"tenant": _("The selected tenant does not belong to the selected tenant group.")}
)
return attrs
+8 -7
View File
@@ -1,4 +1,5 @@
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
from dcim.models import Device, Manufacturer
@@ -55,7 +56,7 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(),
label="Software Product",
label=_("Software Product"),
)
class Meta:
@@ -82,12 +83,12 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
device_id = ModelMultipleChoiceFilter(queryset=Device.objects.all())
virtualmachine_id = ModelMultipleChoiceFilter(
queryset=VirtualMachine.objects.all(),
label="Virtual Machine",
label=_("Virtual Machine"),
)
cluster_id = ModelMultipleChoiceFilter(queryset=Cluster.objects.all())
software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(),
label="Software Product",
label=_("Software Product"),
)
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
@@ -114,17 +115,17 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
name = CharFilter(lookup_expr="icontains")
description = CharFilter(lookup_expr="icontains")
type = CharFilter(lookup_expr="icontains")
spdx_expression = CharFilter(lookup_expr="icontains", label="SPDX expression")
spdx_expression = CharFilter(lookup_expr="icontains", label=_("SPDX expression"))
stored_location = CharFilter(lookup_expr="icontains")
software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(),
label="Software Product",
label=_("Software Product"),
)
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all())
tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label="Tenant Group")
tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label="Tenant")
tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label=_("Tenant Group"))
tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label=_("Tenant"))
class Meta:
model = SoftwareLicense
+22 -19
View File
@@ -1,5 +1,6 @@
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import (
@@ -31,9 +32,9 @@ class SoftwareLicenseForm(NetBoxModelForm):
"software_product",
"version",
"installation",
name="Software License",
name=_("Software License"),
),
FieldSet("tenant_group", "tenant", name="Tenancy"),
FieldSet("tenant_group", "tenant", name=_("Tenancy")),
FieldSet(
"type",
"spdx_expression",
@@ -43,12 +44,12 @@ class SoftwareLicenseForm(NetBoxModelForm):
"expiration_date",
"support",
"license_amount",
name="License Details",
name=_("License Details"),
),
FieldSet("tags", name="Tags"),
FieldSet("tags", name=_("Tags")),
)
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression")
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label=_("SPDX expression"))
stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker())
expiration_date = DateField(required=False, widget=DatePicker())
@@ -56,7 +57,7 @@ class SoftwareLicenseForm(NetBoxModelForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
label="Software Product",
label=_("Software Product"),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
@@ -75,13 +76,13 @@ class SoftwareLicenseForm(NetBoxModelForm):
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label="Tenant Group",
label=_("Tenant Group"),
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
query_params={"group_id": "$tenant_group"},
label="Tenant",
label=_("Tenant"),
)
class Meta:
@@ -132,40 +133,42 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
name = CharField(required=False)
description = CharField(required=False)
type = CharField(required=False)
spdx_expression = CharField(required=False, label="SPDX expression")
spdx_expression = CharField(required=False, label=_("SPDX expression"))
stored_location = CharField(required=False)
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
label=_("Software Product"),
)
version_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
label="Version",
label=_("Version"),
)
installation_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
required=False,
label="Installation",
label=_("Installation"),
)
tenant_group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label="Tenant Group",
label=_("Tenant Group"),
)
tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
query_params={"group_id": "$tenant_group_id"},
label="Tenant",
label=_("Tenant"),
)
class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
support = NullBooleanField(required=False, help_text="Support (values other than 'True' and 'False' are ignored)")
support = NullBooleanField(
required=False, help_text=_("Support (values other than 'True' and 'False' are ignored)")
)
class Meta:
model = SoftwareLicense
@@ -226,7 +229,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
comments = CommentField()
type = CharField(required=False)
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression")
spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label=_("SPDX expression"))
stored_location = CharField(required=False)
stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker())
@@ -237,7 +240,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
label=_("Software Product"),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
@@ -256,11 +259,11 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label="Tenant Group",
label=_("Tenant Group"),
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
query_params={"group_id": "$tenant_group"},
label="Tenant",
label=_("Tenant"),
)
+2 -1
View File
@@ -1,4 +1,5 @@
from django.forms import CharField
from django.utils.translation import gettext_lazy as _
from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
@@ -46,7 +47,7 @@ class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label="Manufacturer",
label=_("Manufacturer"),
)
@@ -1,5 +1,6 @@
from django.forms import ValidationError
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
@@ -22,13 +23,13 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label="Virtual Machine",
label=_("Virtual Machine"),
)
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
label="Software Product",
label=_("Software Product"),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
@@ -54,8 +55,8 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
software_product = self.cleaned_data["software_product"]
if version not in software_product.softwareproductversion_set.all():
raise ValidationError(
f"Version '{version}' doesn't exist on {software_product}, make sure you've "
f"selected a compatible version or first select the software product."
_("Version '%(version)s' does not belong to %(product)s. Select a compatible version.")
% {"version": version, "product": software_product}
)
return version
@@ -73,27 +74,27 @@ class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
label="Device",
label=_("Device"),
)
virtualmachine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label="Virtual Machine",
label=_("Virtual Machine"),
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label="Cluster",
label=_("Cluster"),
)
software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
label=_("Software Product"),
)
version_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False,
label="Version",
label=_("Version"),
)
@@ -122,13 +123,13 @@ class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label="Virtual Machine",
label=_("Virtual Machine"),
)
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
label=_("Software Product"),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
+8 -5
View File
@@ -1,5 +1,6 @@
from django.forms import DateField, CharField, MultipleChoiceField
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
@@ -23,7 +24,7 @@ class SoftwareProductVersionForm(NetBoxModelForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
label="Software Product",
label=_("Software Product"),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
)
@@ -64,17 +65,19 @@ class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label="Manufacturer",
label=_("Manufacturer"),
)
software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
label=_("Software Product"),
)
class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm):
release_type = CharField(help_text=f"Release type (possible values: {SoftwareReleaseTypes.values})")
release_type = CharField(
help_text=_("Release type (possible values: %(values)s)") % {"values": SoftwareReleaseTypes.values}
)
class Meta:
model = SoftwareProductVersion
@@ -122,5 +125,5 @@ class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
label="Software Product",
label=_("Software Product"),
)
Binary file not shown.
+263
View File
@@ -0,0 +1,263 @@
msgid ""
msgstr ""
"Project-Id-Version: netbox-slm 1.9.1\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Add"
msgstr "Hinzufügen"
msgid "Import"
msgstr "Importieren"
msgid "Software Lifecycle"
msgstr "Software-Lebenszyklus"
msgid "Software Products"
msgstr "Softwareprodukte"
msgid "Software Product"
msgstr "Softwareprodukt"
msgid "Software Product Version"
msgstr "Softwareproduktversion"
msgid "Software Product Installation"
msgstr "Softwareproduktinstallation"
msgid "Software License"
msgstr "Softwarelizenz"
msgid "License Details"
msgstr "Lizenzdetails"
msgid "Versions"
msgstr "Versionen"
msgid "Version"
msgstr "Version"
msgid "Installations"
msgstr "Installationen"
msgid "Installation"
msgstr "Installation"
msgid "Add an installation"
msgstr "Installation hinzufügen"
msgid "Licenses"
msgstr "Lizenzen"
msgid "Tenancy"
msgstr "Mandantenzuordnung"
msgid "Tenant Group"
msgstr "Mandantengruppe"
msgid "Tenant"
msgstr "Mandant"
msgid "Manufacturer"
msgstr "Hersteller"
msgid "Device"
msgstr "Gerät"
msgid "Virtual Machine"
msgstr "Virtuelle Maschine"
msgid "Cluster"
msgstr "Cluster"
msgid "Name"
msgstr "Name"
msgid "Description"
msgstr "Beschreibung"
msgid "Type"
msgstr "Typ"
msgid "SPDX expression"
msgstr "SPDX-Ausdruck"
msgid "Stored location"
msgstr "Ablageort"
msgid "Start date"
msgstr "Startdatum"
msgid "Expiration date"
msgstr "Ablaufdatum"
msgid "Support"
msgstr "Support"
msgid "License amount"
msgstr "Lizenzanzahl"
msgid "Release date"
msgstr "Veröffentlichungsdatum"
msgid "Documentation URL"
msgstr "Dokumentations-URL"
msgid "End of support"
msgstr "Supportende"
msgid "Filename"
msgstr "Dateiname"
msgid "File checksum"
msgstr "Dateiprüfsumme"
msgid "Release type"
msgstr "Veröffentlichungstyp"
msgid "Release candidate"
msgstr "Veröffentlichungskandidat"
msgid "Stable release"
msgstr "Stabile Veröffentlichung"
msgid "Alpha"
msgstr "Alpha"
msgid "Beta"
msgstr "Beta"
msgid "Tags"
msgstr "Tags"
msgid "None"
msgstr "Keine"
msgid "Link"
msgstr "Link"
msgid "Support (values other than 'True' and 'False' are ignored)"
msgstr "Support (andere Werte als True und False werden ignoriert)"
msgid "Release type (possible values: %(values)s)"
msgstr "Veröffentlichungstyp (mögliche Werte: %(values)s)"
msgid "Version '%(version)s' does not belong to %(product)s. Select a compatible version."
msgstr "Version %(version)s gehört nicht zu %(product)s. Wähle eine kompatible Version aus."
msgid "Installation requires exactly one platform destination."
msgstr "Eine Installation benötigt genau ein Plattformziel."
msgid "%(value)s is not a known SPDX license expression"
msgstr "%(value)s ist kein bekannter SPDX-Lizenzausdruck"
msgid "The selected tenant does not belong to the selected tenant group."
msgstr "Der ausgewählte Mandant gehört nicht zur ausgewählten Mandantengruppe."
msgid "Software Lifecycle Management"
msgstr "Software-Lebenszyklusverwaltung"
msgid "Software Lifecycle Management NetBox Plugin."
msgstr "NetBox-Plugin zur Verwaltung des Software-Lebenszyklus."
msgid "name"
msgstr "Name"
msgid "comments"
msgstr "Kommentare"
msgid "description"
msgstr "Beschreibung"
msgid "manufacturer"
msgstr "Hersteller"
msgid "software product"
msgstr "Softwareprodukt"
msgid "software products"
msgstr "Softwareprodukte"
msgid "release date"
msgstr "Veröffentlichungsdatum"
msgid "documentation URL"
msgstr "Dokumentations-URL"
msgid "end of support"
msgstr "Supportende"
msgid "filename"
msgstr "Dateiname"
msgid "file checksum"
msgstr "Dateiprüfsumme"
msgid "file link"
msgstr "Dateilink"
msgid "release type"
msgstr "Veröffentlichungstyp"
msgid "software product version"
msgstr "Softwareproduktversion"
msgid "software product versions"
msgstr "Softwareproduktversionen"
msgid "device"
msgstr "Gerät"
msgid "virtual machine"
msgstr "Virtuelle Maschine"
msgid "cluster"
msgstr "Cluster"
msgid "version"
msgstr "Version"
msgid "software product installation"
msgstr "Softwareproduktinstallation"
msgid "software product installations"
msgstr "Softwareproduktinstallationen"
msgid "type"
msgstr "Typ"
msgid "stored location"
msgstr "Ablageort"
msgid "stored location URL"
msgstr "Ablageort-URL"
msgid "start date"
msgstr "Startdatum"
msgid "expiration date"
msgstr "Ablaufdatum"
msgid "support"
msgstr "Support"
msgid "license amount"
msgstr "Lizenzanzahl"
msgid "installation"
msgstr "Installation"
msgid "tenant group"
msgstr "Mandantengruppe"
msgid "tenant"
msgstr "Mandant"
msgid "software license"
msgstr "Softwarelizenz"
msgid "software licenses"
msgstr "Softwarelizenzen"
+91 -42
View File
@@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from license_expression import Licensing, get_spdx_licensing
from netbox.models import NetBoxModel
@@ -21,14 +22,20 @@ class LaxURLField(models.URLField):
class SoftwareProduct(NetBoxModel):
name = models.CharField(max_length=128)
comments = models.TextField(blank=True)
name = models.CharField(_("name"), max_length=128)
comments = models.TextField(_("comments"), blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True)
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(
to="dcim.Manufacturer", verbose_name=_("manufacturer"), on_delete=models.PROTECT, null=True, blank=True
)
objects = RestrictedQuerySet.as_manager()
class Meta:
verbose_name = _("software product")
verbose_name_plural = _("software products")
def __str__(self):
return self.name
@@ -44,25 +51,26 @@ class SoftwareProduct(NetBoxModel):
class SoftwareReleaseTypes(models.TextChoices):
ALPHA = "A", "Alpha"
BETA = "B", "Beta"
RELEASE_CANDIDATE = "RC", "Release candidate"
STABLE = "S", "Stable release"
ALPHA = "A", _("Alpha")
BETA = "B", _("Beta")
RELEASE_CANDIDATE = "RC", _("Release candidate")
STABLE = "S", _("Stable release")
class SoftwareProductVersion(NetBoxModel):
name = models.CharField(max_length=64)
comments = models.TextField(blank=True)
name = models.CharField(_("name"), max_length=64)
comments = models.TextField(_("comments"), blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
release_date = models.DateField(null=True, blank=True)
documentation_url = LaxURLField(max_length=1024, null=True, blank=True)
end_of_support = models.DateField(null=True, blank=True)
filename = models.CharField(max_length=64, null=True, blank=True)
file_checksum = models.CharField(max_length=128, null=True, blank=True)
file_link = LaxURLField(max_length=1024, null=True, blank=True)
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
release_date = models.DateField(_("release date"), null=True, blank=True)
documentation_url = LaxURLField(_("documentation URL"), max_length=1024, null=True, blank=True)
end_of_support = models.DateField(_("end of support"), null=True, blank=True)
filename = models.CharField(_("filename"), max_length=64, null=True, blank=True)
file_checksum = models.CharField(_("file checksum"), max_length=128, null=True, blank=True)
file_link = LaxURLField(_("file link"), max_length=1024, null=True, blank=True)
release_type = models.CharField(
_("release type"),
max_length=3,
choices=SoftwareReleaseTypes.choices,
default=SoftwareReleaseTypes.STABLE,
@@ -70,11 +78,16 @@ class SoftwareProductVersion(NetBoxModel):
software_product = models.ForeignKey(
to="netbox_slm.SoftwareProduct",
verbose_name=_("software product"),
on_delete=models.PROTECT,
)
objects = RestrictedQuerySet.as_manager()
class Meta:
verbose_name = _("software product version")
verbose_name_plural = _("software product versions")
def __str__(self):
return self.name
@@ -88,15 +101,27 @@ class SoftwareProductVersion(NetBoxModel):
class SoftwareProductInstallation(NetBoxModel):
comments = models.TextField(blank=True)
comments = models.TextField(_("comments"), blank=True)
device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True)
virtualmachine = models.ForeignKey(
to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True
device = models.ForeignKey(
to="dcim.Device", verbose_name=_("device"), on_delete=models.PROTECT, null=True, blank=True
)
virtualmachine = models.ForeignKey(
to="virtualization.VirtualMachine",
verbose_name=_("virtual machine"),
on_delete=models.PROTECT,
null=True,
blank=True,
)
cluster = models.ForeignKey(
to="virtualization.Cluster", verbose_name=_("cluster"), on_delete=models.PROTECT, null=True, blank=True
)
software_product = models.ForeignKey(
to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT
)
version = models.ForeignKey(
to="netbox_slm.SoftwareProductVersion", verbose_name=_("version"), on_delete=models.PROTECT
)
cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT)
objects = RestrictedQuerySet.as_manager()
@@ -104,6 +129,8 @@ class SoftwareProductInstallation(NetBoxModel):
return f"{self.pk} ({self.platform})"
class Meta:
verbose_name = _("software product installation")
verbose_name_plural = _("software product installations")
constraints = [
models.CheckConstraint(
name="%(app_label)s_%(class)s_platform",
@@ -112,7 +139,7 @@ class SoftwareProductInstallation(NetBoxModel):
| models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True)
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False)
),
violation_error_message="Installation requires exactly one platform destination.",
violation_error_message=_("Installation requires exactly one platform destination."),
)
]
@@ -141,30 +168,45 @@ def spdx_license_names():
def validate_spdx_expression(value):
expression_info = spdx_licensing.validate(value)
if expression_info.errors:
raise ValidationError(f"{value} is not a known SPDX license expression")
raise ValidationError(_("%(value)s is not a known SPDX license expression") % {"value": value})
class SoftwareLicense(NetBoxModel):
name = models.CharField(max_length=128)
comments = models.TextField(blank=True)
name = models.CharField(_("name"), max_length=128)
comments = models.TextField(_("comments"), blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
type = models.CharField(max_length=128)
spdx_expression = models.CharField(max_length=64, null=True, blank=True, validators=[validate_spdx_expression])
stored_location = models.CharField(max_length=255, null=True, blank=True)
stored_location_url = LaxURLField(max_length=1024, null=True, blank=True)
start_date = models.DateField(null=True, blank=True)
expiration_date = models.DateField(null=True, blank=True)
support = models.BooleanField(default=None, null=True, blank=True)
license_amount = models.PositiveIntegerField(default=None, null=True, blank=True)
description = models.CharField(_("description"), max_length=255, null=True, blank=True)
type = models.CharField(_("type"), max_length=128)
spdx_expression = models.CharField(
_("SPDX expression"), max_length=64, null=True, blank=True, validators=[validate_spdx_expression]
)
stored_location = models.CharField(_("stored location"), max_length=255, null=True, blank=True)
stored_location_url = LaxURLField(_("stored location URL"), max_length=1024, null=True, blank=True)
start_date = models.DateField(_("start date"), null=True, blank=True)
expiration_date = models.DateField(_("expiration date"), null=True, blank=True)
support = models.BooleanField(_("support"), default=None, null=True, blank=True)
license_amount = models.PositiveIntegerField(_("license amount"), default=None, null=True, blank=True)
software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT)
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True)
software_product = models.ForeignKey(
to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT
)
version = models.ForeignKey(
to="netbox_slm.SoftwareProductVersion",
verbose_name=_("version"),
on_delete=models.PROTECT,
null=True,
blank=True,
)
installation = models.ForeignKey(
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
to="netbox_slm.SoftwareProductInstallation",
verbose_name=_("installation"),
on_delete=models.SET_NULL,
null=True,
blank=True,
)
tenant_group = models.ForeignKey(
to="tenancy.TenantGroup",
verbose_name=_("tenant group"),
on_delete=models.PROTECT,
null=True,
blank=True,
@@ -172,6 +214,7 @@ class SoftwareLicense(NetBoxModel):
)
tenant = models.ForeignKey(
to="tenancy.Tenant",
verbose_name=_("tenant"),
on_delete=models.PROTECT,
null=True,
blank=True,
@@ -180,6 +223,10 @@ class SoftwareLicense(NetBoxModel):
objects = RestrictedQuerySet.as_manager()
class Meta:
verbose_name = _("software license")
verbose_name_plural = _("software licenses")
def __str__(self):
return self.name
@@ -189,10 +236,12 @@ class SoftwareLicense(NetBoxModel):
def clean(self):
super().clean()
if self.tenant_group and self.tenant and self.tenant.group_id != self.tenant_group_id:
raise ValidationError({"tenant": "The selected tenant does not belong to the selected tenant group."})
raise ValidationError(
{"tenant": _("The selected tenant does not belong to the selected tenant group.")}
)
@property
def stored_location_txt(self):
if self.stored_location_url and not self.stored_location:
return "Link"
return _("Link")
return self.stored_location
+14 -13
View File
@@ -1,21 +1,22 @@
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
from django.utils.translation import gettext_lazy as _
from . import SLMConfig
slm_items = (
PluginMenuItem(
link="plugins:netbox_slm:softwareproduct_list",
link_text="Software Products",
link_text=_("Software Products"),
permissions=["netbox_slm.add_softwareproduct"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_add",
"Add",
_("Add"),
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproduct"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproduct_bulk_import",
"Import",
_("Import"),
"mdi mdi-upload",
permissions=["netbox_slm.import_softwareproduct"],
),
@@ -23,18 +24,18 @@ slm_items = (
),
PluginMenuItem(
link="plugins:netbox_slm:softwareproductversion_list",
link_text="Versions",
link_text=_("Versions"),
permissions=["netbox_slm.add_softwareproductversion"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_add",
"Add",
_("Add"),
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproductversion"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_bulk_import",
"Import",
_("Import"),
"mdi mdi-upload",
permissions=["netbox_slm.import_softwareproductversion"],
),
@@ -42,18 +43,18 @@ slm_items = (
),
PluginMenuItem(
link="plugins:netbox_slm:softwareproductinstallation_list",
link_text="Installations",
link_text=_("Installations"),
permissions=["netbox_slm.add_softwareproductinstallation"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_add",
"Add",
_("Add"),
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproductinstallation"],
),
PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_bulk_import",
"Import",
_("Import"),
"mdi mdi-upload",
permissions=["netbox_slm.import_softwareproductinstallation"],
),
@@ -61,18 +62,18 @@ slm_items = (
),
PluginMenuItem(
link="plugins:netbox_slm:softwarelicense_list",
link_text="Licenses",
link_text=_("Licenses"),
permissions=["netbox_slm.add_softwarelicense"],
buttons=(
PluginMenuButton(
"plugins:netbox_slm:softwarelicense_add",
"Add",
_("Add"),
"mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwarelicense"],
),
PluginMenuButton(
"plugins:netbox_slm:softwarelicense_bulk_import",
"Import",
_("Import"),
"mdi mdi-upload",
permissions=["netbox_slm.import_softwarelicense"],
),
@@ -82,7 +83,7 @@ slm_items = (
if get_plugin_config("netbox_slm", "top_level_menu"):
menu = PluginMenu(
label="Software Lifecycle",
label=_("Software Lifecycle"),
groups=((SLMConfig.verbose_name, slm_items),),
icon_class="mdi mdi-content-save",
)
+6 -5
View File
@@ -1,5 +1,6 @@
import django_tables2 as tables
from django.db.models import Count, F, Value
from django.utils.translation import gettext_lazy as _
from netbox.tables import NetBoxTable, ToggleColumn, columns
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
@@ -45,7 +46,7 @@ class SoftwareProductVersionTable(NetBoxTable):
pk = ToggleColumn()
name = tables.LinkColumn()
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count")
@@ -89,7 +90,7 @@ class SoftwareProductInstallationTable(NetBoxTable):
platform = tables.Column(accessor="platform", linkify=True)
type = tables.Column(accessor="render_type")
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
version = tables.Column(accessor="version", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
@@ -137,14 +138,14 @@ class SoftwareLicenseTable(NetBoxTable):
name = tables.LinkColumn()
type = tables.Column()
spdx_expression = tables.Column(verbose_name="SPDX expression")
spdx_expression = tables.Column(verbose_name=_("SPDX expression"))
stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
software_product = tables.Column(accessor="software_product", verbose_name=_("Software Product"), linkify=True)
version = tables.Column(accessor="version", linkify=True)
installation = tables.Column(accessor="installation", linkify=True)
tenant_group = tables.Column(accessor="tenant_group", verbose_name="Tenant Group", linkify=True)
tenant_group = tables.Column(accessor="tenant_group", verbose_name=_("Tenant Group"), linkify=True)
tenant = tables.Column(accessor="tenant", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
@@ -1,12 +1,13 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">
Installations
{% trans "Installations" %}
{% if perms.netbox_slm.add_softwareproductinstallation %}
<div class="card-actions">
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation_add' %}?{{ search_obj }}={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add an installation
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an installation" %}
</a>
</div>
{% endif %}
@@ -3,53 +3,54 @@
{% load static %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software License
{% trans "Software License" %}
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.type }}</td>
</tr>
<tr>
<th scope="row">SPDX expression</th>
<th scope="row">{% trans "SPDX expression" %}</th>
<td>{{ object.spdx_expression }}</td>
</tr>
<tr>
<th scope="row">Software Product</th>
<th scope="row">{% trans "Software Product" %}</th>
<td>{{ object.software_product|linkify }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<th scope="row">{% trans "Version" %}</th>
<td>{{ object.version|linkify }}</td>
</tr>
<tr>
<th scope="row">Installation</th>
<th scope="row">{% trans "Installation" %}</th>
<td>{{ object.installation|linkify }}</td>
</tr>
<tr>
<th scope="row">Tenant Group</th>
<th scope="row">{% trans "Tenant Group" %}</th>
<td>{{ object.tenant_group|linkify }}</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<th scope="row">{% trans "Tenant" %}</th>
<td>{{ object.tenant|linkify }}</td>
</tr>
<tr>
<th scope="row">Stored location</th>
<th scope="row">{% trans "Stored location" %}</th>
{% if object.stored_location_url %}
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
{% else %}
@@ -57,19 +58,19 @@
{% endif %}
</tr>
<tr>
<th scope="row">Start date</th>
<th scope="row">{% trans "Start date" %}</th>
<td>{{ object.start_date }}</td>
</tr>
<tr>
<th scope="row">Expiration date</th>
<th scope="row">{% trans "Expiration date" %}</th>
<td>{{ object.expiration_date }}</td>
</tr>
<tr>
<th scope="row">Support</th>
<th scope="row">{% trans "Support" %}</th>
<td>{{ object.support }}</td>
</tr>
<tr>
<th scope="row">License amount</th>
<th scope="row">{% trans "License amount" %}</th>
<td>{{ object.license_amount }}</td>
</tr>
</table>
@@ -3,60 +3,61 @@
{% load static %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product
{% trans "Software Product" %}
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<th scope="row">{% trans "Versions" %}</th>
<td>
{% for version in object.softwareproductversion_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
{{ version }}
</a>
{% empty %}
None
{% trans "None" %}
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Installations</th>
<th scope="row">{% trans "Installations" %}</th>
<td>
{% for installation in object.softwareproductinstallation_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
{{ installation }}
</a>
{% empty %}
None
{% trans "None" %}
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Licenses</th>
<th scope="row">{% trans "Licenses" %}</th>
<td>
{% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }}
</a>
{% empty %}
None
{% trans "None" %}
{% endfor %}
</td>
</tr>
@@ -3,48 +3,49 @@
{% load static %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product Installation
{% trans "Software Product Installation" %}
</h5>
<table class="table table-hover attr-table">
{% if object.device %}
<tr>
<th scope="row">Device</th>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
{% elif object.virtualmachine %}
<tr>
<th scope="row">Virtual Machine</th>
<th scope="row">{% trans "Virtual Machine" %}</th>
<td>{{ object.virtualmachine|linkify }}</td>
</tr>
{% else %}
<tr>
<th scope="row">Cluster</th>
<th scope="row">{% trans "Cluster" %}</th>
<td>{{ object.cluster|linkify }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Software Product</th>
<th scope="row">{% trans "Software Product" %}</th>
<td>{{ object.software_product|linkify }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<th scope="row">{% trans "Version" %}</th>
<td>{{ object.version|linkify }}</td>
</tr>
<tr>
<th scope="row">Licenses</th>
<th scope="row">{% trans "Licenses" %}</th>
<td>
{% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }}
</a>
{% empty %}
None
{% trans "None" %}
{% endfor %}
</td>
</tr>
@@ -3,37 +3,38 @@
{% load static %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product Version
{% trans "Software Product Version" %}
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.software_product.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">Software Product</th>
<th scope="row">{% trans "Software Product" %}</th>
<td>{{ object.software_product|linkify }}</td>
</tr>
<tr>
<th scope="row">Release date</th>
<th scope="row">{% trans "Release date" %}</th>
<td>{{ object.release_date }}</td>
</tr>
<tr>
<th scope="row">Documentation url</th>
<th scope="row">{% trans "Documentation URL" %}</th>
{% if object.documentation_url %}
<td><a href="{{ object.documentation_url }}">{{ object.documentation_url }}</a></td>
{% else %}
@@ -41,11 +42,11 @@
{% endif %}
</tr>
<tr>
<th scope="row">End of support</th>
<th scope="row">{% trans "End of support" %}</th>
<td>{{ object.end_of_support }}</td>
</tr>
<tr>
<th scope="row">Filename</th>
<th scope="row">{% trans "Filename" %}</th>
{% if object.file_link %}
<td><a href="{{ object.file_link }}">{{ object.filename }}</a></td>
{% else %}
@@ -53,34 +54,34 @@
{% endif %}
</tr>
<tr>
<th scope="row">File checksum</th>
<th scope="row">{% trans "File checksum" %}</th>
<td>{{ object.file_checksum }}</td>
</tr>
<tr>
<th scope="row">Release type</th>
<th scope="row">{% trans "Release type" %}</th>
<td>{{ object.get_release_type_display }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<th scope="row">{% trans "Installations" %}</th>
<td>
{% for installation in object.softwareproductinstallation_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
{{ installation }}
</a>
{% empty %}
None
{% trans "None" %}
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Licenses</th>
<th scope="row">{% trans "Licenses" %}</th>
<td>
{% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }}
</a>
{% empty %}
None
{% trans "None" %}
{% endfor %}
</td>
</tr>
+7
View File
@@ -1,4 +1,5 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext, override
from netbox_slm.forms import SoftwareLicenseForm
from tenancy.models import Tenant, TenantGroup
@@ -73,6 +74,12 @@ class ModelTestCase(SlmBaseTestCase):
any("tenant_group" in fieldset.items and "tenant" in fieldset.items for fieldset in form.fieldsets)
)
def test_german_translation(self):
with override("de"):
self.assertEqual("Softwarelizenz", gettext("Software License"))
self.assertEqual("Mandantengruppe", gettext("Tenant Group"))
self.assertEqual("Lizenzen", gettext("Licenses"))
def test_software_license_rejects_tenant_from_other_group(self):
other_group = TenantGroup.objects.create(name="other group", slug="other-group")
other_tenant = Tenant.objects.create(name="other tenant", slug="other-tenant", group=other_group)