feat: allow module quantity input
This commit is contained in:
@@ -30,7 +30,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
|
/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.
|
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
|
dort bereits vorausgewählt. Das funktioniert sowohl für modulare Patchpanels
|
||||||
als auch für andere NetBox-Geräte mit Modulschächten.
|
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
|
Beschreibung. Optional werden die Komponenten aus den Vorlagen des Modultyps
|
||||||
für jedes Modul repliziert. NetBox prüft dabei jeden Einbau mit derselben Logik
|
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
|
wie beim einzelnen Modul. Der Vorgang ist atomar: Ist ein Schacht inzwischen
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class NetBoxUtilitiesConfig(PluginConfig):
|
|||||||
name = "netbox_utilities"
|
name = "netbox_utilities"
|
||||||
verbose_name = "NetBox Utilities"
|
verbose_name = "NetBox Utilities"
|
||||||
description = "Personal navigation, global tenant filtering, and bulk module installation"
|
description = "Personal navigation, global tenant filtering, and bulk module installation"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
author = "LKE"
|
author = "LKE"
|
||||||
base_url = "utilities"
|
base_url = "utilities"
|
||||||
min_version = "4.6.5"
|
min_version = "4.6.5"
|
||||||
|
|||||||
@@ -21,9 +21,19 @@ class BulkModuleInstallForm(forms.Form):
|
|||||||
module_bays = DynamicModelMultipleChoiceField(
|
module_bays = DynamicModelMultipleChoiceField(
|
||||||
label="Modulschächte",
|
label="Modulschächte",
|
||||||
queryset=ModuleBay.objects.all(),
|
queryset=ModuleBay.objects.all(),
|
||||||
|
required=False,
|
||||||
query_params={"device_id": "$device"},
|
query_params={"device_id": "$device"},
|
||||||
context={"disabled": "_occupied"},
|
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(
|
status = forms.ChoiceField(
|
||||||
label="Status",
|
label="Status",
|
||||||
@@ -56,12 +66,35 @@ class BulkModuleInstallForm(forms.Form):
|
|||||||
device = device.pk
|
device = device.pk
|
||||||
if device:
|
if device:
|
||||||
module_bays = module_bays.filter(device_id=device)
|
module_bays = module_bays.filter(device_id=device)
|
||||||
|
self.available_module_bays = module_bays
|
||||||
self.fields["module_bays"].queryset = module_bays
|
self.fields["module_bays"].queryset = module_bays
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
device = cleaned_data.get("device")
|
device = cleaned_data.get("device")
|
||||||
module_bays = cleaned_data.get("module_bays")
|
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:
|
if device and module_bays:
|
||||||
invalid_bays = [
|
invalid_bays = [
|
||||||
module_bay.name
|
module_bay.name
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
<h2 class="card-header">Module mehrfach einbauen</h2>
|
<h2 class="card-header">Module mehrfach einbauen</h2>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-info">
|
<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.
|
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.
|
Tritt bei einem Modul ein Fehler auf, wird keines der ausgewählten Module gespeichert.
|
||||||
</div>
|
</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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-utilities"
|
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"
|
description = "Personal navigation, global tenant filtering, and bulk module installation for NetBox 4.6"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
Reference in New Issue
Block a user