fix: derive image attachment dimensions during import
This commit is contained in:
@@ -135,6 +135,9 @@ Belegungen löst **Aktualisieren** das Zielgerät mit Warnung von seiner Positio
|
|||||||
**Überspringen** lässt das importierte Gerät positionslos und **Import abbrechen**
|
**Überspringen** lässt das importierte Gerät positionslos und **Import abbrechen**
|
||||||
meldet den Rackplatzkonflikt vor dem Datenbankfehler. Mehr-U- und
|
meldet den Rackplatzkonflikt vor dem Datenbankfehler. Mehr-U- und
|
||||||
Full-Depth-Belegungen werden dabei berücksichtigt.
|
Full-Depth-Belegungen werden dabei berücksichtigt.
|
||||||
|
Bei Bildanhängen werden Breite und Höhe direkt aus der Bilddatei im Archiv
|
||||||
|
ermittelt. Dadurch sind die Pflichtfelder von NetBox auch im Prüflauf und bei
|
||||||
|
Dateispeichern ohne unmittelbaren Modell-Save gesetzt.
|
||||||
|
|
||||||
Auf Quelle und Ziel müssen jeweils dieselben Plugin-Versionen und Migrationen
|
Auf Quelle und Ziel müssen jeweils dieselben Plugin-Versionen und Migrationen
|
||||||
installiert sein. Verschlüsselte Zugangsdaten von NetBox-VM-Import sind nur bei
|
installiert sein. Verschlüsselte Zugangsdaten von NetBox-VM-Import sind nur bei
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class NetBoxExportConfig(PluginConfig):
|
|||||||
name = "netbox_export"
|
name = "netbox_export"
|
||||||
verbose_name = "NetBox-Export"
|
verbose_name = "NetBox-Export"
|
||||||
description = "Portable ZIP export and import for tenants and locations"
|
description = "Portable ZIP export and import for tenants and locations"
|
||||||
version = "0.3.9"
|
version = "0.3.10"
|
||||||
author = "NetBox Export contributors"
|
author = "NetBox Export contributors"
|
||||||
base_url = "netbox-export"
|
base_url = "netbox-export"
|
||||||
min_version = "4.6.0"
|
min_version = "4.6.0"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def export_scope(
|
|||||||
"created_at": datetime.now(UTC).isoformat(),
|
"created_at": datetime.now(UTC).isoformat(),
|
||||||
"source_instance": str(InstanceIdentity.local_id()),
|
"source_instance": str(InstanceIdentity.local_id()),
|
||||||
"source_netbox_version": getattr(getattr(settings, "RELEASE", None), "version", "4.6"),
|
"source_netbox_version": getattr(getattr(settings, "RELEASE", None), "version", "4.6"),
|
||||||
"plugin_version": "0.3.9",
|
"plugin_version": "0.3.10",
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": scope_type,
|
"type": scope_type,
|
||||||
"source_pk": str(scope_id),
|
"source_pk": str(scope_id),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.apps import apps
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.images import get_image_dimensions
|
||||||
from django.db import IntegrityError, models, transaction
|
from django.db import IntegrityError, models, transaction
|
||||||
|
|
||||||
from netbox_export.models import ImportedObjectMapping
|
from netbox_export.models import ImportedObjectMapping
|
||||||
@@ -462,15 +463,32 @@ def _set_generic_relations(obj, record, resolver, *, allow_deferred: bool):
|
|||||||
|
|
||||||
|
|
||||||
def _set_files(obj, record, assets, saved_files, *, dry_run: bool):
|
def _set_files(obj, record, assets, saved_files, *, dry_run: bool):
|
||||||
if dry_run:
|
|
||||||
return
|
|
||||||
for name, spec in record.get("files", {}).items():
|
for name, spec in record.get("files", {}).items():
|
||||||
if not spec or "path" not in spec or spec["path"] not in assets:
|
if not spec or "path" not in spec or spec["path"] not in assets:
|
||||||
continue
|
continue
|
||||||
filename = spec.get("name", spec["path"]).replace("\\", "/").rsplit("/", 1)[-1]
|
filename = spec.get("name", spec["path"]).replace("\\", "/").rsplit("/", 1)[-1]
|
||||||
|
field = obj._meta.get_field(name)
|
||||||
|
content = ContentFile(assets[spec["path"]])
|
||||||
|
dimensions = None
|
||||||
|
if isinstance(field, models.ImageField):
|
||||||
|
dimensions = get_image_dimensions(content)
|
||||||
|
content.seek(0)
|
||||||
|
width, height = dimensions
|
||||||
|
if field.width_field and width is not None:
|
||||||
|
setattr(obj, field.width_field, width)
|
||||||
|
if field.height_field and height is not None:
|
||||||
|
setattr(obj, field.height_field, height)
|
||||||
|
if dry_run:
|
||||||
|
continue
|
||||||
file_value = getattr(obj, name)
|
file_value = getattr(obj, name)
|
||||||
file_value.save(filename, ContentFile(assets[spec["path"]]), save=False)
|
file_value.save(filename, content, save=False)
|
||||||
saved_files.append((file_value.storage, file_value.name))
|
saved_files.append((file_value.storage, file_value.name))
|
||||||
|
if dimensions is not None:
|
||||||
|
width, height = dimensions
|
||||||
|
if field.width_field and width is not None:
|
||||||
|
setattr(obj, field.width_field, width)
|
||||||
|
if field.height_field and height is not None:
|
||||||
|
setattr(obj, field.height_field, height)
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_files(saved_files):
|
def _cleanup_files(saved_files):
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-export"
|
name = "netbox-export"
|
||||||
version = "0.3.9"
|
version = "0.3.10"
|
||||||
description = "Portable ZIP export and import for scoped NetBox data"
|
description = "Portable ZIP export and import for scoped NetBox data"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import io
|
||||||
|
|
||||||
|
from django.core.files.storage import InMemoryStorage
|
||||||
|
from django.db import models
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from netbox_export.services.importer import _cleanup_files, _set_files
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAsset(models.Model):
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to="test-images",
|
||||||
|
height_field="image_height",
|
||||||
|
width_field="image_width",
|
||||||
|
)
|
||||||
|
image_height = models.PositiveSmallIntegerField()
|
||||||
|
image_width = models.PositiveSmallIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "file_import_tests"
|
||||||
|
|
||||||
|
|
||||||
|
def image_bytes(width=3, height=2):
|
||||||
|
output = io.BytesIO()
|
||||||
|
Image.new("RGB", (width, height), color="white").save(output, format="PNG")
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def image_record():
|
||||||
|
return {
|
||||||
|
"files": {
|
||||||
|
"image": {
|
||||||
|
"path": "assets/extras.imageattachment_17/image/server-room.png",
|
||||||
|
"name": "image-attachments/location_2_Serverraum.jpg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dry_run_derives_required_image_dimensions_without_storing_file():
|
||||||
|
obj = ImageAsset()
|
||||||
|
assets = {"assets/extras.imageattachment_17/image/server-room.png": image_bytes()}
|
||||||
|
saved_files = []
|
||||||
|
|
||||||
|
_set_files(obj, image_record(), assets, saved_files, dry_run=True)
|
||||||
|
|
||||||
|
assert (obj.image_width, obj.image_height) == (3, 2)
|
||||||
|
assert not obj.image
|
||||||
|
assert saved_files == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_import_keeps_derived_dimensions_after_storage_save(monkeypatch):
|
||||||
|
field = ImageAsset._meta.get_field("image")
|
||||||
|
storage = InMemoryStorage()
|
||||||
|
monkeypatch.setattr(field, "storage", storage)
|
||||||
|
obj = ImageAsset()
|
||||||
|
assets = {"assets/extras.imageattachment_17/image/server-room.png": image_bytes(5, 4)}
|
||||||
|
saved_files = []
|
||||||
|
|
||||||
|
_set_files(obj, image_record(), assets, saved_files, dry_run=False)
|
||||||
|
|
||||||
|
assert (obj.image_width, obj.image_height) == (5, 4)
|
||||||
|
assert storage.exists(obj.image.name)
|
||||||
|
assert saved_files == [(storage, obj.image.name)]
|
||||||
|
|
||||||
|
_cleanup_files(saved_files)
|
||||||
|
assert not storage.exists(obj.image.name)
|
||||||
Reference in New Issue
Block a user