feat: allow module quantity input

This commit is contained in:
2026-07-30 16:10:15 +02:00
parent 8a86ca28f7
commit 503df48ac7
6 changed files with 104 additions and 6 deletions
+1 -1
View File
@@ -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"
+34 -1
View File
@@ -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 (1x)",
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
@@ -14,7 +14,9 @@
<h2 class="card-header">Module mehrfach einbauen</h2>
<div class="card-body">
<div class="alert alert-info">
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.
</div>
@@ -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)