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
+7 -2
View File
@@ -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 (1x)** 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
+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)
+1 -1
View File
@@ -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"