fix: render all devices sharing rack units
This commit is contained in:
@@ -105,9 +105,11 @@ sudo systemctl restart netbox netbox-rq
|
|||||||
Nach Änderungen an JavaScript oder CSS kann ein Hard-Reload des Browsers mit
|
Nach Änderungen an JavaScript oder CSS kann ein Hard-Reload des Browsers mit
|
||||||
`Strg+F5` erforderlich sein.
|
`Strg+F5` erforderlich sein.
|
||||||
|
|
||||||
Ab Version `0.9.1` werden die Teilbreiten-Adapter auf der Rack- und
|
Ab Version `0.9.2` werden Rack-SVG und Reorder direkt aus allen Geräten des
|
||||||
Reorder-Seite auch dann eingebunden, wenn keine Teilbreitengeräte gefunden
|
Racks aufgebaut. Dadurch können mehrere Geräte derselben HE nicht mehr durch
|
||||||
wurden. Nach diesem Update ist `collectstatic` deshalb zwingend erforderlich;
|
NetBox' native Ein-Gerät-pro-HE-Darstellung verloren gehen. Die Adapter werden
|
||||||
|
auch dann eingebunden, wenn keine Teilbreitengeräte gefunden wurden. Nach
|
||||||
|
diesem Update ist `collectstatic` deshalb zwingend erforderlich;
|
||||||
die Dateien `topology-rack-width.js` und `reorder-rack-width.js` müssen unter
|
die Dateien `topology-rack-width.js` und `reorder-rack-width.js` müssen unter
|
||||||
`/opt/netbox/netbox/static/netbox_utilities/` vorhanden sein.
|
`/opt/netbox/netbox/static/netbox_utilities/` vorhanden sein.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from netbox.plugins import PluginConfig, get_plugin_config
|
from netbox.plugins import PluginConfig, get_plugin_config
|
||||||
|
|
||||||
__version__ = "0.9.1"
|
__version__ = "0.9.2"
|
||||||
|
|
||||||
|
|
||||||
class NetBoxUtilitiesConfig(PluginConfig):
|
class NetBoxUtilitiesConfig(PluginConfig):
|
||||||
|
|||||||
@@ -251,7 +251,20 @@ def _decimal_range(start, stop):
|
|||||||
def _install_rack_methods():
|
def _install_rack_methods():
|
||||||
from dcim.models import Rack
|
from dcim.models import Rack
|
||||||
|
|
||||||
if getattr(Rack, "_netbox_utilities_rack_width_installed", False):
|
# Version 0.9.0 replaced get_rack_units() globally. A long-running
|
||||||
|
# process which reloads the plugin must restore NetBox's native method
|
||||||
|
# before installing the current, non-destructive implementation.
|
||||||
|
already_installed = getattr(Rack, "_netbox_utilities_rack_width_installed", False)
|
||||||
|
installed_version = getattr(Rack, "_netbox_utilities_rack_width_version", None)
|
||||||
|
if already_installed and installed_version is None:
|
||||||
|
for method_name in ("get_rack_units", "get_available_units"):
|
||||||
|
method = getattr(Rack, method_name)
|
||||||
|
if wrapped := getattr(method, "__wrapped__", None):
|
||||||
|
setattr(Rack, method_name, wrapped)
|
||||||
|
already_installed = False
|
||||||
|
|
||||||
|
if already_installed:
|
||||||
|
Rack._netbox_utilities_rack_width_version = 2
|
||||||
return
|
return
|
||||||
original_available_units = Rack.get_available_units
|
original_available_units = Rack.get_available_units
|
||||||
|
|
||||||
@@ -275,6 +288,7 @@ def _install_rack_methods():
|
|||||||
|
|
||||||
Rack.get_available_units = width_aware_available_units
|
Rack.get_available_units = width_aware_available_units
|
||||||
Rack._netbox_utilities_rack_width_installed = True
|
Rack._netbox_utilities_rack_width_installed = True
|
||||||
|
Rack._netbox_utilities_rack_width_version = 2
|
||||||
|
|
||||||
|
|
||||||
def _install_device_validation():
|
def _install_device_validation():
|
||||||
@@ -418,42 +432,32 @@ def _install_device_form():
|
|||||||
def _install_rack_svg():
|
def _install_rack_svg():
|
||||||
from dcim.svg.racks import RackElevationSVG
|
from dcim.svg.racks import RackElevationSVG
|
||||||
|
|
||||||
if getattr(RackElevationSVG, "_netbox_utilities_rack_width_installed", False):
|
if getattr(RackElevationSVG, "_netbox_utilities_rack_width_version", None) == 3:
|
||||||
return
|
return
|
||||||
original_draw_face = RackElevationSVG.draw_face
|
current_draw_face = RackElevationSVG.draw_face
|
||||||
|
current_globals = getattr(current_draw_face, "__globals__", {})
|
||||||
|
if current_globals.get("__name__") == __name__ and hasattr(current_draw_face, "__wrapped__"):
|
||||||
|
original_draw_face = current_draw_face.__wrapped__
|
||||||
|
else:
|
||||||
|
original_draw_face = current_draw_face
|
||||||
|
|
||||||
@wraps(original_draw_face)
|
@wraps(original_draw_face)
|
||||||
def width_aware_draw_face(elevation, face, opposite=False):
|
def width_aware_draw_face(elevation, face, opposite=False):
|
||||||
partial_devices = list(_partial_width_elevation_devices(elevation.rack, face))
|
from svgwrite.shapes import Rect
|
||||||
partial_device_ids = {device.pk for device in partial_devices}
|
|
||||||
|
|
||||||
# Draw regular devices with NetBox's native geometry. Partial devices
|
# NetBox's get_rack_units() stores only one device per rack unit and
|
||||||
# are skipped here and rendered once with their stored horizontal slot
|
# therefore cannot represent two devices mounted beside each other.
|
||||||
# below. Rack.get_rack_units() deliberately remains untouched so that
|
# Query every mounted device directly and render each exactly once.
|
||||||
# other consumers such as netbox-reorder-rack keep receiving devices.
|
for device in _rack_elevation_devices(elevation.rack, face):
|
||||||
for unit in elevation.rack.get_rack_units(face=face, expand_devices=False):
|
try:
|
||||||
device = unit["device"]
|
placement = device.netbox_utilities_rack_placement
|
||||||
if device and device.pk in partial_device_ids:
|
except ObjectDoesNotExist:
|
||||||
continue
|
placement = None
|
||||||
height = unit.get("height", decimal.Decimal("1.0"))
|
|
||||||
coords = elevation._get_device_coords(unit["id"], height)
|
|
||||||
size = (elevation.unit_width, int(elevation.unit_height * height))
|
|
||||||
if device and device.pk in elevation.permitted_device_ids:
|
|
||||||
if device.face == face and not opposite:
|
|
||||||
elevation.draw_device_front(device, coords, size)
|
|
||||||
else:
|
|
||||||
elevation.draw_device_rear(device, coords, size)
|
|
||||||
elif device:
|
|
||||||
from svgwrite.shapes import Rect
|
|
||||||
|
|
||||||
elevation.drawing.add(Rect(coords, size, class_="blocked"))
|
|
||||||
|
|
||||||
for device in partial_devices:
|
|
||||||
placement = device.netbox_utilities_rack_placement
|
|
||||||
height = decimal.Decimal(str(device.device_type.u_height))
|
height = decimal.Decimal(str(device.device_type.u_height))
|
||||||
coords = elevation._get_device_coords(device.position, height)
|
coords = elevation._get_device_coords(device.position, height)
|
||||||
width = elevation.unit_width / placement.width
|
width = elevation.unit_width if placement is None else elevation.unit_width / placement.width
|
||||||
coords = (coords[0] + width * (placement.horizontal_position - 1), coords[1])
|
if placement is not None:
|
||||||
|
coords = (coords[0] + width * (placement.horizontal_position - 1), coords[1])
|
||||||
size = (width, int(elevation.unit_height * height))
|
size = (width, int(elevation.unit_height * height))
|
||||||
if device.pk in elevation.permitted_device_ids:
|
if device.pk in elevation.permitted_device_ids:
|
||||||
if device.face == face and not opposite:
|
if device.face == face and not opposite:
|
||||||
@@ -461,16 +465,15 @@ def _install_rack_svg():
|
|||||||
else:
|
else:
|
||||||
elevation.draw_device_rear(device, coords, size)
|
elevation.draw_device_rear(device, coords, size)
|
||||||
else:
|
else:
|
||||||
from svgwrite.shapes import Rect
|
|
||||||
|
|
||||||
elevation.drawing.add(Rect(coords, size, class_="blocked"))
|
elevation.drawing.add(Rect(coords, size, class_="blocked"))
|
||||||
|
|
||||||
RackElevationSVG.draw_face = width_aware_draw_face
|
RackElevationSVG.draw_face = width_aware_draw_face
|
||||||
RackElevationSVG._netbox_utilities_rack_width_installed = True
|
RackElevationSVG._netbox_utilities_rack_width_installed = True
|
||||||
|
RackElevationSVG._netbox_utilities_rack_width_version = 3
|
||||||
|
|
||||||
|
|
||||||
def _partial_width_elevation_devices(rack, face):
|
def _rack_elevation_devices(rack, face):
|
||||||
"""Return the annotated Device instances expected by NetBox's SVG renderer."""
|
"""Return every annotated device which must be drawn on a rack face."""
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -478,16 +481,17 @@ def _partial_width_elevation_devices(rack, face):
|
|||||||
rack=rack,
|
rack=rack,
|
||||||
position__gt=0,
|
position__gt=0,
|
||||||
device_type__u_height__gt=0,
|
device_type__u_height__gt=0,
|
||||||
netbox_utilities_rack_placement__isnull=False,
|
|
||||||
)
|
)
|
||||||
.filter(Q(face=face) | Q(device_type__is_full_depth=True))
|
.filter(Q(face=face) | Q(device_type__is_full_depth=True))
|
||||||
.select_related(
|
.select_related(
|
||||||
"device_type",
|
"device_type",
|
||||||
"device_type__manufacturer",
|
"device_type__manufacturer",
|
||||||
"role",
|
"role",
|
||||||
|
"virtual_chassis",
|
||||||
"netbox_utilities_rack_placement",
|
"netbox_utilities_rack_placement",
|
||||||
)
|
)
|
||||||
.annotate(devicebay_count=Count("devicebays"))
|
.annotate(devicebay_count=Count("devicebays"))
|
||||||
|
.order_by("position", "pk")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,16 @@ def _device_label(device):
|
|||||||
return device.label or str(device.device_type)
|
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"):
|
def _empty_reorder_rack_width_data(request, *, status="ready"):
|
||||||
selected_view = request.GET.get("view", "images-and-labels")
|
selected_view = request.GET.get("view", "images-and-labels")
|
||||||
return {
|
return {
|
||||||
@@ -251,7 +261,7 @@ def _empty_reorder_rack_width_data(request, *, status="ready"):
|
|||||||
|
|
||||||
|
|
||||||
def get_reorder_rack_width_data(request):
|
def get_reorder_rack_width_data(request):
|
||||||
"""Describe partial-width widgets for netbox-reorder-rack's GridStack UI."""
|
"""Describe every mounted widget for netbox-reorder-rack's GridStack UI."""
|
||||||
if not reorder_rack_width_enabled(request):
|
if not reorder_rack_width_enabled(request):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -272,16 +282,22 @@ def get_reorder_rack_width_data(request):
|
|||||||
Device.objects.restrict(request.user, "view")
|
Device.objects.restrict(request.user, "view")
|
||||||
.filter(
|
.filter(
|
||||||
rack=rack,
|
rack=rack,
|
||||||
position__isnull=False,
|
position__gt=0,
|
||||||
netbox_utilities_rack_placement__isnull=False,
|
device_type__u_height__gt=0,
|
||||||
)
|
)
|
||||||
.select_related("device_type", "role", "netbox_utilities_rack_placement")
|
.select_related(
|
||||||
.order_by("pk")
|
"device_type",
|
||||||
|
"device_type__manufacturer",
|
||||||
|
"role",
|
||||||
|
"virtual_chassis",
|
||||||
|
"netbox_utilities_rack_placement",
|
||||||
|
)
|
||||||
|
.order_by("position", "pk")
|
||||||
)
|
)
|
||||||
permission = get_permission_for_model(Device, "change")
|
permission = get_permission_for_model(Device, "change")
|
||||||
for device in devices:
|
for device in devices:
|
||||||
placement = device.netbox_utilities_rack_placement
|
width, horizontal_position = _device_width_position(device)
|
||||||
grid_width, grid_x = grid_dimensions(placement.width, placement.horizontal_position)
|
grid_width, grid_x = grid_dimensions(width, horizontal_position)
|
||||||
role_color = device.role.color or "1685fc"
|
role_color = device.role.color or "1685fc"
|
||||||
grid_height = int(Decimal(str(device.device_type.u_height)) * 2)
|
grid_height = int(Decimal(str(device.device_type.u_height)) * 2)
|
||||||
data["devices"].append(
|
data["devices"].append(
|
||||||
@@ -294,8 +310,8 @@ def get_reorder_rack_width_data(request):
|
|||||||
"grid_y": reorder_grid_y(rack, device.position, device.device_type.u_height),
|
"grid_y": reorder_grid_y(rack, device.position, device.device_type.u_height),
|
||||||
"grid_width": grid_width,
|
"grid_width": grid_width,
|
||||||
"grid_height": grid_height,
|
"grid_height": grid_height,
|
||||||
"width": placement.width,
|
"width": width,
|
||||||
"horizontal_position": placement.horizontal_position,
|
"horizontal_position": horizontal_position,
|
||||||
"color": role_color,
|
"color": role_color,
|
||||||
"text_color": foreground_color(role_color),
|
"text_color": foreground_color(role_color),
|
||||||
"front_image": _image_url(device.device_type.front_image),
|
"front_image": _image_url(device.device_type.front_image),
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
if (!gridElements.front || !gridElements.rear || !gridElements.other) return;
|
if (!gridElements.front || !gridElements.rear || !gridElements.other) return;
|
||||||
|
|
||||||
const setAttribute = (element, name, value) => element.setAttribute(name, String(value));
|
const setAttribute = (element, name, value) => element.setAttribute(name, String(value));
|
||||||
const partialDeviceIds = new Set(data.devices.map(device => String(device.id)));
|
|
||||||
|
|
||||||
const originalFetch = window.fetch.bind(window);
|
const originalFetch = window.fetch.bind(window);
|
||||||
window.fetch = (resource, options = {}) => {
|
window.fetch = (resource, options = {}) => {
|
||||||
const resourceUrl = typeof resource === 'string' || resource instanceof URL
|
const resourceUrl = typeof resource === 'string' || resource instanceof URL
|
||||||
@@ -48,10 +46,13 @@
|
|||||||
return originalFetch(resource, {...options, headers});
|
return originalFetch(resource, {...options, headers});
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(gridElements).forEach(grid => {
|
Object.entries(gridElements).forEach(([face, grid]) => {
|
||||||
setAttribute(grid, 'gs-column', columns);
|
setAttribute(grid, 'gs-column', columns);
|
||||||
grid.querySelectorAll('.grid-stack-item').forEach(item => {
|
grid.querySelectorAll('.grid-stack-item').forEach(item => {
|
||||||
if (partialDeviceIds.has(item.getAttribute('gs-id'))) {
|
// The native Reorder view is based on Rack.get_rack_units(), which can
|
||||||
|
// expose only one device per rack unit. Rebuild the mounted grids from
|
||||||
|
// the complete server payload; keep native non-racked devices intact.
|
||||||
|
if (face !== 'other') {
|
||||||
item.remove();
|
item.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from functools import wraps
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -13,7 +14,8 @@ from django.urls import resolve, reverse
|
|||||||
|
|
||||||
from netbox_utilities.rack_width import (
|
from netbox_utilities.rack_width import (
|
||||||
_cleanup_unracked_placement,
|
_cleanup_unracked_placement,
|
||||||
_partial_width_elevation_devices,
|
_install_rack_methods,
|
||||||
|
_rack_elevation_devices,
|
||||||
available_units_for_device,
|
available_units_for_device,
|
||||||
horizontal_interval,
|
horizontal_interval,
|
||||||
include_rack_width_in_fieldsets,
|
include_rack_width_in_fieldsets,
|
||||||
@@ -32,16 +34,17 @@ class RackWidthTest(SimpleTestCase):
|
|||||||
self.assertIn("positionField.disabled = width === 1", script)
|
self.assertIn("positionField.disabled = width === 1", script)
|
||||||
self.assertIn("positionField.tomselect.sync()", script)
|
self.assertIn("positionField.tomselect.sync()", script)
|
||||||
|
|
||||||
def test_partial_width_svg_devices_include_core_device_bay_annotation(self):
|
def test_svg_devices_include_core_device_bay_annotation_and_optional_placement(self):
|
||||||
from dcim.choices import DeviceFaceChoices
|
from dcim.choices import DeviceFaceChoices
|
||||||
from dcim.models import Rack
|
from dcim.models import Rack
|
||||||
|
|
||||||
devices = _partial_width_elevation_devices(Rack(pk=3), DeviceFaceChoices.FACE_FRONT)
|
devices = _rack_elevation_devices(Rack(pk=3), DeviceFaceChoices.FACE_FRONT)
|
||||||
|
|
||||||
self.assertIn("devicebay_count", devices.query.annotations)
|
self.assertIn("devicebay_count", devices.query.annotations)
|
||||||
self.assertIn("netbox_utilities_rack_placement", devices.query.select_related)
|
self.assertIn("netbox_utilities_rack_placement", devices.query.select_related)
|
||||||
self.assertIn("device_type", devices.query.select_related)
|
self.assertIn("device_type", devices.query.select_related)
|
||||||
self.assertIn("manufacturer", devices.query.select_related["device_type"])
|
self.assertIn("manufacturer", devices.query.select_related["device_type"])
|
||||||
|
self.assertNotIn("netbox_utilities_rack_placement__isnull", str(devices.query))
|
||||||
|
|
||||||
def test_core_rack_units_remain_available_to_other_plugins(self):
|
def test_core_rack_units_remain_available_to_other_plugins(self):
|
||||||
from dcim.models import Rack
|
from dcim.models import Rack
|
||||||
@@ -49,10 +52,43 @@ class RackWidthTest(SimpleTestCase):
|
|||||||
self.assertEqual(Rack.get_rack_units.__module__, "dcim.models.racks")
|
self.assertEqual(Rack.get_rack_units.__module__, "dcim.models.racks")
|
||||||
self.assertFalse(hasattr(Rack.get_rack_units, "__wrapped__"))
|
self.assertFalse(hasattr(Rack.get_rack_units, "__wrapped__"))
|
||||||
|
|
||||||
|
def test_replaces_legacy_rack_unit_wrapper_during_upgrade(self):
|
||||||
|
def native_rack_units(rack):
|
||||||
|
return rack
|
||||||
|
|
||||||
|
def native_available_units(rack, *args, **kwargs):
|
||||||
|
return rack, args, kwargs
|
||||||
|
|
||||||
|
@wraps(native_rack_units)
|
||||||
|
def legacy_rack_units(rack):
|
||||||
|
return native_rack_units(rack)
|
||||||
|
|
||||||
|
@wraps(native_available_units)
|
||||||
|
def legacy_available_units(rack, *args, **kwargs):
|
||||||
|
return native_available_units(rack, *args, **kwargs)
|
||||||
|
|
||||||
|
class LegacyRack:
|
||||||
|
get_rack_units = legacy_rack_units
|
||||||
|
get_available_units = legacy_available_units
|
||||||
|
_netbox_utilities_rack_width_installed = True
|
||||||
|
|
||||||
|
with patch.dict("sys.modules", {"dcim.models": SimpleNamespace(Rack=LegacyRack)}):
|
||||||
|
_install_rack_methods()
|
||||||
|
|
||||||
|
self.assertIs(LegacyRack.get_rack_units, native_rack_units)
|
||||||
|
self.assertIs(LegacyRack.get_available_units.__wrapped__, native_available_units)
|
||||||
|
self.assertEqual(LegacyRack._netbox_utilities_rack_width_version, 2)
|
||||||
|
|
||||||
def test_svg_draws_all_partial_devices_sharing_one_unit_exactly_once(self):
|
def test_svg_draws_all_partial_devices_sharing_one_unit_exactly_once(self):
|
||||||
from dcim.svg.racks import RackElevationSVG
|
from dcim.svg.racks import RackElevationSVG
|
||||||
|
|
||||||
regular = SimpleNamespace(pk=1, face="front")
|
regular = SimpleNamespace(
|
||||||
|
pk=1,
|
||||||
|
face="front",
|
||||||
|
position=12,
|
||||||
|
device_type=SimpleNamespace(u_height=1),
|
||||||
|
netbox_utilities_rack_placement=None,
|
||||||
|
)
|
||||||
partial_right = SimpleNamespace(
|
partial_right = SimpleNamespace(
|
||||||
pk=2,
|
pk=2,
|
||||||
face="front",
|
face="front",
|
||||||
@@ -67,16 +103,7 @@ class RackWidthTest(SimpleTestCase):
|
|||||||
device_type=SimpleNamespace(u_height=1),
|
device_type=SimpleNamespace(u_height=1),
|
||||||
netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=1),
|
netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=1),
|
||||||
)
|
)
|
||||||
rack = SimpleNamespace(
|
rack = SimpleNamespace(get_rack_units=MagicMock())
|
||||||
get_rack_units=MagicMock(
|
|
||||||
return_value=[
|
|
||||||
{"id": 12, "device": regular, "height": 1},
|
|
||||||
# NetBox's native unit list can expose only one of multiple
|
|
||||||
# partial devices occupying the same rack unit.
|
|
||||||
{"id": 11, "device": partial_right, "height": 1},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elevation = SimpleNamespace(
|
elevation = SimpleNamespace(
|
||||||
rack=rack,
|
rack=rack,
|
||||||
unit_width=200,
|
unit_width=200,
|
||||||
@@ -89,12 +116,12 @@ class RackWidthTest(SimpleTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"netbox_utilities.rack_width._partial_width_elevation_devices",
|
"netbox_utilities.rack_width._rack_elevation_devices",
|
||||||
return_value=[partial_left, partial_right],
|
return_value=[regular, partial_left, partial_right],
|
||||||
):
|
):
|
||||||
RackElevationSVG.draw_face(elevation, "front")
|
RackElevationSVG.draw_face(elevation, "front")
|
||||||
|
|
||||||
rack.get_rack_units.assert_called_once_with(face="front", expand_devices=False)
|
rack.get_rack_units.assert_not_called()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
elevation.draw_device_front.call_args_list,
|
elevation.draw_device_front.call_args_list,
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ class ReorderRackFrontendTest(SimpleTestCase):
|
|||||||
self.assertIn("grid.on('dropped'", script)
|
self.assertIn("grid.on('dropped'", script)
|
||||||
self.assertIn("closest('#saveButton')", script)
|
self.assertIn("closest('#saveButton')", script)
|
||||||
self.assertIn("X-NetBox-Utilities-Rack-Grid-Columns", script)
|
self.assertIn("X-NetBox-Utilities-Rack-Grid-Columns", script)
|
||||||
|
self.assertIn("if (face !== 'other')", script)
|
||||||
|
self.assertNotIn("partialDeviceIds", script)
|
||||||
self.assertNotIn("JSON.stringify", script)
|
self.assertNotIn("JSON.stringify", script)
|
||||||
|
|
||||||
@patch("netbox_utilities.reorder_rack.reorder_rack_width_enabled", return_value=True)
|
@patch("netbox_utilities.reorder_rack.reorder_rack_width_enabled", return_value=True)
|
||||||
@@ -232,7 +234,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
|
|||||||
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
|
@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.Device")
|
||||||
@patch("netbox_utilities.reorder_rack.Rack")
|
@patch("netbox_utilities.reorder_rack.Rack")
|
||||||
def test_describes_partial_devices_for_reorder_grid(
|
def test_describes_every_mounted_device_for_reorder_grid(
|
||||||
self,
|
self,
|
||||||
rack_model,
|
rack_model,
|
||||||
device_model,
|
device_model,
|
||||||
@@ -262,8 +264,44 @@ class ReorderRackFrontendTest(SimpleTestCase):
|
|||||||
position=Decimal(4),
|
position=Decimal(4),
|
||||||
netbox_utilities_rack_placement=placement,
|
netbox_utilities_rack_placement=placement,
|
||||||
)
|
)
|
||||||
|
adjacent_device = SimpleNamespace(
|
||||||
|
pk=12,
|
||||||
|
name="Router",
|
||||||
|
label="Router",
|
||||||
|
virtual_chassis=None,
|
||||||
|
device_type=SimpleNamespace(
|
||||||
|
u_height=1,
|
||||||
|
is_full_depth=False,
|
||||||
|
front_image=None,
|
||||||
|
rear_image=None,
|
||||||
|
),
|
||||||
|
role=SimpleNamespace(color="fedcba"),
|
||||||
|
face="front",
|
||||||
|
position=Decimal(4),
|
||||||
|
netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=1),
|
||||||
|
)
|
||||||
|
full_width_device = SimpleNamespace(
|
||||||
|
pk=11,
|
||||||
|
name="Switch",
|
||||||
|
label="Switch",
|
||||||
|
virtual_chassis=None,
|
||||||
|
device_type=SimpleNamespace(
|
||||||
|
u_height=1,
|
||||||
|
is_full_depth=False,
|
||||||
|
front_image=None,
|
||||||
|
rear_image=None,
|
||||||
|
),
|
||||||
|
role=SimpleNamespace(color="123456"),
|
||||||
|
face="front",
|
||||||
|
position=Decimal(5),
|
||||||
|
netbox_utilities_rack_placement=None,
|
||||||
|
)
|
||||||
device_queryset = MagicMock()
|
device_queryset = MagicMock()
|
||||||
device_queryset.filter.return_value.select_related.return_value.order_by.return_value = [device]
|
device_queryset.filter.return_value.select_related.return_value.order_by.return_value = [
|
||||||
|
device,
|
||||||
|
adjacent_device,
|
||||||
|
full_width_device,
|
||||||
|
]
|
||||||
device_model.objects.restrict.return_value = device_queryset
|
device_model.objects.restrict.return_value = device_queryset
|
||||||
|
|
||||||
with (
|
with (
|
||||||
@@ -277,13 +315,27 @@ class ReorderRackFrontendTest(SimpleTestCase):
|
|||||||
|
|
||||||
self.assertEqual(result["columns"], 12)
|
self.assertEqual(result["columns"], 12)
|
||||||
self.assertEqual(result["status"], "ready")
|
self.assertEqual(result["status"], "ready")
|
||||||
|
self.assertEqual(len(result["devices"]), 3)
|
||||||
self.assertEqual(result["devices"][0]["label"], "Fritzbox")
|
self.assertEqual(result["devices"][0]["label"], "Fritzbox")
|
||||||
self.assertFalse(result["images"])
|
self.assertFalse(result["images"])
|
||||||
self.assertTrue(result["labels"])
|
self.assertTrue(result["labels"])
|
||||||
|
device_queryset.filter.assert_called_once_with(
|
||||||
|
rack=rack,
|
||||||
|
position__gt=0,
|
||||||
|
device_type__u_height__gt=0,
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{key: result["devices"][0][key] for key in ("id", "grid_x", "grid_width", "width", "horizontal_position")},
|
{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},
|
{"id": 10, "grid_x": 6, "grid_width": 6, "width": 2, "horizontal_position": 2},
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{key: result["devices"][1][key] for key in ("id", "grid_x", "grid_width", "width")},
|
||||||
|
{"id": 12, "grid_x": 0, "grid_width": 6, "width": 2},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{key: result["devices"][2][key] for key in ("id", "grid_x", "grid_width", "width")},
|
||||||
|
{"id": 11, "grid_x": 0, "grid_width": 12, "width": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReorderRackPatchTest(SimpleTestCase):
|
class ReorderRackPatchTest(SimpleTestCase):
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-utilities"
|
name = "netbox-utilities"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6"
|
description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering 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