fix: restore populated rack and reorder views

This commit is contained in:
2026-08-13 09:37:59 +02:00
parent bc2024ac13
commit a5f8cb62c7
12 changed files with 379 additions and 67 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.9.2"
__version__ = "0.9.3"
class NetBoxUtilitiesConfig(PluginConfig):
+68 -10
View File
@@ -1,5 +1,6 @@
import decimal
import logging
from collections import defaultdict
from contextvars import ContextVar
from copy import deepcopy
from fractions import Fraction
@@ -114,6 +115,65 @@ def get_width_position(device, placements=None):
return _stored_width_position(device)
def effective_width_positions(devices, placements=None):
"""Return stored rack widths and infer missing rows for devices sharing one slot."""
devices = list(devices)
groups = defaultdict(list)
result = {}
for device in devices:
key = (
device.rack_id,
device.face,
decimal.Decimal(str(device.position)),
decimal.Decimal(str(device.device_type.u_height)),
)
groups[key].append(device)
for group in groups.values():
stored = {}
missing = []
for device in group:
if placements is not None:
placement = placements.get(device.pk)
else:
try:
placement = device.netbox_utilities_rack_placement
except ObjectDoesNotExist:
placement = None
if placement is None:
missing.append(device)
else:
stored[device.pk] = normalize_width_position(placement.width, placement.horizontal_position)
candidate_width = None
stored_widths = {width for width, _position in stored.values()}
if len(stored_widths) == 1:
stored_width = next(iter(stored_widths))
if len(group) <= stored_width:
candidate_width = stored_width
elif not stored and 2 <= len(group) <= 4:
candidate_width = len(group)
if candidate_width is not None:
used_positions = {position for width, position in stored.values() if width == candidate_width}
free_positions = iter(
position for position in range(1, candidate_width + 1) if position not in used_positions
)
for device in sorted(missing, key=lambda item: item.pk):
position = next(free_positions, None)
if position is None:
break
result[device.pk] = (candidate_width, position, "inferred")
for device_id, (width, position) in stored.items():
result[device_id] = (width, position, "stored")
for device in missing:
result.setdefault(device.pk, (FULL_WIDTH, 1, "default"))
return result
def stage_width_position(device, width, horizontal_position):
width, horizontal_position = normalize_width_position(width, horizontal_position)
device._netbox_utilities_rack_width = width
@@ -432,7 +492,7 @@ def _install_device_form():
def _install_rack_svg():
from dcim.svg.racks import RackElevationSVG
if getattr(RackElevationSVG, "_netbox_utilities_rack_width_version", None) == 3:
if getattr(RackElevationSVG, "_netbox_utilities_rack_width_version", None) == 4:
return
current_draw_face = RackElevationSVG.draw_face
current_globals = getattr(current_draw_face, "__globals__", {})
@@ -448,16 +508,14 @@ def _install_rack_svg():
# NetBox's get_rack_units() stores only one device per rack unit and
# therefore cannot represent two devices mounted beside each other.
# Query every mounted device directly and render each exactly once.
for device in _rack_elevation_devices(elevation.rack, face):
try:
placement = device.netbox_utilities_rack_placement
except ObjectDoesNotExist:
placement = None
devices = list(_rack_elevation_devices(elevation.rack, face))
widths = effective_width_positions(devices)
for device in devices:
width_divisor, horizontal_position, _source = widths[device.pk]
height = decimal.Decimal(str(device.device_type.u_height))
coords = elevation._get_device_coords(device.position, height)
width = elevation.unit_width if placement is None else elevation.unit_width / placement.width
if placement is not None:
coords = (coords[0] + width * (placement.horizontal_position - 1), coords[1])
width = elevation.unit_width / width_divisor
coords = (coords[0] + width * (horizontal_position - 1), coords[1])
size = (width, int(elevation.unit_height * height))
if device.pk in elevation.permitted_device_ids:
if device.face == face and not opposite:
@@ -469,7 +527,7 @@ def _install_rack_svg():
RackElevationSVG.draw_face = width_aware_draw_face
RackElevationSVG._netbox_utilities_rack_width_installed = True
RackElevationSVG._netbox_utilities_rack_width_version = 3
RackElevationSVG._netbox_utilities_rack_width_version = 4
def _rack_elevation_devices(rack, face):
+14 -15
View File
@@ -16,7 +16,7 @@ from rest_framework.response import Response
from utilities.permissions import get_permission_for_model
from .models import DeviceRackPlacement
from .rack_width import FULL_WIDTH, normalize_width_position, stage_width_position
from .rack_width import FULL_WIDTH, effective_width_positions, normalize_width_position, stage_width_position
logger = logging.getLogger(__name__)
@@ -116,11 +116,14 @@ def apply_rack_layout(*, rack, placements, user):
placement.device_id: placement
for placement in DeviceRackPlacement.objects.select_for_update().filter(device_id__in=placement_by_id)
}
mounted_devices = [device for device in devices.values() if device.position is not None and device.position > 0]
effective_placements = effective_width_positions(mounted_devices, stored_placements)
target_placements = []
for placement in placements:
stored = stored_placements.get(placement.device_id)
current_width = stored.width if stored else FULL_WIDTH
current_horizontal_position = stored.horizontal_position if stored else 1
current_width, current_horizontal_position, _source = effective_placements.get(
placement.device_id,
(FULL_WIDTH, 1, "default"),
)
if placement.position is None:
target_width, target_horizontal_position = FULL_WIDTH, 1
elif placement.grid_x is None:
@@ -238,16 +241,6 @@ def _device_label(device):
return device.label or str(device.device_type)
def _device_width_position(device):
try:
placement = device.netbox_utilities_rack_placement
except ObjectDoesNotExist:
placement = None
if placement is None:
return FULL_WIDTH, 1
return normalize_width_position(placement.width, placement.horizontal_position)
def _empty_reorder_rack_width_data(request, *, status="ready"):
selected_view = request.GET.get("view", "images-and-labels")
return {
@@ -257,6 +250,8 @@ def _empty_reorder_rack_width_data(request, *, status="ready"):
"labels": selected_view != "images-only",
"devices": [],
"status": status,
"schema_version": 3,
"complete": False,
}
@@ -294,9 +289,11 @@ def get_reorder_rack_width_data(request):
)
.order_by("position", "pk")
)
devices = list(devices)
widths = effective_width_positions(devices)
permission = get_permission_for_model(Device, "change")
for device in devices:
width, horizontal_position = _device_width_position(device)
width, horizontal_position, width_source = widths[device.pk]
grid_width, grid_x = grid_dimensions(width, horizontal_position)
role_color = device.role.color or "1685fc"
grid_height = int(Decimal(str(device.device_type.u_height)) * 2)
@@ -312,6 +309,7 @@ def get_reorder_rack_width_data(request):
"grid_height": grid_height,
"width": width,
"horizontal_position": horizontal_position,
"width_source": width_source,
"color": role_color,
"text_color": foreground_color(role_color),
"front_image": _image_url(device.device_type.front_image),
@@ -320,6 +318,7 @@ def get_reorder_rack_width_data(request):
}
)
data["unit_width"] = get_config().RACK_ELEVATION_DEFAULT_UNIT_WIDTH
data["complete"] = True
return data
except (
AttributeError,
@@ -15,6 +15,22 @@
console.warn('NetBox Utilities is leaving Reorder Rack in its native layout.', data.status);
return;
}
if (data.schema_version !== 3 || data.complete !== true || !Array.isArray(data.devices)) {
console.warn('NetBox Utilities received an incomplete Reorder Rack payload and kept the native layout.');
return;
}
const validDeviceData = data.devices.every(device => (
Number.isInteger(Number(device.id))
&& ['front', 'rear'].includes(device.face)
&& Number.isInteger(Number(device.grid_x))
&& Number.isInteger(Number(device.grid_y))
&& Number.isInteger(Number(device.grid_width))
&& Number.isInteger(Number(device.grid_height))
));
if (!validDeviceData) {
console.warn('NetBox Utilities received invalid Reorder Rack device data and kept the native layout.');
return;
}
const columns = Number.parseInt(data.columns, 10) || 12;
const gridElements = {
@@ -24,6 +40,15 @@
};
if (!gridElements.front || !gridElements.rear || !gridElements.other) return;
const nativeMountedItems = [
...gridElements.front.querySelectorAll('.grid-stack-item'),
...gridElements.rear.querySelectorAll('.grid-stack-item'),
];
if (!data.devices.length && nativeMountedItems.length) {
console.warn('NetBox Utilities received no replacement devices and kept the populated native layout.');
return;
}
const setAttribute = (element, name, value) => element.setAttribute(name, String(value));
const originalFetch = window.fetch.bind(window);
window.fetch = (resource, options = {}) => {
@@ -11,6 +11,10 @@
console.error('NetBox Utilities could not read the Topology Views rack widths.', error);
return;
}
if (!Array.isArray(payload) && (payload?.schema_version !== 3 || payload?.complete !== true)) {
console.warn('NetBox Utilities received an incomplete Topology Views payload and kept the native layout.');
return;
}
if (!Array.isArray(payload) && payload?.status && payload.status !== 'ready') {
console.warn('NetBox Utilities is leaving Topology Views in its native layout.', payload.status);
return;
@@ -36,6 +40,7 @@
return {
width,
horizontalPosition,
source: String(value.width_source || 'stored'),
leftPercent: Number.isFinite(Number(value.left_percent))
? Number(value.left_percent)
: (horizontalPosition - 1) / width * 100,
@@ -87,6 +92,7 @@
device.classList.add('netbox-utilities-partial-width');
device.dataset.rackWidth = String(placement.width);
device.dataset.horizontalPosition = String(placement.horizontalPosition);
device.dataset.rackWidthSource = placement.source;
device.style.setProperty('--netbox-utilities-rack-device-left', `${placement.leftPercent}%`);
device.style.setProperty('--netbox-utilities-rack-device-width', `${placement.widthPercent}%`);
});
@@ -28,6 +28,8 @@ class OptionalRackIntegrationHeadTest(SimpleTestCase):
"labels": True,
"devices": [],
"status": "ready",
"schema_version": 3,
"complete": True,
}
with (
patch("netbox_utilities.template_content.get_reorder_rack_width_data", return_value=data),
@@ -40,7 +42,7 @@ class OptionalRackIntegrationHeadTest(SimpleTestCase):
self.assertIn('"status": "ready"', html)
def test_topology_adapter_is_emitted_for_an_empty_enabled_payload(self):
data = {"devices": [], "status": "ready"}
data = {"devices": [], "status": "ready", "schema_version": 3, "complete": True}
with (
patch("netbox_utilities.template_content.get_reorder_rack_width_data", return_value=None),
patch("netbox_utilities.template_content.get_topology_rack_width_data", return_value=data),
+60
View File
@@ -17,6 +17,7 @@ from netbox_utilities.rack_width import (
_install_rack_methods,
_rack_elevation_devices,
available_units_for_device,
effective_width_positions,
horizontal_interval,
include_rack_width_in_fieldsets,
intervals_cover_full_width,
@@ -84,6 +85,7 @@ class RackWidthTest(SimpleTestCase):
regular = SimpleNamespace(
pk=1,
rack_id=3,
face="front",
position=12,
device_type=SimpleNamespace(u_height=1),
@@ -91,6 +93,7 @@ class RackWidthTest(SimpleTestCase):
)
partial_right = SimpleNamespace(
pk=2,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
@@ -98,6 +101,7 @@ class RackWidthTest(SimpleTestCase):
)
partial_left = SimpleNamespace(
pk=3,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
@@ -131,6 +135,62 @@ class RackWidthTest(SimpleTestCase):
],
)
def test_infers_two_half_widths_when_legacy_devices_share_one_unit(self):
devices = [
SimpleNamespace(
pk=21,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
),
SimpleNamespace(
pk=22,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
),
]
self.assertEqual(
effective_width_positions(devices),
{
21: (2, 1, "inferred"),
22: (2, 2, "inferred"),
},
)
def test_infers_missing_position_next_to_stored_half_width(self):
devices = [
SimpleNamespace(
pk=31,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=2),
),
SimpleNamespace(
pk=32,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
),
]
self.assertEqual(
effective_width_positions(devices),
{
31: (2, 2, "stored"),
32: (2, 1, "inferred"),
},
)
def test_adds_rack_width_fields_to_existing_location_fieldset(self):
from utilities.forms.rendering import FieldSet
+93 -5
View File
@@ -97,8 +97,20 @@ class ApplyRackLayoutTest(SimpleTestCase):
placement_model,
):
events = []
first = MagicMock(pk=10, position=Decimal(1), face="front")
second = MagicMock(pk=11, position=Decimal(2), face="front")
first = MagicMock(
pk=10,
rack_id=5,
position=Decimal(1),
face="front",
device_type=SimpleNamespace(u_height=1),
)
second = MagicMock(
pk=11,
rack_id=5,
position=Decimal(2),
face="front",
device_type=SimpleNamespace(u_height=1),
)
first.snapshot.side_effect = lambda: events.append("snapshot-10")
second.snapshot.side_effect = lambda: events.append("snapshot-11")
first.full_clean.side_effect = lambda: events.append("validate-10")
@@ -126,7 +138,13 @@ class ApplyRackLayoutTest(SimpleTestCase):
@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, placement_model):
device = MagicMock(pk=10, position=Decimal(1), face="front")
device = MagicMock(
pk=10,
rack_id=5,
position=Decimal(1),
face="front",
device_type=SimpleNamespace(u_height=1),
)
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()
@@ -148,7 +166,13 @@ class ApplyRackLayoutTest(SimpleTestCase):
_get_permission,
placement_model,
):
device = MagicMock(pk=10, position=Decimal(4), face="front")
device = MagicMock(
pk=10,
rack_id=5,
position=Decimal(4),
face="front",
device_type=SimpleNamespace(u_height=1),
)
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]
@@ -178,7 +202,13 @@ class ApplyRackLayoutTest(SimpleTestCase):
_get_permission,
placement_model,
):
device = MagicMock(pk=10, position=Decimal(4), face="front")
device = MagicMock(
pk=10,
rack_id=5,
position=Decimal(4),
face="front",
device_type=SimpleNamespace(u_height=1),
)
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]
@@ -194,6 +224,51 @@ class ApplyRackLayoutTest(SimpleTestCase):
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_persists_inferred_half_width_when_shared_device_is_moved(
self,
device_model,
_get_permission,
placement_model,
):
left = MagicMock(
pk=10,
rack_id=5,
position=Decimal(4),
face="front",
device_type=SimpleNamespace(u_height=1),
)
right = MagicMock(
pk=11,
rack_id=5,
position=Decimal(4),
face="front",
device_type=SimpleNamespace(u_height=1),
)
device_model.objects.select_for_update.return_value.filter.return_value = [left, right]
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, "x": 0, "y": 4},
{"id": 11, "x": 6, "y": 5},
]
),
grid_columns=12,
)
changed = apply_rack_layout(rack=SimpleNamespace(pk=5), placements=placements, user=user)
self.assertEqual(changed, [right])
placement_model.objects.update_or_create.assert_called_once_with(
device=right,
defaults={"width": 2, "horizontal_position": 2},
)
class ReorderRackFrontendTest(SimpleTestCase):
def test_grid_y_matches_reorder_plugin_for_both_numbering_directions(self):
@@ -213,6 +288,9 @@ class ReorderRackFrontendTest(SimpleTestCase):
self.assertIn("closest('#saveButton')", script)
self.assertIn("X-NetBox-Utilities-Rack-Grid-Columns", script)
self.assertIn("if (face !== 'other')", script)
self.assertIn("data.schema_version !== 3", script)
self.assertIn("data.complete !== true", script)
self.assertIn("nativeMountedItems.length", script)
self.assertNotIn("partialDeviceIds", script)
self.assertNotIn("JSON.stringify", script)
@@ -229,6 +307,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
self.assertEqual(result["status"], "native-fallback")
self.assertEqual(result["devices"], [])
self.assertFalse(result["complete"])
@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")
@@ -250,6 +329,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
placement = SimpleNamespace(width=2, horizontal_position=2)
device = SimpleNamespace(
pk=10,
rack_id=5,
name="Fritzbox",
label="Fritzbox",
virtual_chassis=None,
@@ -266,6 +346,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
)
adjacent_device = SimpleNamespace(
pk=12,
rack_id=5,
name="Router",
label="Router",
virtual_chassis=None,
@@ -282,6 +363,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
)
full_width_device = SimpleNamespace(
pk=11,
rack_id=5,
name="Switch",
label="Switch",
virtual_chassis=None,
@@ -315,6 +397,8 @@ class ReorderRackFrontendTest(SimpleTestCase):
self.assertEqual(result["columns"], 12)
self.assertEqual(result["status"], "ready")
self.assertEqual(result["schema_version"], 3)
self.assertTrue(result["complete"])
self.assertEqual(len(result["devices"]), 3)
self.assertEqual(result["devices"][0]["label"], "Fritzbox")
self.assertFalse(result["images"])
@@ -336,6 +420,10 @@ class ReorderRackFrontendTest(SimpleTestCase):
{key: result["devices"][2][key] for key in ("id", "grid_x", "grid_width", "width")},
{"id": 11, "grid_x": 0, "grid_width": 12, "width": 1},
)
self.assertEqual(
[device["width_source"] for device in result["devices"]],
["stored", "stored", "default"],
)
class ReorderRackPatchTest(SimpleTestCase):
+37 -16
View File
@@ -43,7 +43,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
def test_serializes_fractional_width_geometry_and_device_url(self):
placement = SimpleNamespace(
device_id=17,
device=SimpleNamespace(get_absolute_url=lambda: "/dcim/devices/17/"),
device=SimpleNamespace(pk=17, get_absolute_url=lambda: "/dcim/devices/17/"),
width=3,
horizontal_position=2,
)
@@ -57,12 +57,12 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
"horizontal_position": 2,
"left_percent": 33.33333333,
"width_percent": 33.33333333,
"width_source": "stored",
},
)
@patch("netbox_utilities.topology_views.DeviceRackPlacement.objects")
@patch("netbox_utilities.topology_views.topology_rack_width_enabled", return_value=True)
def test_queries_only_permitted_devices_from_selected_racks(self, _enabled, placement_objects):
def test_queries_all_permitted_devices_and_infers_shared_unit_widths(self, _enabled):
self.request.resolver_match = SimpleNamespace(view_name="plugins:netbox_topology_views:rack_elevation")
rack_queryset = MagicMock()
rack_queryset.filter.return_value = rack_queryset
@@ -71,19 +71,32 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
rack_model.objects.restrict.return_value = rack_queryset
device_queryset = MagicMock()
device_queryset.filter.return_value = "permitted-device-query"
selected_devices = MagicMock()
device_queryset.filter.return_value = selected_devices
device_model = MagicMock()
device_model.objects.restrict.return_value = device_queryset
placement = SimpleNamespace(
device_id=17,
device=SimpleNamespace(get_absolute_url=lambda: "/dcim/devices/17/"),
width=2,
horizontal_position=2,
)
placement_queryset = MagicMock()
placement_queryset.select_related.return_value.order_by.return_value = [placement]
placement_objects.filter.return_value = placement_queryset
devices = [
SimpleNamespace(
pk=17,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
get_absolute_url=lambda: "/dcim/devices/17/",
),
SimpleNamespace(
pk=18,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
get_absolute_url=lambda: "/dcim/devices/18/",
),
]
selected_devices.select_related.return_value.order_by.return_value = devices
with patch.dict("sys.modules", {"dcim.models": SimpleNamespace(Device=device_model, Rack=rack_model)}):
result = get_topology_rack_width_data(self.request)
@@ -91,12 +104,17 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
rack_queryset.filter.assert_called_once_with(pk__in=["3"])
device_queryset.filter.assert_called_once_with(
rack_id__in="selected-rack-query",
position__isnull=False,
position__gt=0,
device_type__u_height__gt=0,
)
placement_objects.filter.assert_called_once_with(device__in="permitted-device-query")
self.assertEqual(result["status"], "ready")
self.assertEqual(result["schema_version"], 3)
self.assertTrue(result["complete"])
self.assertEqual(len(result["devices"]), 2)
self.assertEqual(result["devices"][0]["width"], 2)
self.assertEqual(result["devices"][0]["horizontal_position"], 2)
self.assertEqual(result["devices"][0]["horizontal_position"], 1)
self.assertEqual(result["devices"][1]["horizontal_position"], 2)
self.assertEqual(result["devices"][0]["width_source"], "inferred")
def test_frontend_supports_live_view_and_all_rack_export_formats(self):
script = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "topology-rack-width.js").read_text(
@@ -115,6 +133,8 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
self.assertIn("liveViewCompatible", script)
self.assertIn("payload?.devices", script)
self.assertIn("payload.status !== 'ready'", script)
self.assertIn("payload?.schema_version !== 3", script)
self.assertIn("placement.source", script)
self.assertLess(script.index("liveViewCompatible"), script.index("netbox-utilities-partial-width"))
self.assertLess(script.index("exportGraphic(graphic"), script.index("event.preventDefault()"))
@@ -126,6 +146,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
self.assertEqual(result["status"], "native-fallback")
self.assertEqual(result["devices"], [])
self.assertFalse(result["complete"])
def test_partial_width_css_uses_topology_device_slot_geometry(self):
stylesheet = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "netbox_utilities.css").read_text(
+33 -13
View File
@@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import OperationalError, ProgrammingError
from netbox.plugins import get_plugin_config
from .models import DeviceRackPlacement
from .rack_width import effective_width_positions
logger = logging.getLogger(__name__)
@@ -27,15 +27,25 @@ def topology_rack_width_enabled(request):
def serialize_topology_placement(placement):
width = int(placement.width)
horizontal_position = int(placement.horizontal_position)
return serialize_topology_width(
placement.device,
placement.width,
placement.horizontal_position,
source="stored",
)
def serialize_topology_width(device, width, horizontal_position, *, source):
width = int(width)
horizontal_position = int(horizontal_position)
return {
"device_id": placement.device_id,
"url": placement.device.get_absolute_url(),
"device_id": device.pk,
"url": device.get_absolute_url(),
"width": width,
"horizontal_position": horizontal_position,
"left_percent": round((horizontal_position - 1) / width * 100, 8),
"width_percent": round(100 / width, 8),
"width_source": source,
}
@@ -44,7 +54,7 @@ def get_topology_rack_width_data(request):
if not topology_rack_width_enabled(request):
return None
data = {"devices": [], "status": "ready"}
data = {"devices": [], "status": "ready", "schema_version": 3, "complete": False}
if not request.GET:
return data
@@ -63,14 +73,24 @@ def get_topology_rack_width_data(request):
if selected_locations:
racks = racks.filter(location_id__in=selected_locations)
devices = Device.objects.restrict(request.user, "view").filter(
rack_id__in=racks.values("pk"),
position__isnull=False,
devices = list(
Device.objects.restrict(request.user, "view")
.filter(
rack_id__in=racks.values("pk"),
position__gt=0,
device_type__u_height__gt=0,
)
.select_related("device_type", "netbox_utilities_rack_placement")
.order_by("rack_id", "face", "position", "pk")
)
placements = (
DeviceRackPlacement.objects.filter(device__in=devices).select_related("device").order_by("device_id")
)
data["devices"] = [serialize_topology_placement(placement) for placement in placements]
widths = effective_width_positions(devices)
data["devices"] = [
serialize_topology_width(device, width, position, source=source)
for device in devices
for width, position, source in (widths[device.pk],)
if width > 1
]
data["complete"] = True
return data
except (
AttributeError,