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 8dc7426..6e8442e 100644 Binary files a/netbox_slm/locale/de/LC_MESSAGES/django.mo and b/netbox_slm/locale/de/LC_MESSAGES/django.mo differ 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)