From 4ee78ae4fe4ace7f8068aa7d5ad0cd6e4ddaaf3f Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 12 Aug 2026 15:57:22 +0200 Subject: [PATCH] fix: update rack width controls and images --- README.md | 8 ++++++- netbox_utilities/__init__.py | 2 +- netbox_utilities/rack_width.py | 14 +++++++++++ .../static/netbox_utilities/forms.js | 11 +++++++-- netbox_utilities/tests/test_rack_width.py | 24 +++++++++++++++++++ pyproject.toml | 2 +- 6 files changed, 56 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1a2485a..9f7b461 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden: ```bash /opt/netbox/venv/bin/pip install --upgrade --force-reinstall \ - "git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.8.6" + "git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.8.7" ``` Alternativ kann hinter dem `@` die vollständige Commit-ID stehen. @@ -150,6 +150,12 @@ sowohl die Gerätehöhe als auch die Breite und verhindert horizontale oder vertikale Überschneidungen. Mehrere HE hohe Geräte werden ebenfalls unterstützt. +Nach einer Änderung der Rackbreite wird die **Breitenposition** unmittelbar +aktualisiert; ein Zwischenspeichern oder Neuladen ist nicht erforderlich. +Hinterlegte Vorder- und Rückseitenbilder des Gerätetyps werden bei +Teilbreitengeräten proportional in die verfügbare Breite eingepasst, ohne das +Bild seitlich abzuschneiden. + Ohne Breitenangabe belegt ein Gerät wie bisher die volle Rackbreite. Das gilt automatisch für sämtliche vorhandenen Geräte; es findet keine Änderung oder Migration bestehender Platzierungen statt. Teilbreite und Position werden auf diff --git a/netbox_utilities/__init__.py b/netbox_utilities/__init__.py index 418b146..9d47c74 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.8.6" +__version__ = "0.8.7" class NetBoxUtilitiesConfig(PluginConfig): diff --git a/netbox_utilities/rack_width.py b/netbox_utilities/rack_width.py index deebfdb..87b8992 100644 --- a/netbox_utilities/rack_width.py +++ b/netbox_utilities/rack_width.py @@ -465,6 +465,7 @@ def _install_rack_svg(): elevation.draw_device_front(device, coords, size) else: elevation.draw_device_rear(device, coords, size) + _fit_latest_elevation_image(elevation.drawing) else: from svgwrite.shapes import Rect @@ -496,6 +497,19 @@ def _partial_width_elevation_devices(rack, face): ) +def _fit_latest_elevation_image(drawing): + """Fit the latest device image into a partial-width slot without cropping it.""" + from svgwrite.image import Image + + if not getattr(drawing, "elements", None): + return + container = drawing.elements[-1] + for element in reversed(getattr(container, "elements", ())): + if isinstance(element, Image): + element.fit(scale="meet") + return + + def _validate_before_save(sender, instance, raw=False, **kwargs): if not raw: using = kwargs.get("using") diff --git a/netbox_utilities/static/netbox_utilities/forms.js b/netbox_utilities/static/netbox_utilities/forms.js index 4cb50d8..a1a7440 100644 --- a/netbox_utilities/static/netbox_utilities/forms.js +++ b/netbox_utilities/static/netbox_utilities/forms.js @@ -103,11 +103,18 @@ const position = Number.parseInt(option.value || '1', 10); option.disabled = position > width; }); - if (width === 1 || Number.parseInt(positionField.value || '1', 10) > width) { + + const resetPosition = width === 1 || Number.parseInt(positionField.value || '1', 10) > width; + if (resetPosition) { positionField.value = '1'; + } + + positionField.disabled = width === 1; + if (positionField.tomselect) positionField.tomselect.sync(); + + if (resetPosition) { positionField.dispatchEvent(new Event('change', {bubbles: true})); } - positionField.disabled = width === 1; }; widthField.addEventListener('change', updatePositions); diff --git a/netbox_utilities/tests/test_rack_width.py b/netbox_utilities/tests/test_rack_width.py index b972182..3913988 100644 --- a/netbox_utilities/tests/test_rack_width.py +++ b/netbox_utilities/tests/test_rack_width.py @@ -1,4 +1,5 @@ from importlib import import_module +from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -12,6 +13,7 @@ from django.urls import resolve, reverse from netbox_utilities.rack_width import ( _cleanup_unracked_placement, + _fit_latest_elevation_image, _partial_width_elevation_devices, available_units_for_device, horizontal_interval, @@ -25,6 +27,28 @@ from netbox_utilities.rack_width import ( class RackWidthTest(SimpleTestCase): + def test_rack_width_javascript_synchronizes_netbox_tom_select(self): + script = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "forms.js").read_text(encoding="utf-8") + + self.assertIn("positionField.disabled = width === 1", script) + self.assertIn("positionField.tomselect.sync()", script) + + def test_partial_width_device_image_is_fitted_without_cropping(self): + from svgwrite import Drawing + from svgwrite.container import Hyperlink + from svgwrite.image import Image + + drawing = Drawing() + link = Hyperlink("/dcim/devices/1/") + device_image = Image("/media/device.png", size=(100, 20)) + device_image.fit(scale="slice") + link.add(device_image) + drawing.add(link) + + _fit_latest_elevation_image(drawing) + + self.assertEqual(device_image.attribs["preserveAspectRatio"], "xMidYMid meet") + def test_partial_width_svg_devices_include_core_device_bay_annotation(self): from dcim.choices import DeviceFaceChoices from dcim.models import Rack diff --git a/pyproject.toml b/pyproject.toml index f5f3aea..0b5fa24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "netbox-utilities" -version = "0.8.6" +version = "0.8.7" description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6" readme = "README.md" requires-python = ">=3.12"