Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7c9ce995f | ||
|
|
a9fd36f31c |
@@ -359,7 +359,17 @@ The operation is atomic: if a bay is taken meanwhile or a naming conflict
|
||||
occurs, nothing is saved. Requires the permission to add modules; object
|
||||
permissions still apply.
|
||||
|
||||
<<<<<<< HEAD
|
||||
NetBox begrenzt Pillow global auf 25 Megapixel und lehnt Bilder ab 50
|
||||
Megapixeln ab. Damit aktuelle 50-MP-Smartphone-Fotos (z. B. Google Pixel,
|
||||
8160×6144) hochgeladen werden können, hebt der Mehrfach-Upload dieses Limit
|
||||
während der Verarbeitung auf 100 Megapixel an. Wird ein Bild abgelehnt, zeigt
|
||||
das Formular zusätzlich die genaue Fehlermeldung von Pillow an.
|
||||
|
||||
### Module mehrfach einbauen
|
||||
=======
|
||||
### Personalised navigation
|
||||
>>>>>>> a85e4d6375a673a333d601fa8867e00a96423fec
|
||||
|
||||
Under **Plugins > NetBox Utilities > Customize navigation** users see every menu
|
||||
they have access to. Arrow buttons change the order; a toggle hides a menu.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from netbox.plugins import PluginConfig, get_plugin_config
|
||||
|
||||
__version__ = "0.14.0"
|
||||
__version__ = "0.14.1"
|
||||
|
||||
|
||||
class NetBoxUtilitiesConfig(PluginConfig):
|
||||
|
||||
@@ -35,6 +35,14 @@ class PatchpanelPairingTest(SimpleTestCase):
|
||||
|
||||
synchronize.assert_called_once_with(42)
|
||||
|
||||
@patch("netbox_utilities.patchpanel.synchronize_patchpanel")
|
||||
def test_raw_component_imports_do_not_trigger_automation(self, synchronize):
|
||||
component = SimpleNamespace(device_id=42)
|
||||
|
||||
_component_saved(None, component, created=True, raw=True)
|
||||
|
||||
synchronize.assert_not_called()
|
||||
|
||||
@patch("netbox_utilities.patchpanel.synchronize_patchpanel")
|
||||
def test_existing_device_save_does_not_trigger_automation(self, synchronize):
|
||||
class Container:
|
||||
@@ -89,6 +97,14 @@ class PatchpanelPairingTest(SimpleTestCase):
|
||||
self.assertEqual(port_identifier(port(2, "Rear Port 1")), (1,))
|
||||
self.assertEqual(port_identifier(port(3, "FrontPort01")), (1,))
|
||||
|
||||
def test_pairs_plain_numeric_port_names_one_to_one(self):
|
||||
front_ports = [port(24, "24"), port(1, "1"), port(2, "2")]
|
||||
rear_ports = [port(102, "2"), port(124, "24"), port(101, "1")]
|
||||
|
||||
pairs = pair_ports(front_ports, rear_ports)
|
||||
|
||||
self.assertEqual([(front.pk, rear.pk) for front, rear in pairs], [(1, 101), (2, 102), (24, 124)])
|
||||
|
||||
def test_does_not_pair_different_identifiers(self):
|
||||
pairs = pair_ports([port(1, "Front 1")], [port(102, "Rear 2")])
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
|
||||
from dcim.models import Device, Module, Rack
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
@@ -50,6 +53,24 @@ def _safe_return_url(request, default_name="home"):
|
||||
return reverse(default_name)
|
||||
|
||||
|
||||
# NetBox limits Pillow to 25 MP (bomb error above 50 MP), which rejects
|
||||
# current 50 MP smartphone photos such as Google Pixel images (8160x6144).
|
||||
BULK_IMAGE_MAX_PIXELS = 100_000_000
|
||||
|
||||
|
||||
@contextmanager
|
||||
def raised_image_pixel_limit():
|
||||
from PIL import Image
|
||||
|
||||
original_limit = Image.MAX_IMAGE_PIXELS
|
||||
if original_limit is not None and original_limit < BULK_IMAGE_MAX_PIXELS:
|
||||
Image.MAX_IMAGE_PIXELS = BULK_IMAGE_MAX_PIXELS
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
Image.MAX_IMAGE_PIXELS = original_limit
|
||||
|
||||
|
||||
class BulkImageUploadView(ContentTypePermissionRequiredMixin, View):
|
||||
template_name = "netbox_utilities/bulk_image_upload.html"
|
||||
|
||||
@@ -65,9 +86,19 @@ class BulkImageUploadView(ContentTypePermissionRequiredMixin, View):
|
||||
parent = self._get_parent(request, request.POST)
|
||||
form = BulkImageUploadForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
with raised_image_pixel_limit():
|
||||
return self._process_upload(request, form, parent)
|
||||
return self._render(request, form, parent)
|
||||
|
||||
def _process_upload(self, request, form, parent):
|
||||
image_forms = self._build_image_forms(parent, form.cleaned_data)
|
||||
invalid = [image_form for image_form in image_forms if not image_form.is_valid()]
|
||||
if invalid:
|
||||
for image_form in invalid:
|
||||
image = image_form.files.get("image")
|
||||
reason = self._image_error_reason(image)
|
||||
if reason:
|
||||
form.add_error("images", f"{getattr(image, 'name', 'Bild')}: Pillow meldet: {reason}")
|
||||
for image_form in invalid:
|
||||
filename = getattr(image_form.files.get("image"), "name", "Bild")
|
||||
for errors in image_form.errors.values():
|
||||
@@ -109,6 +140,26 @@ class BulkImageUploadView(ContentTypePermissionRequiredMixin, View):
|
||||
queryset = queryset.restrict(request.user, "view")
|
||||
return get_object_or_404(queryset, pk=object_id)
|
||||
|
||||
@staticmethod
|
||||
def _image_error_reason(image):
|
||||
"""Return the underlying Pillow error, which Django hides behind a generic message."""
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
if hasattr(image, "temporary_file_path"):
|
||||
source = image.temporary_file_path()
|
||||
else:
|
||||
image.seek(0)
|
||||
source = BytesIO(image.read())
|
||||
with Image.open(source) as opened:
|
||||
opened.verify()
|
||||
except Exception as error: # noqa: BLE001 - surface any Pillow failure
|
||||
return f"{type(error).__name__}: {error}"
|
||||
finally:
|
||||
if hasattr(image, "seek"):
|
||||
image.seek(0)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _build_image_forms(parent, cleaned_data):
|
||||
image_forms = []
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "netbox-utilities"
|
||||
version = "0.14.0"
|
||||
version = "0.14.1"
|
||||
description = "Navigation, tenant utilities, connection VLANs, partial-width racks, and bulk operations for NetBox 4.6/4.7"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
Reference in New Issue
Block a user