diff --git a/README.md b/README.md index e395daf..468cac2 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,12 @@ autorisierten Rack-Datensatz. Die Topology-Rack-Ansicht leitet ihre Breiten ausschließlich aus den Geräten ab, die der Topology-View tatsächlich in seine HTML-Antwort geschrieben hat. Zusätzliche Berechtigungsabfragen können die beiden Ansichten dadurch nicht mehr fälschlich leeren. +Ab Version `0.9.7` enthält Reorder eigene 12-Spalten-CSS-Regeln. Diese beheben +die Inkompatibilität zwischen GridStack 10 aus Reorder Rack `1.1.4` und dem +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. 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 2bd9433..dd45012 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.6" +__version__ = "0.9.7" class NetBoxUtilitiesConfig(PluginConfig): @@ -38,10 +38,6 @@ class NetBoxUtilitiesConfig(PluginConfig): from .reorder_rack import install_reorder_rack_bulk_save install_reorder_rack_bulk_save() - if get_plugin_config("netbox_utilities", "topology_views_rack_width_enabled"): - from .topology_views import install_topology_rack_width_support - - install_topology_rack_width_support() config = NetBoxUtilitiesConfig diff --git a/netbox_utilities/middleware.py b/netbox_utilities/middleware.py index 4e6199a..1abde76 100644 --- a/netbox_utilities/middleware.py +++ b/netbox_utilities/middleware.py @@ -22,7 +22,10 @@ class GlobalTenantFilterMiddleware: try: if scope is not None and request.method in {"GET", "HEAD"}: self._inject_filter_parameter(request, scope) - return self.get_response(request) + response = self.get_response(request) + from .topology_views import apply_topology_rack_widths + + return apply_topology_rack_widths(request, response) finally: active_tenant_scope.reset(token) diff --git a/netbox_utilities/static/netbox_utilities/netbox_utilities.css b/netbox_utilities/static/netbox_utilities/netbox_utilities.css index 831f1e1..3a64774 100644 --- a/netbox_utilities/static/netbox_utilities/netbox_utilities.css +++ b/netbox_utilities/static/netbox_utilities/netbox_utilities.css @@ -36,6 +36,41 @@ width: calc(var(--netbox-utilities-rack-device-width) - 6px) !important; } +/* + * netbox-reorder-rack 1.1.4 bundles GridStack 10, while NetBox 4.6.7 + * 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. + */ +.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item { + width: calc(100% / 12) !important; +} + +.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-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-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 d24ed51..81639e2 100644 --- a/netbox_utilities/templates/netbox_utilities/reorder_rack.html +++ b/netbox_utilities/templates/netbox_utilities/reorder_rack.html @@ -34,7 +34,7 @@

Non-Racked Devices

