feat: integrate rack widths across plugins

This commit is contained in:
2026-08-12 17:12:19 +02:00
parent ec67e6b7d0
commit 70825c4040
14 changed files with 1267 additions and 43 deletions
+189 -7
View File
@@ -1,16 +1,21 @@
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase
from django.test import RequestFactory, SimpleTestCase
from rest_framework.exceptions import PermissionDenied
from netbox_utilities.reorder_rack import (
RackLayoutError,
apply_rack_layout,
bulk_reorder_update,
get_reorder_rack_width_data,
grid_dimensions,
horizontal_position_from_grid_x,
install_reorder_rack_bulk_save,
parse_device_placements,
reorder_grid_y,
)
@@ -32,14 +37,41 @@ class RackLayoutParsingTest(SimpleTestCase):
)
self.assertEqual(
[(item.device_id, item.position, item.face) for item in placements],
[(item.device_id, item.position, item.face, item.grid_x) for item in placements],
[
(10, Decimal(4), "front"),
(11, Decimal("8.5"), "front"),
(12, None, ""),
(10, Decimal(4), "front", None),
(11, Decimal("8.5"), "front", None),
(12, None, "", None),
],
)
def test_reads_horizontal_positions_only_from_marked_twelve_column_payload(self):
placements = parse_device_placements(
rack_layout(front=[{"id": 10, "x": 6, "y": 4}]),
grid_columns=12,
)
self.assertEqual(placements[0].grid_x, 6)
def test_maps_rack_widths_to_twelve_column_grid(self):
self.assertEqual(grid_dimensions(1, 1), (12, 0))
self.assertEqual(grid_dimensions(2, 2), (6, 6))
self.assertEqual(grid_dimensions(3, 2), (4, 4))
self.assertEqual(grid_dimensions(4, 4), (3, 9))
def test_maps_grid_x_back_to_horizontal_position(self):
self.assertEqual(horizontal_position_from_grid_x(2, 6), 2)
self.assertEqual(horizontal_position_from_grid_x(3, 8), 3)
self.assertEqual(horizontal_position_from_grid_x(4, 9), 4)
def test_rejects_misaligned_grid_position(self):
with self.assertRaisesMessage(RackLayoutError, "nicht zulässig"):
horizontal_position_from_grid_x(2, 3)
def test_rejects_non_integer_grid_position(self):
with self.assertRaisesMessage(RackLayoutError, "ganze Zahl"):
horizontal_position_from_grid_x(2, 6.0)
def test_rejects_duplicate_devices(self):
with self.assertRaisesMessage(RackLayoutError, "mehrfach"):
parse_device_placements(
@@ -55,9 +87,15 @@ class RackLayoutParsingTest(SimpleTestCase):
class ApplyRackLayoutTest(SimpleTestCase):
@patch("netbox_utilities.reorder_rack.DeviceRackPlacement")
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
@patch("netbox_utilities.reorder_rack.Device")
def test_clears_all_old_positions_before_saving_targets(self, device_model, _get_permission):
def test_clears_all_old_positions_before_saving_targets(
self,
device_model,
_get_permission,
placement_model,
):
events = []
first = MagicMock(pk=10, position=Decimal(1), face="front")
second = MagicMock(pk=11, position=Decimal(2), face="front")
@@ -70,6 +108,7 @@ class ApplyRackLayoutTest(SimpleTestCase):
device_model.objects.select_for_update.return_value.filter.return_value = [first, second]
device_model.objects.filter.return_value.update.side_effect = lambda **_kwargs: events.append("clear")
placement_model.objects.select_for_update.return_value.filter.return_value = []
user = MagicMock()
user.has_perm.return_value = True
placements = parse_device_placements(rack_layout(front=[{"id": 10, "y": 2}, {"id": 11, "y": 1}]))
@@ -83,11 +122,13 @@ class ApplyRackLayoutTest(SimpleTestCase):
self.assertEqual((first.position, first.face), (Decimal(2), "front"))
self.assertEqual((second.position, second.face), (Decimal(1), "front"))
@patch("netbox_utilities.reorder_rack.DeviceRackPlacement")
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
@patch("netbox_utilities.reorder_rack.Device")
def test_rejects_changed_device_without_permission(self, device_model, _get_permission):
def test_rejects_changed_device_without_permission(self, device_model, _get_permission, placement_model):
device = MagicMock(pk=10, position=Decimal(1), face="front")
device_model.objects.select_for_update.return_value.filter.return_value = [device]
placement_model.objects.select_for_update.return_value.filter.return_value = []
user = MagicMock()
user.has_perm.return_value = False
placements = parse_device_placements(rack_layout(front=[{"id": 10, "y": 2}]))
@@ -98,6 +139,147 @@ class ApplyRackLayoutTest(SimpleTestCase):
device_model.objects.filter.assert_not_called()
device.save.assert_not_called()
@patch("netbox_utilities.reorder_rack.DeviceRackPlacement")
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
@patch("netbox_utilities.reorder_rack.Device")
def test_saves_horizontal_only_move_for_partial_width_device(
self,
device_model,
_get_permission,
placement_model,
):
device = MagicMock(pk=10, position=Decimal(4), face="front")
stored = SimpleNamespace(device_id=10, width=2, horizontal_position=1)
device_model.objects.select_for_update.return_value.filter.return_value = [device]
placement_model.objects.select_for_update.return_value.filter.return_value = [stored]
user = MagicMock()
user.has_perm.return_value = True
placements = parse_device_placements(
rack_layout(front=[{"id": 10, "x": 6, "y": 4}]),
grid_columns=12,
)
changed = apply_rack_layout(rack=SimpleNamespace(pk=5), placements=placements, user=user)
self.assertEqual(changed, [device])
self.assertEqual(device._netbox_utilities_rack_width, 2)
self.assertEqual(device._netbox_utilities_horizontal_position, 2)
placement_model.objects.update_or_create.assert_called_once_with(
device=device,
defaults={"width": 2, "horizontal_position": 2},
)
@patch("netbox_utilities.reorder_rack.DeviceRackPlacement")
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
@patch("netbox_utilities.reorder_rack.Device")
def test_legacy_payload_preserves_stored_horizontal_position(
self,
device_model,
_get_permission,
placement_model,
):
device = MagicMock(pk=10, position=Decimal(4), face="front")
stored = SimpleNamespace(device_id=10, width=2, horizontal_position=2)
device_model.objects.select_for_update.return_value.filter.return_value = [device]
placement_model.objects.select_for_update.return_value.filter.return_value = [stored]
user = MagicMock()
user.has_perm.return_value = True
placements = parse_device_placements(rack_layout(front=[{"id": 10, "x": 0, "y": 5}]))
changed = apply_rack_layout(rack=SimpleNamespace(pk=5), placements=placements, user=user)
self.assertEqual(changed, [device])
placement_model.objects.update_or_create.assert_called_once_with(
device=device,
defaults={"width": 2, "horizontal_position": 2},
)
class ReorderRackFrontendTest(SimpleTestCase):
def test_grid_y_matches_reorder_plugin_for_both_numbering_directions(self):
self.assertEqual(reorder_grid_y(SimpleNamespace(u_height=42, desc_units=True), 4, 2), 6)
self.assertEqual(reorder_grid_y(SimpleNamespace(u_height=42, desc_units=False), 4, 2), 74)
def test_width_adapter_runs_before_gridstack_and_preserves_original_save(self):
script = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "reorder-rack-width.js").read_text(
encoding="utf-8"
)
self.assertIn("setAttribute(grid, 'gs-column', columns)", script)
self.assertIn("setAttribute(item, 'gs-w', device.grid_width)", script)
self.assertIn("setAttribute(item, 'gs-x', device.grid_x)", script)
self.assertIn("grid.on('dragstop'", script)
self.assertIn("grid.on('dropped'", script)
self.assertIn("closest('#saveButton')", script)
self.assertIn("X-NetBox-Utilities-Rack-Grid-Columns", script)
self.assertNotIn("JSON.stringify", script)
@patch("netbox_utilities.reorder_rack.reorder_rack_width_enabled", return_value=True)
@patch("netbox_utilities.reorder_rack.Rack")
def test_malformed_partial_placement_fails_open(self, rack_model, _enabled):
request = RequestFactory().get("/dcim/racks/5/reorder/")
request.resolver_match = SimpleNamespace(kwargs={"pk": 5}, view_name="dcim:rack_reorder")
request.user = MagicMock()
rack_model.objects.restrict.return_value.filter.return_value.first.return_value = SimpleNamespace(pk=5)
with patch("netbox_utilities.reorder_rack.Device.objects.restrict", side_effect=AttributeError("stale")):
self.assertIsNone(get_reorder_rack_width_data(request))
@patch("netbox_utilities.reorder_rack.reorder_rack_width_enabled", return_value=True)
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
@patch("netbox_utilities.reorder_rack.Device")
@patch("netbox_utilities.reorder_rack.Rack")
def test_describes_partial_devices_for_reorder_grid(
self,
rack_model,
device_model,
_get_permission,
_enabled,
):
request = RequestFactory().get("/dcim/racks/5/reorder/?view=labels-only")
request.resolver_match = SimpleNamespace(kwargs={"pk": 5}, view_name="dcim:rack_reorder")
request.user = MagicMock()
request.user.has_perm.return_value = True
rack = SimpleNamespace(pk=5, u_height=42, desc_units=False)
rack_model.objects.restrict.return_value.filter.return_value.first.return_value = rack
placement = SimpleNamespace(width=2, horizontal_position=2)
device = SimpleNamespace(
pk=10,
name="Fritzbox",
virtual_chassis=None,
device_type=SimpleNamespace(
u_height=1,
is_full_depth=False,
front_image=None,
rear_image=None,
),
role=SimpleNamespace(color="abcdef"),
face="front",
position=Decimal(4),
netbox_utilities_rack_placement=placement,
)
device_queryset = MagicMock()
device_queryset.filter.return_value.select_related.return_value.order_by.return_value = [device]
device_model.objects.restrict.return_value = device_queryset
with (
patch("dcim.svg.racks.get_device_name", return_value="Fritzbox"),
patch(
"netbox.config.get_config",
return_value=SimpleNamespace(RACK_ELEVATION_DEFAULT_UNIT_WIDTH=220),
),
patch("utilities.html.foreground_color", return_value="000000"),
):
result = get_reorder_rack_width_data(request)
self.assertEqual(result["columns"], 12)
self.assertFalse(result["images"])
self.assertTrue(result["labels"])
self.assertEqual(
{key: result["devices"][0][key] for key in ("id", "grid_x", "grid_width", "width", "horizontal_position")},
{"id": 10, "grid_x": 6, "grid_width": 6, "width": 2, "horizontal_position": 2},
)
class ReorderRackPatchTest(SimpleTestCase):
def test_patches_supported_plugin_only_once(self):