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 ### Added
* Optional tenant group and tenant assignments for software licenses * Optional tenant group and tenant assignments for software licenses
* German translation for navigation, forms, tables, detail views, choices, and validation messages
### Changed ### 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. 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 ## Funktionen
- Softwareprodukte und Hersteller verwalten - 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 /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 ### 3. Plugin in NetBox aktivieren
+4 -3
View File
@@ -15,15 +15,16 @@ limitations under the License.
""" """
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from django.utils.translation import gettext_lazy as _
__version__ = "1.9.1" __version__ = "1.10.0"
class SLMConfig(PluginConfig): class SLMConfig(PluginConfig):
name = "netbox_slm" name = "netbox_slm"
verbose_name = "Software Lifecycle Management" verbose_name = _("Software Lifecycle Management")
version = __version__ version = __version__
description = "Software Lifecycle Management Netbox Plugin." description = _("Software Lifecycle Management NetBox Plugin.")
author = "ICTU" author = "ICTU"
author_email = "open-source-projects@ictu.nl" author_email = "open-source-projects@ictu.nl"
base_url = "slm" base_url = "slm"
+2 -1
View File
@@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
from django.utils.translation import gettext_lazy as _
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense 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)) tenant = attrs.get("tenant", getattr(self.instance, "tenant", None))
if tenant_group and tenant and tenant.group_id != tenant_group.pk: if tenant_group and tenant and tenant.group_id != tenant_group.pk:
raise serializers.ValidationError( 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 return attrs
+8 -7
View File
@@ -1,4 +1,5 @@
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
from dcim.models import Device, Manufacturer from dcim.models import Device, Manufacturer
@@ -55,7 +56,7 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
software_product_id = ModelMultipleChoiceFilter( software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
label="Software Product", label=_("Software Product"),
) )
class Meta: class Meta:
@@ -82,12 +83,12 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
device_id = ModelMultipleChoiceFilter(queryset=Device.objects.all()) device_id = ModelMultipleChoiceFilter(queryset=Device.objects.all())
virtualmachine_id = ModelMultipleChoiceFilter( virtualmachine_id = ModelMultipleChoiceFilter(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
label="Virtual Machine", label=_("Virtual Machine"),
) )
cluster_id = ModelMultipleChoiceFilter(queryset=Cluster.objects.all()) cluster_id = ModelMultipleChoiceFilter(queryset=Cluster.objects.all())
software_product_id = ModelMultipleChoiceFilter( software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
label="Software Product", label=_("Software Product"),
) )
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all()) version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
@@ -114,17 +115,17 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
name = CharFilter(lookup_expr="icontains") name = CharFilter(lookup_expr="icontains")
description = CharFilter(lookup_expr="icontains") description = CharFilter(lookup_expr="icontains")
type = 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") stored_location = CharFilter(lookup_expr="icontains")
software_product_id = ModelMultipleChoiceFilter( software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
label="Software Product", label=_("Software Product"),
) )
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all()) version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all()) installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all())
tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label="Tenant Group") tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label=_("Tenant Group"))
tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label="Tenant") tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label=_("Tenant"))
class Meta: class Meta:
model = SoftwareLicense model = SoftwareLicense
+22 -19
View File
@@ -1,5 +1,6 @@
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
from netbox_slm.models import ( from netbox_slm.models import (
@@ -31,9 +32,9 @@ class SoftwareLicenseForm(NetBoxModelForm):
"software_product", "software_product",
"version", "version",
"installation", "installation",
name="Software License", name=_("Software License"),
), ),
FieldSet("tenant_group", "tenant", name="Tenancy"), FieldSet("tenant_group", "tenant", name=_("Tenancy")),
FieldSet( FieldSet(
"type", "type",
"spdx_expression", "spdx_expression",
@@ -43,12 +44,12 @@ class SoftwareLicenseForm(NetBoxModelForm):
"expiration_date", "expiration_date",
"support", "support",
"license_amount", "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) stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker()) start_date = DateField(required=False, widget=DatePicker())
expiration_date = DateField(required=False, widget=DatePicker()) expiration_date = DateField(required=False, widget=DatePicker())
@@ -56,7 +57,7 @@ class SoftwareLicenseForm(NetBoxModelForm):
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=True, required=True,
label="Software Product", label=_("Software Product"),
) )
version = DynamicModelChoiceField( version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
@@ -75,13 +76,13 @@ class SoftwareLicenseForm(NetBoxModelForm):
tenant_group = DynamicModelChoiceField( tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False,
label="Tenant Group", label=_("Tenant Group"),
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
query_params={"group_id": "$tenant_group"}, query_params={"group_id": "$tenant_group"},
label="Tenant", label=_("Tenant"),
) )
class Meta: class Meta:
@@ -132,40 +133,42 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
name = CharField(required=False) name = CharField(required=False)
description = CharField(required=False) description = CharField(required=False)
type = 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) stored_location = CharField(required=False)
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
software_product_id = DynamicModelMultipleChoiceField( software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=False, required=False,
label="Software Product", label=_("Software Product"),
) )
version_id = DynamicModelMultipleChoiceField( version_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
required=False, required=False,
label="Version", label=_("Version"),
) )
installation_id = DynamicModelMultipleChoiceField( installation_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductInstallation.objects.all(), queryset=SoftwareProductInstallation.objects.all(),
required=False, required=False,
label="Installation", label=_("Installation"),
) )
tenant_group_id = DynamicModelMultipleChoiceField( tenant_group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False,
label="Tenant Group", label=_("Tenant Group"),
) )
tenant_id = DynamicModelMultipleChoiceField( tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
query_params={"group_id": "$tenant_group_id"}, query_params={"group_id": "$tenant_group_id"},
label="Tenant", label=_("Tenant"),
) )
class SoftwareLicenseBulkImportForm(NetBoxModelImportForm): 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: class Meta:
model = SoftwareLicense model = SoftwareLicense
@@ -226,7 +229,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
comments = CommentField() comments = CommentField()
type = CharField(required=False) 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 = CharField(required=False)
stored_location_url = LaxURLField(required=False) stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker()) start_date = DateField(required=False, widget=DatePicker())
@@ -237,7 +240,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=False, required=False,
label="Software Product", label=_("Software Product"),
) )
version = DynamicModelChoiceField( version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
@@ -256,11 +259,11 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
tenant_group = DynamicModelChoiceField( tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False,
label="Tenant Group", label=_("Tenant Group"),
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
query_params={"group_id": "$tenant_group"}, 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.forms import CharField
from django.utils.translation import gettext_lazy as _
from dcim.models import Manufacturer from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
@@ -46,7 +47,7 @@ class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
label="Manufacturer", label=_("Manufacturer"),
) )
@@ -1,5 +1,6 @@
from django.forms import ValidationError from django.forms import ValidationError
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from dcim.models import Device from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
@@ -22,13 +23,13 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
virtualmachine = DynamicModelChoiceField( virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
label="Virtual Machine", label=_("Virtual Machine"),
) )
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False) cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=True, required=True,
label="Software Product", label=_("Software Product"),
) )
version = DynamicModelChoiceField( version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
@@ -54,8 +55,8 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
software_product = self.cleaned_data["software_product"] software_product = self.cleaned_data["software_product"]
if version not in software_product.softwareproductversion_set.all(): if version not in software_product.softwareproductversion_set.all():
raise ValidationError( raise ValidationError(
f"Version '{version}' doesn't exist on {software_product}, make sure you've " _("Version '%(version)s' does not belong to %(product)s. Select a compatible version.")
f"selected a compatible version or first select the software product." % {"version": version, "product": software_product}
) )
return version return version
@@ -73,27 +74,27 @@ class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
device_id = DynamicModelMultipleChoiceField( device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
label="Device", label=_("Device"),
) )
virtualmachine_id = DynamicModelMultipleChoiceField( virtualmachine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
label="Virtual Machine", label=_("Virtual Machine"),
) )
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
label="Cluster", label=_("Cluster"),
) )
software_product_id = DynamicModelMultipleChoiceField( software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=False, required=False,
label="Software Product", label=_("Software Product"),
) )
version_id = DynamicModelMultipleChoiceField( version_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
required=False, required=False,
label="Version", label=_("Version"),
) )
@@ -122,13 +123,13 @@ class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
virtualmachine = DynamicModelChoiceField( virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
label="Virtual Machine", label=_("Virtual Machine"),
) )
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False) cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=False, required=False,
label="Software Product", label=_("Software Product"),
) )
version = DynamicModelChoiceField( version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(), queryset=SoftwareProductVersion.objects.all(),
+8 -5
View File
@@ -1,5 +1,6 @@
from django.forms import DateField, CharField, MultipleChoiceField from django.forms import DateField, CharField, MultipleChoiceField
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from dcim.models import Manufacturer from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm
@@ -23,7 +24,7 @@ class SoftwareProductVersionForm(NetBoxModelForm):
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=True, required=True,
label="Software Product", label=_("Software Product"),
widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}), widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}),
) )
@@ -64,17 +65,19 @@ class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
label="Manufacturer", label=_("Manufacturer"),
) )
software_product_id = DynamicModelMultipleChoiceField( software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=False, required=False,
label="Software Product", label=_("Software Product"),
) )
class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm): 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: class Meta:
model = SoftwareProductVersion model = SoftwareProductVersion
@@ -122,5 +125,5 @@ class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
required=False, 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.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from license_expression import Licensing, get_spdx_licensing from license_expression import Licensing, get_spdx_licensing
from netbox.models import NetBoxModel from netbox.models import NetBoxModel
@@ -21,14 +22,20 @@ class LaxURLField(models.URLField):
class SoftwareProduct(NetBoxModel): class SoftwareProduct(NetBoxModel):
name = models.CharField(max_length=128) name = models.CharField(_("name"), max_length=128)
comments = models.TextField(blank=True) comments = models.TextField(_("comments"), blank=True)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(_("description"), max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, 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() objects = RestrictedQuerySet.as_manager()
class Meta:
verbose_name = _("software product")
verbose_name_plural = _("software products")
def __str__(self): def __str__(self):
return self.name return self.name
@@ -44,25 +51,26 @@ class SoftwareProduct(NetBoxModel):
class SoftwareReleaseTypes(models.TextChoices): class SoftwareReleaseTypes(models.TextChoices):
ALPHA = "A", "Alpha" ALPHA = "A", _("Alpha")
BETA = "B", "Beta" BETA = "B", _("Beta")
RELEASE_CANDIDATE = "RC", "Release candidate" RELEASE_CANDIDATE = "RC", _("Release candidate")
STABLE = "S", "Stable release" STABLE = "S", _("Stable release")
class SoftwareProductVersion(NetBoxModel): class SoftwareProductVersion(NetBoxModel):
name = models.CharField(max_length=64) name = models.CharField(_("name"), max_length=64)
comments = models.TextField(blank=True) comments = models.TextField(_("comments"), blank=True)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(_("description"), max_length=255, null=True, blank=True)
release_date = models.DateField(null=True, blank=True) release_date = models.DateField(_("release date"), null=True, blank=True)
documentation_url = LaxURLField(max_length=1024, null=True, blank=True) documentation_url = LaxURLField(_("documentation URL"), max_length=1024, null=True, blank=True)
end_of_support = models.DateField(null=True, blank=True) end_of_support = models.DateField(_("end of support"), null=True, blank=True)
filename = models.CharField(max_length=64, null=True, blank=True) filename = models.CharField(_("filename"), max_length=64, null=True, blank=True)
file_checksum = models.CharField(max_length=128, null=True, blank=True) file_checksum = models.CharField(_("file checksum"), max_length=128, null=True, blank=True)
file_link = LaxURLField(max_length=1024, null=True, blank=True) file_link = LaxURLField(_("file link"), max_length=1024, null=True, blank=True)
release_type = models.CharField( release_type = models.CharField(
_("release type"),
max_length=3, max_length=3,
choices=SoftwareReleaseTypes.choices, choices=SoftwareReleaseTypes.choices,
default=SoftwareReleaseTypes.STABLE, default=SoftwareReleaseTypes.STABLE,
@@ -70,11 +78,16 @@ class SoftwareProductVersion(NetBoxModel):
software_product = models.ForeignKey( software_product = models.ForeignKey(
to="netbox_slm.SoftwareProduct", to="netbox_slm.SoftwareProduct",
verbose_name=_("software product"),
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
class Meta:
verbose_name = _("software product version")
verbose_name_plural = _("software product versions")
def __str__(self): def __str__(self):
return self.name return self.name
@@ -88,15 +101,27 @@ class SoftwareProductVersion(NetBoxModel):
class SoftwareProductInstallation(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) device = models.ForeignKey(
virtualmachine = models.ForeignKey( to="dcim.Device", verbose_name=_("device"), on_delete=models.PROTECT, null=True, blank=True
to="virtualization.VirtualMachine", 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() objects = RestrictedQuerySet.as_manager()
@@ -104,6 +129,8 @@ class SoftwareProductInstallation(NetBoxModel):
return f"{self.pk} ({self.platform})" return f"{self.pk} ({self.platform})"
class Meta: class Meta:
verbose_name = _("software product installation")
verbose_name_plural = _("software product installations")
constraints = [ constraints = [
models.CheckConstraint( models.CheckConstraint(
name="%(app_label)s_%(class)s_platform", 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=False, cluster__isnull=True)
| models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False) | 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): def validate_spdx_expression(value):
expression_info = spdx_licensing.validate(value) expression_info = spdx_licensing.validate(value)
if expression_info.errors: 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): class SoftwareLicense(NetBoxModel):
name = models.CharField(max_length=128) name = models.CharField(_("name"), max_length=128)
comments = models.TextField(blank=True) comments = models.TextField(_("comments"), blank=True)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(_("description"), max_length=255, null=True, blank=True)
type = models.CharField(max_length=128) type = models.CharField(_("type"), max_length=128)
spdx_expression = models.CharField(max_length=64, null=True, blank=True, validators=[validate_spdx_expression]) spdx_expression = models.CharField(
stored_location = models.CharField(max_length=255, null=True, blank=True) _("SPDX expression"), max_length=64, null=True, blank=True, validators=[validate_spdx_expression]
stored_location_url = LaxURLField(max_length=1024, null=True, blank=True) )
start_date = models.DateField(null=True, blank=True) stored_location = models.CharField(_("stored location"), max_length=255, null=True, blank=True)
expiration_date = models.DateField(null=True, blank=True) stored_location_url = LaxURLField(_("stored location URL"), max_length=1024, null=True, blank=True)
support = models.BooleanField(default=None, null=True, blank=True) start_date = models.DateField(_("start date"), null=True, blank=True)
license_amount = models.PositiveIntegerField(default=None, 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) software_product = models.ForeignKey(
version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True) 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( 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( tenant_group = models.ForeignKey(
to="tenancy.TenantGroup", to="tenancy.TenantGroup",
verbose_name=_("tenant group"),
on_delete=models.PROTECT, on_delete=models.PROTECT,
null=True, null=True,
blank=True, blank=True,
@@ -172,6 +214,7 @@ class SoftwareLicense(NetBoxModel):
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to="tenancy.Tenant", to="tenancy.Tenant",
verbose_name=_("tenant"),
on_delete=models.PROTECT, on_delete=models.PROTECT,
null=True, null=True,
blank=True, blank=True,
@@ -180,6 +223,10 @@ class SoftwareLicense(NetBoxModel):
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
class Meta:
verbose_name = _("software license")
verbose_name_plural = _("software licenses")
def __str__(self): def __str__(self):
return self.name return self.name
@@ -189,10 +236,12 @@ class SoftwareLicense(NetBoxModel):
def clean(self): def clean(self):
super().clean() super().clean()
if self.tenant_group and self.tenant and self.tenant.group_id != self.tenant_group_id: 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 @property
def stored_location_txt(self): def stored_location_txt(self):
if self.stored_location_url and not self.stored_location: if self.stored_location_url and not self.stored_location:
return "Link" return _("Link")
return self.stored_location return self.stored_location
+14 -13
View File
@@ -1,21 +1,22 @@
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
from django.utils.translation import gettext_lazy as _
from . import SLMConfig from . import SLMConfig
slm_items = ( slm_items = (
PluginMenuItem( PluginMenuItem(
link="plugins:netbox_slm:softwareproduct_list", link="plugins:netbox_slm:softwareproduct_list",
link_text="Software Products", link_text=_("Software Products"),
permissions=["netbox_slm.add_softwareproduct"], permissions=["netbox_slm.add_softwareproduct"],
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproduct_add", "plugins:netbox_slm:softwareproduct_add",
"Add", _("Add"),
"mdi mdi-plus-thick", "mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproduct"], permissions=["netbox_slm.add_softwareproduct"],
), ),
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproduct_bulk_import", "plugins:netbox_slm:softwareproduct_bulk_import",
"Import", _("Import"),
"mdi mdi-upload", "mdi mdi-upload",
permissions=["netbox_slm.import_softwareproduct"], permissions=["netbox_slm.import_softwareproduct"],
), ),
@@ -23,18 +24,18 @@ slm_items = (
), ),
PluginMenuItem( PluginMenuItem(
link="plugins:netbox_slm:softwareproductversion_list", link="plugins:netbox_slm:softwareproductversion_list",
link_text="Versions", link_text=_("Versions"),
permissions=["netbox_slm.add_softwareproductversion"], permissions=["netbox_slm.add_softwareproductversion"],
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_add", "plugins:netbox_slm:softwareproductversion_add",
"Add", _("Add"),
"mdi mdi-plus-thick", "mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproductversion"], permissions=["netbox_slm.add_softwareproductversion"],
), ),
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproductversion_bulk_import", "plugins:netbox_slm:softwareproductversion_bulk_import",
"Import", _("Import"),
"mdi mdi-upload", "mdi mdi-upload",
permissions=["netbox_slm.import_softwareproductversion"], permissions=["netbox_slm.import_softwareproductversion"],
), ),
@@ -42,18 +43,18 @@ slm_items = (
), ),
PluginMenuItem( PluginMenuItem(
link="plugins:netbox_slm:softwareproductinstallation_list", link="plugins:netbox_slm:softwareproductinstallation_list",
link_text="Installations", link_text=_("Installations"),
permissions=["netbox_slm.add_softwareproductinstallation"], permissions=["netbox_slm.add_softwareproductinstallation"],
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_add", "plugins:netbox_slm:softwareproductinstallation_add",
"Add", _("Add"),
"mdi mdi-plus-thick", "mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwareproductinstallation"], permissions=["netbox_slm.add_softwareproductinstallation"],
), ),
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwareproductinstallation_bulk_import", "plugins:netbox_slm:softwareproductinstallation_bulk_import",
"Import", _("Import"),
"mdi mdi-upload", "mdi mdi-upload",
permissions=["netbox_slm.import_softwareproductinstallation"], permissions=["netbox_slm.import_softwareproductinstallation"],
), ),
@@ -61,18 +62,18 @@ slm_items = (
), ),
PluginMenuItem( PluginMenuItem(
link="plugins:netbox_slm:softwarelicense_list", link="plugins:netbox_slm:softwarelicense_list",
link_text="Licenses", link_text=_("Licenses"),
permissions=["netbox_slm.add_softwarelicense"], permissions=["netbox_slm.add_softwarelicense"],
buttons=( buttons=(
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwarelicense_add", "plugins:netbox_slm:softwarelicense_add",
"Add", _("Add"),
"mdi mdi-plus-thick", "mdi mdi-plus-thick",
permissions=["netbox_slm.add_softwarelicense"], permissions=["netbox_slm.add_softwarelicense"],
), ),
PluginMenuButton( PluginMenuButton(
"plugins:netbox_slm:softwarelicense_bulk_import", "plugins:netbox_slm:softwarelicense_bulk_import",
"Import", _("Import"),
"mdi mdi-upload", "mdi mdi-upload",
permissions=["netbox_slm.import_softwarelicense"], permissions=["netbox_slm.import_softwarelicense"],
), ),
@@ -82,7 +83,7 @@ slm_items = (
if get_plugin_config("netbox_slm", "top_level_menu"): if get_plugin_config("netbox_slm", "top_level_menu"):
menu = PluginMenu( menu = PluginMenu(
label="Software Lifecycle", label=_("Software Lifecycle"),
groups=((SLMConfig.verbose_name, slm_items),), groups=((SLMConfig.verbose_name, slm_items),),
icon_class="mdi mdi-content-save", icon_class="mdi mdi-content-save",
) )
+6 -5
View File
@@ -1,5 +1,6 @@
import django_tables2 as tables import django_tables2 as tables
from django.db.models import Count, F, Value 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.tables import NetBoxTable, ToggleColumn, columns
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
@@ -45,7 +46,7 @@ class SoftwareProductVersionTable(NetBoxTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() 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) manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True)
installations = tables.Column(accessor="get_installation_count") installations = tables.Column(accessor="get_installation_count")
@@ -89,7 +90,7 @@ class SoftwareProductInstallationTable(NetBoxTable):
platform = tables.Column(accessor="platform", linkify=True) platform = tables.Column(accessor="platform", linkify=True)
type = tables.Column(accessor="render_type") type = tables.Column(accessor="render_type")
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) 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) version = tables.Column(accessor="version", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list") tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list")
@@ -137,14 +138,14 @@ class SoftwareLicenseTable(NetBoxTable):
name = tables.LinkColumn() name = tables.LinkColumn()
type = tables.Column() 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) stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url)
manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) 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) version = tables.Column(accessor="version", linkify=True)
installation = tables.Column(accessor="installation", 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) tenant = tables.Column(accessor="tenant", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list") tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
@@ -1,12 +1,13 @@
{% load helpers %} {% load helpers %}
{% load i18n %}
<div class="card"> <div class="card">
<h2 class="card-header"> <h2 class="card-header">
Installations {% trans "Installations" %}
{% if perms.netbox_slm.add_softwareproductinstallation %} {% if perms.netbox_slm.add_softwareproductinstallation %}
<div class="card-actions"> <div class="card-actions">
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation_add' %}?{{ search_obj }}={{ object.pk }}" class="btn btn-ghost-primary btn-sm"> <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> </a>
</div> </div>
{% endif %} {% endif %}
@@ -3,53 +3,54 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block content %} {% block content %}
<div class="row my-3"> <div class="row my-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Software License {% trans "Software License" %}
</h5> </h5>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td> <td>{{ object.description }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">{% trans "Type" %}</th>
<td>{{ object.type }}</td> <td>{{ object.type }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">SPDX expression</th> <th scope="row">{% trans "SPDX expression" %}</th>
<td>{{ object.spdx_expression }}</td> <td>{{ object.spdx_expression }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Software Product</th> <th scope="row">{% trans "Software Product" %}</th>
<td>{{ object.software_product|linkify }}</td> <td>{{ object.software_product|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Version</th> <th scope="row">{% trans "Version" %}</th>
<td>{{ object.version|linkify }}</td> <td>{{ object.version|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Installation</th> <th scope="row">{% trans "Installation" %}</th>
<td>{{ object.installation|linkify }}</td> <td>{{ object.installation|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Tenant Group</th> <th scope="row">{% trans "Tenant Group" %}</th>
<td>{{ object.tenant_group|linkify }}</td> <td>{{ object.tenant_group|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Tenant</th> <th scope="row">{% trans "Tenant" %}</th>
<td>{{ object.tenant|linkify }}</td> <td>{{ object.tenant|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Stored location</th> <th scope="row">{% trans "Stored location" %}</th>
{% if object.stored_location_url %} {% if object.stored_location_url %}
<td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td> <td><a href="{{ object.stored_location_url }}">{{ object.stored_location_txt }}</a></td>
{% else %} {% else %}
@@ -57,19 +58,19 @@
{% endif %} {% endif %}
</tr> </tr>
<tr> <tr>
<th scope="row">Start date</th> <th scope="row">{% trans "Start date" %}</th>
<td>{{ object.start_date }}</td> <td>{{ object.start_date }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Expiration date</th> <th scope="row">{% trans "Expiration date" %}</th>
<td>{{ object.expiration_date }}</td> <td>{{ object.expiration_date }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Support</th> <th scope="row">{% trans "Support" %}</th>
<td>{{ object.support }}</td> <td>{{ object.support }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">License amount</th> <th scope="row">{% trans "License amount" %}</th>
<td>{{ object.license_amount }}</td> <td>{{ object.license_amount }}</td>
</tr> </tr>
</table> </table>
@@ -3,60 +3,61 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block content %} {% block content %}
<div class="row my-3"> <div class="row my-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Software Product {% trans "Software Product" %}
</h5> </h5>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td> <td>{{ object.description }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify }}</td> <td>{{ object.manufacturer|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Versions</th> <th scope="row">{% trans "Versions" %}</th>
<td> <td>
{% for version in object.softwareproductversion_set.all %} {% for version in object.softwareproductversion_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm"> <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}" class="btn btn-sm">
{{ version }} {{ version }}
</a> </a>
{% empty %} {% empty %}
None {% trans "None" %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Installations</th> <th scope="row">{% trans "Installations" %}</th>
<td> <td>
{% for installation in object.softwareproductinstallation_set.all %} {% for installation in object.softwareproductinstallation_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm"> <a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
{{ installation }} {{ installation }}
</a> </a>
{% empty %} {% empty %}
None {% trans "None" %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Licenses</th> <th scope="row">{% trans "Licenses" %}</th>
<td> <td>
{% for license in object.softwarelicense_set.all %} {% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm"> <a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }} {{ license }}
</a> </a>
{% empty %} {% empty %}
None {% trans "None" %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
@@ -3,48 +3,49 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block content %} {% block content %}
<div class="row my-3"> <div class="row my-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Software Product Installation {% trans "Software Product Installation" %}
</h5> </h5>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
{% if object.device %} {% if object.device %}
<tr> <tr>
<th scope="row">Device</th> <th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td> <td>{{ object.device|linkify }}</td>
</tr> </tr>
{% elif object.virtualmachine %} {% elif object.virtualmachine %}
<tr> <tr>
<th scope="row">Virtual Machine</th> <th scope="row">{% trans "Virtual Machine" %}</th>
<td>{{ object.virtualmachine|linkify }}</td> <td>{{ object.virtualmachine|linkify }}</td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<th scope="row">Cluster</th> <th scope="row">{% trans "Cluster" %}</th>
<td>{{ object.cluster|linkify }}</td> <td>{{ object.cluster|linkify }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">Software Product</th> <th scope="row">{% trans "Software Product" %}</th>
<td>{{ object.software_product|linkify }}</td> <td>{{ object.software_product|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Version</th> <th scope="row">{% trans "Version" %}</th>
<td>{{ object.version|linkify }}</td> <td>{{ object.version|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Licenses</th> <th scope="row">{% trans "Licenses" %}</th>
<td> <td>
{% for license in object.softwarelicense_set.all %} {% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm"> <a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }} {{ license }}
</a> </a>
{% empty %} {% empty %}
None {% trans "None" %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
@@ -3,37 +3,38 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block content %} {% block content %}
<div class="row my-3"> <div class="row my-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Software Product Version {% trans "Software Product Version" %}
</h5> </h5>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td> <td>{{ object.description }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.software_product.manufacturer|linkify }}</td> <td>{{ object.software_product.manufacturer|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Software Product</th> <th scope="row">{% trans "Software Product" %}</th>
<td>{{ object.software_product|linkify }}</td> <td>{{ object.software_product|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Release date</th> <th scope="row">{% trans "Release date" %}</th>
<td>{{ object.release_date }}</td> <td>{{ object.release_date }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Documentation url</th> <th scope="row">{% trans "Documentation URL" %}</th>
{% if object.documentation_url %} {% if object.documentation_url %}
<td><a href="{{ object.documentation_url }}">{{ object.documentation_url }}</a></td> <td><a href="{{ object.documentation_url }}">{{ object.documentation_url }}</a></td>
{% else %} {% else %}
@@ -41,11 +42,11 @@
{% endif %} {% endif %}
</tr> </tr>
<tr> <tr>
<th scope="row">End of support</th> <th scope="row">{% trans "End of support" %}</th>
<td>{{ object.end_of_support }}</td> <td>{{ object.end_of_support }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Filename</th> <th scope="row">{% trans "Filename" %}</th>
{% if object.file_link %} {% if object.file_link %}
<td><a href="{{ object.file_link }}">{{ object.filename }}</a></td> <td><a href="{{ object.file_link }}">{{ object.filename }}</a></td>
{% else %} {% else %}
@@ -53,34 +54,34 @@
{% endif %} {% endif %}
</tr> </tr>
<tr> <tr>
<th scope="row">File checksum</th> <th scope="row">{% trans "File checksum" %}</th>
<td>{{ object.file_checksum }}</td> <td>{{ object.file_checksum }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Release type</th> <th scope="row">{% trans "Release type" %}</th>
<td>{{ object.get_release_type_display }}</td> <td>{{ object.get_release_type_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Installations</th> <th scope="row">{% trans "Installations" %}</th>
<td> <td>
{% for installation in object.softwareproductinstallation_set.all %} {% for installation in object.softwareproductinstallation_set.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm"> <a href="{% url 'plugins:netbox_slm:softwareproductinstallation' pk=installation.pk %}" class="btn btn-sm">
{{ installation }} {{ installation }}
</a> </a>
{% empty %} {% empty %}
None {% trans "None" %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Licenses</th> <th scope="row">{% trans "Licenses" %}</th>
<td> <td>
{% for license in object.softwarelicense_set.all %} {% for license in object.softwarelicense_set.all %}
<a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm"> <a href="{% url 'plugins:netbox_slm:softwarelicense' pk=license.pk %}" class="btn btn-sm">
{{ license }} {{ license }}
</a> </a>
{% empty %} {% empty %}
None {% trans "None" %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
+7
View File
@@ -1,4 +1,5 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext, override
from netbox_slm.forms import SoftwareLicenseForm from netbox_slm.forms import SoftwareLicenseForm
from tenancy.models import Tenant, TenantGroup 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) 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): def test_software_license_rejects_tenant_from_other_group(self):
other_group = TenantGroup.objects.create(name="other group", slug="other-group") 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) other_tenant = Tenant.objects.create(name="other tenant", slug="other-tenant", group=other_group)