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.
This commit is contained in:
2026-07-27 12:28:26 +02:00
parent adfc2ae72c
commit 71a5db0ece
8 changed files with 71 additions and 6 deletions
+1 -1
View File
@@ -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):
@@ -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
Binary file not shown.
@@ -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."
+3 -1
View File
@@ -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."
),
)
]
+20 -1
View File
@@ -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)