From 71a5db0ece912bdb415c276df4ba787188c1187f Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 27 Jul 2026 12:28:26 +0200 Subject: [PATCH] fix: clarify installation platform selection Group device, virtual machine, and cluster as the required platform destination and validate that exactly one is selected with an actionable localized error. --- CHANGELOG.md | 4 +++ README.md | 2 +- netbox_slm/__init__.py | 2 +- .../forms/software_product_installation.py | 29 ++++++++++++++++-- netbox_slm/locale/de/LC_MESSAGES/django.mo | Bin 5693 -> 6282 bytes netbox_slm/locale/de/LC_MESSAGES/django.po | 15 +++++++++ netbox_slm/models.py | 4 ++- netbox_slm/tests/test_models.py | 21 ++++++++++++- 8 files changed, 71 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f74964..4c34f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +* Make the required single platform destination explicit when creating an installation + ### Added * Optional tenant group and tenant assignments for software licenses diff --git a/README.md b/README.md index f5a672f..26b7157 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,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.11.0` stehen. +Für diese Variante muss dort mindestens Version `1.11.1` stehen. ### 3. Plugin in NetBox aktivieren diff --git a/netbox_slm/__init__.py b/netbox_slm/__init__.py index 6a5dcc2..dae1884 100644 --- a/netbox_slm/__init__.py +++ b/netbox_slm/__init__.py @@ -17,7 +17,7 @@ limitations under the License. from netbox.plugins import PluginConfig from django.utils.translation import gettext_lazy as _ -__version__ = "1.11.0" +__version__ = "1.11.1" class SLMConfig(PluginConfig): diff --git a/netbox_slm/forms/software_product_installation.py b/netbox_slm/forms/software_product_installation.py index 9bdcb52..ebed762 100644 --- a/netbox_slm/forms/software_product_installation.py +++ b/netbox_slm/forms/software_product_installation.py @@ -18,14 +18,28 @@ from virtualization.models import Cluster, VirtualMachine class SoftwareProductInstallationForm(NetBoxModelForm): comments = CommentField() + fieldsets = ( + FieldSet("device", "virtualmachine", "cluster", name=_("Platform Destination (select exactly one)")), + FieldSet("software_product", "version", name=_("Software")), + FieldSet("tags", name=_("Tags")), + ) - device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False) + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + help_text=_("Select either a device, a virtual machine, or a cluster."), + ) virtualmachine = DynamicModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, label=_("Virtual Machine"), + help_text=_("Leave empty when another platform destination is selected."), + ) + cluster = DynamicModelChoiceField( + queryset=Cluster.objects.all(), + required=False, + help_text=_("Leave empty when another platform destination is selected."), ) - cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False) software_product = DynamicModelChoiceField( queryset=SoftwareProduct.objects.all(), required=True, @@ -60,6 +74,17 @@ class SoftwareProductInstallationForm(NetBoxModelForm): ) return version + def clean(self): + cleaned_data = super().clean() + destinations = [ + cleaned_data.get("device"), + cleaned_data.get("virtualmachine"), + cleaned_data.get("cluster"), + ] + if sum(destination is not None for destination in destinations) != 1: + raise ValidationError(_("Select exactly one platform destination: device, virtual machine, or cluster.")) + return cleaned_data + class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm): model = SoftwareProductInstallation diff --git a/netbox_slm/locale/de/LC_MESSAGES/django.mo b/netbox_slm/locale/de/LC_MESSAGES/django.mo index 8dc742699b946c2663fb56e3addc154a69722c6b..6e8442e2f24abd7a1ceaa015d331fab67f95875b 100644 GIT binary patch delta 2334 zcmZwHaZHs}7{~FWB4Pp}Nnjwpwq(%g0vSu#pj?JEax!hRrIqjC2zOoHOZUE)D{XcA zLuVsvOUgf5WhAavW-BU-%CY%}xixFqr22zRRmJ2s%%BPy zL&Z6PiaUwgcMes^1?1P{&Nrrr{AM8!s@y{@T!Bh>i<@u4*^FD=cs15AZbK#NL!IbJ z)c!rreRvb&gII(U*oZ%&_ElbE%qsGm2oLM98&%l{s0~N37{A1&IEA&SW9a>xQS+@> zhFegDq);c{g={fg>IP^RDiX} zQqzH2|DYSkP=NE zz)DD%DzPf zK8*_WJ1)aqqUaw_6{S|778~%(r`u^9Gg`!L& zYQf#81RZz}Zbf~a-gNID!p)37La>45AXe=o~=>IOO~UweNG($-hDE`w^A!Jn9N{FUm6)bwy>oP`jxwrT=un zv@x+9H=|nrI4a<7RKS-}t$rKzH9U^QHfK=Zf@#$HQU*#~jVgR4>aJT*ooIJ%M8(}) z#)De>h+y)K&!qk$hr z4S4au)(nm1jkjiZdN!Jk?-RH` z+G%S&-|LSi!jvELdi~CxXuwoF!E9$Nl?-hnVm4+s4o%NnUe|ni*Z=LgvLn7NO#2Bt zx_$nE+!o7mVtz7dgIX_bgMbor&Mupv?u2uYhzf+ delta 1771 zcmYM!c}P`36vy#X?piL-Qri@Ts6Yt*rf)v6mA}B}+r0=f=lI#84nL9J*%$fIILrzzg_rVt$F#dM% zKb`-bA!_~q>kK!`rrCwd@EdA~Kf){#7h)I|V#9PQ-4E#Am39FL46C!v**mH7=b&N@Ol-ej#eTwW#r1PzmfpF011fk4G?+ z_!i(n16xpmuKj&MA~>FQ(6t|+0(78u(1)7$-Z_k!w11%jWJHrs_xMeY0wvXtFKCD`uXccbP# z!H4(?v+<}u! zde?v289+AZSql#ecmb#4ZA`{q)Y%PSE{@P};->e%Q2|2uK&-$-)VE|K z>U||{z$(suE+)aAs@aSX60KQSUQQiDo7RqazNC gYwCzR(X=P3JJ=I_IlSp&!r`WszMsL5i8n(30D;n;X8-^I diff --git a/netbox_slm/locale/de/LC_MESSAGES/django.po b/netbox_slm/locale/de/LC_MESSAGES/django.po index 3382503..4662037 100644 --- a/netbox_slm/locale/de/LC_MESSAGES/django.po +++ b/netbox_slm/locale/de/LC_MESSAGES/django.po @@ -321,3 +321,18 @@ msgstr "Erneuerungsintervall und Einheit müssen gemeinsam angegeben werden." msgid "A lifetime license cannot have a renewal interval or expiration date." msgstr "Eine Lifetime-Lizenz darf kein Erneuerungsintervall und kein Ablaufdatum haben." + +msgid "Platform Destination (select exactly one)" +msgstr "Plattformziel (genau eines auswählen)" + +msgid "Software" +msgstr "Software" + +msgid "Select either a device, a virtual machine, or a cluster." +msgstr "Wähle entweder ein Gerät, eine virtuelle Maschine oder einen Cluster aus." + +msgid "Leave empty when another platform destination is selected." +msgstr "Leer lassen, wenn ein anderes Plattformziel ausgewählt wurde." + +msgid "Select exactly one platform destination: device, virtual machine, or cluster." +msgstr "Wähle genau ein Plattformziel aus: Gerät, virtuelle Maschine oder Cluster." diff --git a/netbox_slm/models.py b/netbox_slm/models.py index 4dbd851..fce3596 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -146,7 +146,9 @@ 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=_( + "Select exactly one platform destination: device, virtual machine, or cluster." + ), ) ] diff --git a/netbox_slm/tests/test_models.py b/netbox_slm/tests/test_models.py index 8b550c8..31d816c 100644 --- a/netbox_slm/tests/test_models.py +++ b/netbox_slm/tests/test_models.py @@ -1,6 +1,6 @@ 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, SoftwareProductInstallationForm from netbox_slm.models import RenewalIntervalUnits, SoftwareLicense from tenancy.models import Tenant, TenantGroup @@ -56,6 +56,25 @@ class ModelTestCase(SlmBaseTestCase): self.assertEqual("cluster", self.software_product_installation.render_type()) self.assertEqual(self.cluster, self.software_product_installation.platform) + def test_installation_form_requires_exactly_one_platform_destination(self): + base_data = { + "software_product": self.software_product.pk, + "version": self.software_product_version.pk, + } + + no_destination_form = SoftwareProductInstallationForm(data=base_data) + self.assertFalse(no_destination_form.is_valid()) + self.assertIn("Select exactly one platform destination", str(no_destination_form.non_field_errors())) + + one_destination_form = SoftwareProductInstallationForm(data={**base_data, "device": self.device.pk}) + self.assertTrue(one_destination_form.is_valid(), one_destination_form.errors) + + multiple_destinations_form = SoftwareProductInstallationForm( + data={**base_data, "device": self.device.pk, "cluster": self.cluster.pk} + ) + self.assertFalse(multiple_destinations_form.is_valid()) + self.assertIn("Select exactly one platform destination", str(multiple_destinations_form.non_field_errors())) + def test_software_license_stored_location_txt(self): self.assertEqual("Link", self.software_license.stored_location_txt)