Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Hauser
601296aa4a feat(ui): Add lazy loading and decoding options for ImageAttr
Introduces `load_lazy` and `decoding` parameters to `ImageAttr` for
enhanced image handling. Lazy loading improves page performance, while
configurable decoding options provide greater flexibility. Updates the
template to conditionally include these attributes in rendered HTML.

Fixes #21369
2026-02-17 18:29:54 +01:00
3 changed files with 34 additions and 46 deletions

View File

@@ -24,11 +24,13 @@ __all__ = (
PLACEHOLDER_HTML = '<span class="text-muted">&mdash;</span>'
IMAGE_DECODING_CHOICES = ('auto', 'async', 'sync')
#
# Attributes
#
class ObjectAttribute:
"""
Base class for representing an attribute of an object.
@@ -193,9 +195,37 @@ class ColorAttr(ObjectAttribute):
class ImageAttr(ObjectAttribute):
"""
An attribute representing an image field on the model. Displays the uploaded image.
Parameters:
load_lazy (bool): If True, the image will be loaded lazily (default: True)
decoding (str): Image decoding option ('async', 'sync', 'auto', None)
"""
template_name = 'ui/attrs/image.html'
def __init__(self, *args, load_lazy=True, decoding=None, **kwargs):
super().__init__(*args, **kwargs)
self.load_lazy = load_lazy
if decoding is not None and decoding not in IMAGE_DECODING_CHOICES:
raise ValueError(
_('Invalid decoding option: {decoding}! Must be one of {image_decoding_choices}').format(
decoding=decoding, image_decoding_choices=', '.join(IMAGE_DECODING_CHOICES)
)
)
# Compute default decoding:
# - lazy images: async decoding (performance-friendly hint)
# - non-lazy images: omit decoding (browser default/auto)
if decoding is None and load_lazy:
decoding = 'async'
self.decoding = decoding
def get_context(self, obj, context):
return {
'decoding': self.decoding,
'load_lazy': self.load_lazy,
}
class RelatedObjectAttr(ObjectAttribute):
"""

View File

@@ -1,3 +1,3 @@
<a href="{{ value.url }}">
<img src="{{ value.url }}" alt="{{ value.name }}" class="img-fluid" />
<img src="{{ value.url }}" alt="{{ value.name }}" class="img-fluid" {% if load_lazy %}loading="lazy" {% endif %} {% if decoding %}decoding="{{ decoding }}" {% endif %}/>
</a>

View File

@@ -1,58 +1,16 @@
# Ruff configuration
####################
exclude = [
".eggs",
".git",
".pyenv",
".pytest_cache",
".ruff_cache",
".venv",
".vscode",
"__pypackages__",
"_build",
"build",
"dist",
"netbox/project-static/**",
"node_modules",
"site-packages",
"venv",
"netbox/project-static/**"
]
# Enforce line length and indent-width
line-length = 120
indent-width = 4
# Ignores anything in .gitignore
respect-gitignore = true
# Always generate Python 3.12-compatible code
target-version = "py312"
[lint]
extend-select = [
"E1", # pycodestyle errors: indentation-related (e.g., unexpected/missing indent)
"E2", # pycodestyle errors: whitespace-related (e.g., missing whitespace, extra spaces)
"E3", # pycodestyle errors: blank lines / spacing around definitions
"E501", # pycodestyle: line too long (enforced with `line-length` above)
"W", # pycodestyle warnings (various style warnings, often whitespace/newlines)
]
ignore = [
"F403", # pyflakes: `from ... import *` used; unable to detect undefined names
"F405", # pyflakes: name may be undefined or defined from star imports
"UP032", # pyupgrade: prefer f-strings over `str.format(...)`
]
extend-select = ["E1", "E2", "E3", "E501", "W"]
ignore = ["F403", "F405"]
preview = true
[lint.per-file-ignores]
"template_code.py" = ["E501"]
[format]
# Use single quotes for strings.
quote-style = "single"
# Indent with spaces, rather than tabs.
indent-style = "space"
# Enforce UNIX line ending
line-ending = "lf"