From 839b076d94dc331c6680c4d6bd9ce81bc3b80790 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 13 Aug 2026 16:10:47 +0200 Subject: [PATCH] fix: preserve reorder drag width and images --- README.md | 6 +++ netbox_utilities/__init__.py | 2 +- .../netbox_utilities/netbox_utilities.css | 51 ++++++++++--------- .../netbox_utilities/reorder_rack.html | 4 +- .../netbox_utilities/reorder_rack_grid.html | 15 ++++-- netbox_utilities/tests/test_reorder_rack.py | 43 ++++++++++++++-- netbox_utilities/tests/test_topology_views.py | 2 +- pyproject.toml | 2 +- 8 files changed, 88 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 468cac2..607f2b9 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,12 @@ GridStack-11-Stylesheet von NetBox 4.6.7, durch die Gerätekacheln mit einer berechneten Breite von `0px` unsichtbar waren. Topology-Breiten werden außerdem nach dem vollständigen Rendern zentral auf die HTTP-Antwort angewendet und zusätzlich direkt in die vorhandenen Geräte-Styles geschrieben. +Ab Version `0.9.8` behalten Geräte beim Ziehen in Reorder ihre tatsächliche +Rackbreite. Die 12-Spalten-Regeln lassen dazu GridStacks temporären +Pixelkoordinaten während des Ziehens Vorrang, statt eine Prozentbreite auf das +Browserfenster anzuwenden. Die serverseitig aufgebauten Reorder-Kacheln zeigen +außerdem wieder die Front- und Rückseitenbilder des Gerätetyps entsprechend +der gewählten Ansicht an. Bereits vorhandene Geräte, die dieselbe HE und Rackseite belegen, aber noch keine Plugin-Platzierungszeile besitzen, werden in Rack-SVG, Reorder und der Topology-Rack-Ansicht ohne Datenbankänderung gleichmäßig nebeneinander diff --git a/netbox_utilities/__init__.py b/netbox_utilities/__init__.py index dd45012..efe5b76 100644 --- a/netbox_utilities/__init__.py +++ b/netbox_utilities/__init__.py @@ -1,6 +1,6 @@ from netbox.plugins import PluginConfig, get_plugin_config -__version__ = "0.9.7" +__version__ = "0.9.8" class NetBoxUtilitiesConfig(PluginConfig): diff --git a/netbox_utilities/static/netbox_utilities/netbox_utilities.css b/netbox_utilities/static/netbox_utilities/netbox_utilities.css index 3a64774..161ab10 100644 --- a/netbox_utilities/static/netbox_utilities/netbox_utilities.css +++ b/netbox_utilities/static/netbox_utilities/netbox_utilities.css @@ -41,35 +41,38 @@ * provides GridStack 11's base stylesheet. GridStack 11 calculates item * widths through a CSS variable which the older runtime never sets. Without * these explicit rules every 12-column widget therefore has width: 0px. + * Do not mark these declarations important: while dragging, GridStack writes + * pixel coordinates inline on a fixed-position helper. Those inline values + * must take precedence or the helper is sized relative to the viewport. */ .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item { - width: calc(100% / 12) !important; + width: calc(100% / 12); } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="2"] { width: calc(200% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="3"] { width: 25% !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="4"] { width: calc(400% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="5"] { width: calc(500% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="6"] { width: 50% !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="7"] { width: calc(700% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="8"] { width: calc(800% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="9"] { width: 75% !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="10"] { width: calc(1000% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="11"] { width: calc(1100% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="12"] { width: 100% !important; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="2"] { width: calc(200% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="3"] { width: 25%; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="4"] { width: calc(400% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="5"] { width: calc(500% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="6"] { width: 50%; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="7"] { width: calc(700% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="8"] { width: calc(800% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="9"] { width: 75%; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="10"] { width: calc(1000% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="11"] { width: calc(1100% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="12"] { width: 100%; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="0"] { left: 0 !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="1"] { left: calc(100% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="2"] { left: calc(200% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="3"] { left: 25% !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="4"] { left: calc(400% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="5"] { left: calc(500% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="6"] { left: 50% !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="7"] { left: calc(700% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="8"] { left: calc(800% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="9"] { left: 75% !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="10"] { left: calc(1000% / 12) !important; } -.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="11"] { left: calc(1100% / 12) !important; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="0"] { left: 0; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="1"] { left: calc(100% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="2"] { left: calc(200% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="3"] { left: 25%; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="4"] { left: calc(400% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="5"] { left: calc(500% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="6"] { left: 50%; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="7"] { left: calc(700% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="8"] { left: calc(800% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="9"] { left: 75%; } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="10"] { left: calc(1000% / 12); } +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="11"] { left: calc(1100% / 12); } .netbox-utilities-sidebar-resizer { display: none; diff --git a/netbox_utilities/templates/netbox_utilities/reorder_rack.html b/netbox_utilities/templates/netbox_utilities/reorder_rack.html index 81639e2..5eaf36f 100644 --- a/netbox_utilities/templates/netbox_utilities/reorder_rack.html +++ b/netbox_utilities/templates/netbox_utilities/reorder_rack.html @@ -45,8 +45,8 @@ gs-locked="{% if not request.user|can_change:device %}true{% else %}false{% endif %}" gs-no-move="{% if not request.user|can_change:device %}true{% else %}false{% endif %}">
- {{ device.name|default:device.device_type }} + style="{% if images and device.device_type.front_image %}background-image: url('{{ device.device_type.front_image.url }}'); background-size: {{ unit_width }}px;{% else %}background-color: #{{ device.role.color|default:'1685fc' }};{% endif %} color: #{% if device.role.color %}{{ device.role.color|text_color }}{% else %}000000{% endif %}"> + {% if images and device.device_type.front_image %}{% if labels %}{{ device.name|default:device.device_type }}{% endif %}{% else %}{{ device.name|default:device.device_type }}{% endif %}
{% endfor %} diff --git a/netbox_utilities/templates/netbox_utilities/reorder_rack_grid.html b/netbox_utilities/templates/netbox_utilities/reorder_rack_grid.html index f98cada..6c14a7f 100644 --- a/netbox_utilities/templates/netbox_utilities/reorder_rack_grid.html +++ b/netbox_utilities/templates/netbox_utilities/reorder_rack_grid.html @@ -20,10 +20,17 @@ data-item-face="{% if device.face == face %}{{ face }}{% else %}back{% endif %}" data-rack-width="{{ device.width }}" data-horizontal-position="{{ device.horizontal_position }}"> -
- {{ device.label }} -
+ {% if device.face == face %} +
+ {% if images and device.front_image %}{% if labels %}{{ device.label }}{% endif %}{% else %}{{ device.label }}{% endif %} +
+ {% else %} +
+ {% if images and device.rear_image %}{% if labels %}{{ device.label }}{% endif %}{% else %}{{ device.label }}{% endif %} +
+ {% endif %} {% endif %} {% endfor %} diff --git a/netbox_utilities/tests/test_reorder_rack.py b/netbox_utilities/tests/test_reorder_rack.py index de42483..0884110 100644 --- a/netbox_utilities/tests/test_reorder_rack.py +++ b/netbox_utilities/tests/test_reorder_rack.py @@ -348,9 +348,44 @@ class ReorderRackFrontendTest(SimpleTestCase): encoding="utf-8" ) self.assertIn('.netbox-utilities-reorder-grid[gs-column="12"]', stylesheet) - self.assertIn('.grid-stack-item[gs-w="6"] { width: 50% !important; }', stylesheet) - self.assertIn('.grid-stack-item[gs-w="12"] { width: 100% !important; }', stylesheet) - self.assertIn('.grid-stack-item[gs-x="6"] { left: 50% !important; }', stylesheet) + self.assertIn('.grid-stack-item[gs-w="6"] { width: 50%; }', stylesheet) + self.assertIn('.grid-stack-item[gs-w="12"] { width: 100%; }', stylesheet) + self.assertIn('.grid-stack-item[gs-x="6"] { left: 50%; }', stylesheet) + self.assertNotIn('.grid-stack-item[gs-w="6"] { width: 50% !important; }', stylesheet) + + rendered_image_grid = render_to_string( + "netbox_utilities/reorder_rack_grid.html", + { + "object": SimpleNamespace(units=[1], u_height=42), + "face": "front", + "grid_id": 0, + "images": True, + "labels": False, + "unit_width": 220, + "reorder_devices": [ + { + "id": 10, + "label": "LEO-Fritzbox", + "face": "front", + "full_depth": False, + "grid_x": 0, + "grid_y": 62, + "grid_width": 6, + "grid_height": 2, + "width": 2, + "horizontal_position": 1, + "color": "f0a000", + "text_color": "000000", + "locked": False, + "front_image": "/media/device-type-images/fritzbox.png", + "rear_image": None, + } + ], + }, + ) + self.assertIn("background-image: url('/media/device-type-images/fritzbox.png')", rendered_image_grid) + self.assertIn("background-size: 220px", rendered_image_grid) + self.assertNotIn(">LEO-Fritzbox<", rendered_image_grid.replace("\n", "")) def test_resolved_reorder_page_uses_server_side_width_view(self): view_class = resolve(reverse("dcim:rack_reorder", kwargs={"pk": 3})).func.view_class @@ -414,7 +449,7 @@ class ReorderRackFrontendTest(SimpleTestCase): self.assertEqual(template_name, "netbox_utilities/reorder_rack.html") self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox") self.assertEqual(context["reorder_devices"][0]["grid_width"], 6) - self.assertEqual(context["asset_version"], "0.9.7") + self.assertEqual(context["asset_version"], "0.9.8") self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value) get_width_data.assert_called_once() self.assertIs(get_width_data.call_args.kwargs["rack"], rack) diff --git a/netbox_utilities/tests/test_topology_views.py b/netbox_utilities/tests/test_topology_views.py index e589ddf..458a1de 100644 --- a/netbox_utilities/tests/test_topology_views.py +++ b/netbox_utilities/tests/test_topology_views.py @@ -246,7 +246,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase): self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html) self.assertIn('.rack-device[href="/dcim/devices/334/"]', html) self.assertIn("left: calc(50% + 3px) !important", html) - self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.7", html) + self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.8", html) self.assertIn("left:calc(0% + 3px)!important", html) self.assertIn("left:calc(50% + 3px)!important", html) self.assertIn("width:calc(50% - 6px)!important", html) diff --git a/pyproject.toml b/pyproject.toml index 0dbdc88..2f9892d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "netbox-utilities" -version = "0.9.7" +version = "0.9.8" description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6" readme = "README.md" requires-python = ">=3.12"