From 503df48ac7d041ae87281e32472363ed9eeebede Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 30 Jul 2026 16:10:15 +0200 Subject: [PATCH] feat: allow module quantity input --- README.md | 9 ++- netbox_utilities/__init__.py | 2 +- netbox_utilities/forms.py | 35 ++++++++++- .../netbox_utilities/bulk_module_install.html | 4 +- .../tests/test_bulk_module_form.py | 58 +++++++++++++++++++ pyproject.toml | 2 +- 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 netbox_utilities/tests/test_bulk_module_form.py diff --git a/README.md b/README.md index 39725d4..6a841fb 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden: ```bash /opt/netbox/venv/bin/pip install --upgrade --force-reinstall \ - "git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.3.0" + "git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.3.1" ``` Alternativ kann hinter dem `@` die vollständige Commit-ID stehen. @@ -135,7 +135,12 @@ Auf Geräteseiten mit freien Modulschächten steht dieselbe Funktion zusätzlich dort bereits vorausgewählt. Das funktioniert sowohl für modulare Patchpanels als auch für andere NetBox-Geräte mit Modulschächten. -Alle ausgewählten Schächte erhalten denselben Modultyp, Status und dieselbe +Der Benutzer kann entweder bestimmte freie Modulschächte auswählen oder über +**Anzahl (1–x)** eine Menge eingeben. Bei der Mengenangabe verwendet das Plugin +die ersten freien Modulschächte in der natürlichen NetBox-Reihenfolge. Beide +Eingabearten können nicht miteinander kombiniert werden. + +Alle gewählten Schächte erhalten denselben Modultyp, Status und dieselbe Beschreibung. Optional werden die Komponenten aus den Vorlagen des Modultyps für jedes Modul repliziert. NetBox prüft dabei jeden Einbau mit derselben Logik wie beim einzelnen Modul. Der Vorgang ist atomar: Ist ein Schacht inzwischen diff --git a/netbox_utilities/__init__.py b/netbox_utilities/__init__.py index d36019c..eb279d5 100644 --- a/netbox_utilities/__init__.py +++ b/netbox_utilities/__init__.py @@ -5,7 +5,7 @@ class NetBoxUtilitiesConfig(PluginConfig): name = "netbox_utilities" verbose_name = "NetBox Utilities" description = "Personal navigation, global tenant filtering, and bulk module installation" - version = "0.3.0" + version = "0.3.1" author = "LKE" base_url = "utilities" min_version = "4.6.5" diff --git a/netbox_utilities/forms.py b/netbox_utilities/forms.py index d3fa9f0..aad19a7 100644 --- a/netbox_utilities/forms.py +++ b/netbox_utilities/forms.py @@ -21,9 +21,19 @@ class BulkModuleInstallForm(forms.Form): module_bays = DynamicModelMultipleChoiceField( label="Modulschächte", queryset=ModuleBay.objects.all(), + required=False, query_params={"device_id": "$device"}, context={"disabled": "_occupied"}, - help_text="Alle ausgewählten Modulschächte erhalten ein Modul des gewählten Typs.", + help_text="Bestimmte freie Schächte auswählen. Alternativ kann die Anzahl verwendet werden.", + ) + quantity = forms.IntegerField( + label="Anzahl (1–x)", + required=False, + min_value=1, + help_text=( + "Baut die angegebene Anzahl automatisch in die ersten freien Modulschächte ein. " + "Nicht zusammen mit einer manuellen Schachtauswahl verwenden." + ), ) status = forms.ChoiceField( label="Status", @@ -56,12 +66,35 @@ class BulkModuleInstallForm(forms.Form): device = device.pk if device: module_bays = module_bays.filter(device_id=device) + self.available_module_bays = module_bays self.fields["module_bays"].queryset = module_bays def clean(self): cleaned_data = super().clean() device = cleaned_data.get("device") module_bays = cleaned_data.get("module_bays") + quantity = cleaned_data.get("quantity") + + if module_bays and quantity: + self.add_error("quantity", "Anzahl und manuelle Schachtauswahl können nicht kombiniert werden.") + elif not module_bays and not quantity and "module_bays" not in self.errors and "quantity" not in self.errors: + self.add_error(None, "Wählen Sie Modulschächte aus oder geben Sie eine Anzahl ein.") + elif quantity and device: + available_bays = self.available_module_bays + available_count = available_bays.count() + if quantity > available_count: + availability = ( + "nur ein freier Modulschacht" + if available_count == 1 + else f"nur {available_count} freie Modulschächte" + ) + self.add_error( + "quantity", + f"Für dieses Gerät sind {availability} verfügbar.", + ) + else: + cleaned_data["module_bays"] = list(available_bays[:quantity]) + if device and module_bays: invalid_bays = [ module_bay.name diff --git a/netbox_utilities/templates/netbox_utilities/bulk_module_install.html b/netbox_utilities/templates/netbox_utilities/bulk_module_install.html index 65d1ba3..0eb51ad 100644 --- a/netbox_utilities/templates/netbox_utilities/bulk_module_install.html +++ b/netbox_utilities/templates/netbox_utilities/bulk_module_install.html @@ -14,7 +14,9 @@

Module mehrfach einbauen

- In alle ausgewählten Modulschächte wird derselbe Modultyp eingebaut. + Wählen Sie bestimmte Modulschächte aus oder geben Sie eine Anzahl von 1 bis x ein. + Bei einer Anzahl werden die ersten freien Schächte automatisch verwendet. + In alle gewählten Schächte wird derselbe Modultyp eingebaut. Seriennummern und Asset-Tags können anschließend je Modul ergänzt werden. Tritt bei einem Modul ein Fehler auf, wird keines der ausgewählten Module gespeichert.
diff --git a/netbox_utilities/tests/test_bulk_module_form.py b/netbox_utilities/tests/test_bulk_module_form.py new file mode 100644 index 0000000..f09b561 --- /dev/null +++ b/netbox_utilities/tests/test_bulk_module_form.py @@ -0,0 +1,58 @@ +from types import SimpleNamespace + +from django import forms +from django.forms.utils import ErrorDict +from django.test import SimpleTestCase + +from netbox_utilities.forms import BulkModuleInstallForm + + +class AvailableModuleBays(list): + def count(self): + return len(self) + + +class BulkModuleInstallFormTest(SimpleTestCase): + def _form_for_clean(self, *, module_bays=None, quantity=None, available_bays=None): + form = BulkModuleInstallForm.__new__(BulkModuleInstallForm) + forms.Form.__init__(form) + form._errors = ErrorDict() + form.cleaned_data = { + "device": SimpleNamespace(pk=10), + "module_bays": module_bays or [], + "quantity": quantity, + } + form.available_module_bays = AvailableModuleBays(available_bays or []) + return form + + def test_quantity_selects_first_available_bays(self): + available_bays = [SimpleNamespace(pk=1), SimpleNamespace(pk=2), SimpleNamespace(pk=3)] + form = self._form_for_clean(quantity=2, available_bays=available_bays) + + cleaned_data = form.clean() + + self.assertEqual(cleaned_data["module_bays"], available_bays[:2]) + self.assertFalse(form.errors) + + def test_quantity_cannot_exceed_available_bays(self): + form = self._form_for_clean(quantity=3, available_bays=[SimpleNamespace(pk=1)]) + + form.clean() + + self.assertIn("quantity", form.errors) + self.assertIn("nur ein freier Modulschacht", str(form.errors["quantity"])) + + def test_quantity_and_manual_selection_are_mutually_exclusive(self): + module_bay = SimpleNamespace(pk=1, name="A1", device_id=10, enabled=True) + form = self._form_for_clean(module_bays=[module_bay], quantity=1, available_bays=[module_bay]) + + form.clean() + + self.assertIn("quantity", form.errors) + + def test_requires_quantity_or_manual_selection(self): + form = self._form_for_clean() + + form.clean() + + self.assertIn("__all__", form.errors) diff --git a/pyproject.toml b/pyproject.toml index a90fae6..fd2731e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "netbox-utilities" -version = "0.3.0" +version = "0.3.1" description = "Personal navigation, global tenant filtering, and bulk module installation for NetBox 4.6" readme = "README.md" requires-python = ">=3.12"