-
{% for device in non_racked %}
{{ unit|rack_unit_to_int }}{% else %}
  • {% endif %} {% endfor %} -
    {% for device in reorder_devices %} diff --git a/netbox_utilities/tests/test_middleware.py b/netbox_utilities/tests/test_middleware.py index 006b778..03c6d3a 100644 --- a/netbox_utilities/tests/test_middleware.py +++ b/netbox_utilities/tests/test_middleware.py @@ -1,6 +1,7 @@ from types import SimpleNamespace from unittest.mock import patch +from django.http import HttpResponse from django.test import RequestFactory, SimpleTestCase from netbox_utilities.middleware import GlobalTenantFilterMiddleware @@ -28,6 +29,21 @@ class GlobalTenantFilterMiddlewareTest(SimpleTestCase): def setUp(self): self.factory = RequestFactory() + @patch("netbox_utilities.topology_views.apply_topology_rack_widths") + @patch("netbox_utilities.middleware.GlobalTenantFilterMiddleware._get_selected_scope", return_value=None) + def test_processes_topology_widths_after_the_view_response(self, _scope, apply_widths): + native_response = HttpResponse("native topology") + final_response = HttpResponse("width-aware topology") + apply_widths.return_value = final_response + middleware = GlobalTenantFilterMiddleware(lambda _request: native_response) + request = self.factory.get("/plugins/netbox_topology_views/rack-elevation/?rack_id=3") + request.session = {} + + result = middleware(request) + + self.assertIs(result, final_response) + apply_widths.assert_called_once_with(request, native_response) + @patch("netbox_utilities.middleware.resolve") def test_injects_and_overrides_tenant_id(self, mocked_resolve): mocked_resolve.return_value = resolver_match(TenantFilterSet) diff --git a/netbox_utilities/tests/test_reorder_rack.py b/netbox_utilities/tests/test_reorder_rack.py index f8a1827..de42483 100644 --- a/netbox_utilities/tests/test_reorder_rack.py +++ b/netbox_utilities/tests/test_reorder_rack.py @@ -310,6 +310,7 @@ class ReorderRackFrontendTest(SimpleTestCase): self.assertIn("{% for device in reorder_devices %}", grid) self.assertIn('gs-w="{{ device.grid_width }}"', grid) self.assertIn('gs-x="{{ device.grid_x }}"', grid) + self.assertIn("netbox-utilities-reorder-grid", grid) self.assertIn('json_script:"netbox-utilities-reorder-rack-width-data"', page) self.assertIn("netbox_utilities/reorder-rack-width.js", page) @@ -343,6 +344,14 @@ class ReorderRackFrontendTest(SimpleTestCase): self.assertIn('gs-id="10"', rendered_grid) self.assertIn("LEO-Fritzbox", rendered_grid) + stylesheet = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "netbox_utilities.css").read_text( + 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) + def test_resolved_reorder_page_uses_server_side_width_view(self): view_class = resolve(reverse("dcim:rack_reorder", kwargs={"pk": 3})).func.view_class @@ -405,7 +414,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.6") + self.assertEqual(context["asset_version"], "0.9.7") 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 f096775..e589ddf 100644 --- a/netbox_utilities/tests/test_topology_views.py +++ b/netbox_utilities/tests/test_topology_views.py @@ -6,11 +6,12 @@ from django.http import HttpResponse from django.test import RequestFactory, SimpleTestCase from netbox_utilities.topology_views import ( + _apply_topology_widths_to_device_tags, _inject_topology_widths, _topology_data_for_visible_devices, _visible_topology_device_urls, + apply_topology_rack_widths, get_topology_rack_width_data, - install_topology_rack_width_support, is_topology_rack_elevation_request, serialize_topology_placement, topology_rack_width_enabled, @@ -245,24 +246,40 @@ 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.6", html) + self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.7", 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) - def test_patches_the_optional_topology_view_only_once(self): - def original_get(self, request): - return self, request + def test_preserves_existing_topology_style_while_applying_width(self): + data = { + "devices": [ + { + "url": "/dcim/devices/334/", + "left_percent": 0, + "width_percent": 50, + } + ] + } + html = ( + 'Fritzbox' + ) - class RackElevationView: - get = original_get + result = _apply_topology_widths_to_device_tags(html, data) - module = SimpleNamespace(RackElevationView=RackElevationView) - with ( - patch("netbox_utilities.topology_views.apps.is_installed", return_value=True), - patch("netbox_utilities.topology_views.get_plugin_config", return_value=True), - patch("netbox_utilities.topology_views.import_module", return_value=module), - ): - self.assertTrue(install_topology_rack_width_support()) - patched_get = RackElevationView.get - self.assertTrue(install_topology_rack_width_support()) + self.assertIn("bottom: 10%", result) + self.assertIn("height: 2%", result) + self.assertIn("left:calc(0% + 3px)!important", result) + self.assertIn("width:calc(50% - 6px)!important", result) - self.assertIs(RackElevationView.get, patched_get) - self.assertIs(RackElevationView._netbox_utilities_original_get, original_get) + @patch("netbox_utilities.topology_views._inject_topology_widths") + @patch("netbox_utilities.topology_views.topology_rack_width_enabled", return_value=True) + def test_response_processor_runs_after_the_topology_view(self, _enabled, inject): + response = HttpResponse("") + inject.return_value = response + + result = apply_topology_rack_widths(self.request, response) + + self.assertIs(result, response) + inject.assert_called_once_with(response) diff --git a/netbox_utilities/topology_views.py b/netbox_utilities/topology_views.py index 0e939b0..165ad5d 100644 --- a/netbox_utilities/topology_views.py +++ b/netbox_utilities/topology_views.py @@ -1,8 +1,6 @@ import logging import re -from functools import wraps from html import unescape -from importlib import import_module from django.apps import apps from django.core.exceptions import ObjectDoesNotExist @@ -17,12 +15,12 @@ from .rack_width import effective_width_positions logger = logging.getLogger(__name__) TOPOLOGY_RACK_ELEVATION_VIEW = "plugins:netbox_topology_views:rack_elevation" -TOPOLOGY_VIEW_PATCH_MARKER = "_netbox_utilities_rack_width_view" RACK_DEVICE_TAG_RE = re.compile( r"]*\bclass=[\"'][^\"']*\brack-device\b[^\"']*[\"'][^>]*>", re.IGNORECASE, ) HREF_RE = re.compile(r"\bhref=[\"'](?P[^\"']+)[\"']", re.IGNORECASE) +STYLE_RE = re.compile(r"\bstyle=(?P[\"'])(?P