fix: update rack width controls and images
This commit is contained in:
@@ -36,7 +36,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
|
/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.
|
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
|
vertikale Überschneidungen. Mehrere HE hohe Geräte werden ebenfalls
|
||||||
unterstützt.
|
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
|
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
|
automatisch für sämtliche vorhandenen Geräte; es findet keine Änderung oder
|
||||||
Migration bestehender Platzierungen statt. Teilbreite und Position werden auf
|
Migration bestehender Platzierungen statt. Teilbreite und Position werden auf
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from netbox.plugins import PluginConfig, get_plugin_config
|
from netbox.plugins import PluginConfig, get_plugin_config
|
||||||
|
|
||||||
__version__ = "0.8.6"
|
__version__ = "0.8.7"
|
||||||
|
|
||||||
|
|
||||||
class NetBoxUtilitiesConfig(PluginConfig):
|
class NetBoxUtilitiesConfig(PluginConfig):
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ def _install_rack_svg():
|
|||||||
elevation.draw_device_front(device, coords, size)
|
elevation.draw_device_front(device, coords, size)
|
||||||
else:
|
else:
|
||||||
elevation.draw_device_rear(device, coords, size)
|
elevation.draw_device_rear(device, coords, size)
|
||||||
|
_fit_latest_elevation_image(elevation.drawing)
|
||||||
else:
|
else:
|
||||||
from svgwrite.shapes import Rect
|
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):
|
def _validate_before_save(sender, instance, raw=False, **kwargs):
|
||||||
if not raw:
|
if not raw:
|
||||||
using = kwargs.get("using")
|
using = kwargs.get("using")
|
||||||
|
|||||||
@@ -103,11 +103,18 @@
|
|||||||
const position = Number.parseInt(option.value || '1', 10);
|
const position = Number.parseInt(option.value || '1', 10);
|
||||||
option.disabled = position > width;
|
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.value = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
positionField.disabled = width === 1;
|
||||||
|
if (positionField.tomselect) positionField.tomselect.sync();
|
||||||
|
|
||||||
|
if (resetPosition) {
|
||||||
positionField.dispatchEvent(new Event('change', {bubbles: true}));
|
positionField.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
}
|
}
|
||||||
positionField.disabled = width === 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
widthField.addEventListener('change', updatePositions);
|
widthField.addEventListener('change', updatePositions);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ from django.urls import resolve, reverse
|
|||||||
|
|
||||||
from netbox_utilities.rack_width import (
|
from netbox_utilities.rack_width import (
|
||||||
_cleanup_unracked_placement,
|
_cleanup_unracked_placement,
|
||||||
|
_fit_latest_elevation_image,
|
||||||
_partial_width_elevation_devices,
|
_partial_width_elevation_devices,
|
||||||
available_units_for_device,
|
available_units_for_device,
|
||||||
horizontal_interval,
|
horizontal_interval,
|
||||||
@@ -25,6 +27,28 @@ from netbox_utilities.rack_width import (
|
|||||||
|
|
||||||
|
|
||||||
class RackWidthTest(SimpleTestCase):
|
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):
|
def test_partial_width_svg_devices_include_core_device_bay_annotation(self):
|
||||||
from dcim.choices import DeviceFaceChoices
|
from dcim.choices import DeviceFaceChoices
|
||||||
from dcim.models import Rack
|
from dcim.models import Rack
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-utilities"
|
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"
|
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