From 9a2fab1d486183f06dbed4267b9eae24723f1d8a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Jul 2025 16:22:04 -0400 Subject: [PATCH] Closes #19591: Establish dedicated tab for image attachments (#19919) * Initial work on #19591 * Ignore images cache directory * Clean up thumbnails layout * Include "add attachment" button * Clean up ObjectImageAttachmentsView * Add html_tag property to ImageAttachment * Misc cleanup * Collapse .gitignore files for /media * Fix conditional in template --- .gitignore | 1 + base_requirements.txt | 4 ++ netbox/extras/models/models.py | 14 ++++++ netbox/media/devicetype-images/.gitignore | 2 - netbox/media/image-attachments/.gitignore | 2 - netbox/netbox/models/features.py | 4 ++ netbox/netbox/settings.py | 1 + netbox/netbox/views/generic/feature_views.py | 38 ++++++++++++++- netbox/project-static/dist/netbox.css | Bin 555402 -> 555469 bytes .../project-static/styles/custom/_misc.scss | 8 +++ netbox/templates/extras/imageattachment.html | 4 +- .../extras/object_imageattachments.html | 46 ++++++++++++++++++ requirements.txt | 1 + 13 files changed, 118 insertions(+), 7 deletions(-) delete mode 100644 netbox/media/devicetype-images/.gitignore delete mode 100644 netbox/media/image-attachments/.gitignore create mode 100644 netbox/templates/extras/object_imageattachments.html diff --git a/.gitignore b/.gitignore index e04e44a30..eb1eccbef 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ yarn-error.log* /netbox/netbox/configuration.py /netbox/netbox/ldap_config.py /netbox/local/* +/netbox/media /netbox/reports/* !/netbox/reports/__init__.py /netbox/scripts/* diff --git a/base_requirements.txt b/base_requirements.txt index d11eff972..7ba631bb9 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -141,6 +141,10 @@ social-auth-app-django # https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md social-auth-core +# Image thumbnail generation +# https://github.com/jazzband/sorl-thumbnail/blob/master/CHANGES.rst +sorl-thumbnail + # Strawberry GraphQL # https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md strawberry-graphql diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index fc69e692c..d687aa821 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -9,6 +9,8 @@ from django.core.validators import ValidationError from django.db import models from django.urls import reverse from django.utils import timezone +from django.utils.html import escape +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from rest_framework.utils.encoders import JSONEncoder @@ -728,6 +730,18 @@ class ImageAttachment(ChangeLoggedModel): def filename(self): return os.path.basename(self.image.name).split('_', 2)[2] + @property + def html_tag(self): + """ + Returns a complete tag suitable for embedding in an HTML document. + """ + return mark_safe('{alt_text}'.format( + url=self.image.url, + height=self.image_height, + width=self.image_width, + alt_text=escape(self.description or self.name), + )) + @property def size(self): """ diff --git a/netbox/media/devicetype-images/.gitignore b/netbox/media/devicetype-images/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/netbox/media/devicetype-images/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/netbox/media/image-attachments/.gitignore b/netbox/media/image-attachments/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/netbox/media/image-attachments/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index b71eecaaa..2f0e63f7f 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -736,6 +736,10 @@ def register_models(*models): register_model_view(model, 'jobs', kwargs={'model': model})( 'netbox.views.generic.ObjectJobsView' ) + if issubclass(model, ImageAttachmentsMixin): + register_model_view(model, 'image-attachments', kwargs={'model': model})( + 'netbox.views.generic.ObjectImageAttachmentsView' + ) if issubclass(model, SyncedDataMixin): register_model_view(model, 'sync', kwargs={'model': model})( 'netbox.views.generic.ObjectSyncDataView' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d293a9979..35900f60e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -424,6 +424,7 @@ INSTALLED_APPS = [ 'mptt', 'rest_framework', 'social_django', + 'sorl.thumbnail', 'taggit', 'timezone_field', 'core', diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index d8ba2b475..338ed0628 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -10,7 +10,7 @@ from django.views.generic import View from core.models import Job, ObjectChange from core.tables import JobTable, ObjectChangeTable from extras.forms import JournalEntryForm -from extras.models import JournalEntry +from extras.models import ImageAttachment, JournalEntry from extras.tables import JournalEntryTable from tenancy.models import ContactAssignment from tenancy.tables import ContactAssignmentTable @@ -25,6 +25,7 @@ __all__ = ( 'BulkSyncDataView', 'ObjectChangeLogView', 'ObjectContactsView', + 'ObjectImageAttachmentsView', 'ObjectJobsView', 'ObjectJournalView', 'ObjectSyncDataView', @@ -84,6 +85,41 @@ class ObjectChangeLogView(ConditionalLoginRequiredMixin, View): }) +class ObjectImageAttachmentsView(ConditionalLoginRequiredMixin, View): + """ + Render all images attached to the object as linked thumbnails. + + Attributes: + base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. + """ + base_template = None + tab = ViewTab( + label=_('Images'), + badge=lambda obj: obj.images.count(), + permission='extras.view_imageattachment', + weight=6000 + ) + + def get(self, request, model, **kwargs): + obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) + image_attachments = ImageAttachment.objects.filter( + object_type=ContentType.objects.get_for_model(obj), + object_id=obj.pk, + ) + + # Default to using "/.html" as the template, if it exists. Otherwise, + # fall back to using base.html. + if self.base_template is None: + self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html" + + return render(request, 'extras/object_imageattachments.html', { + 'object': obj, + 'image_attachments': image_attachments, + 'base_template': self.base_template, + 'tab': self.tab, + }) + + class ObjectJournalView(ConditionalLoginRequiredMixin, View): """ Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 4ceb4e5240cee70bf5f71c3480ab6a90ae36cf2c..91d3ea25c940b6865149a7fcf73859cfe5816f65 100644 GIT binary patch delta 89 zcmeDBta$dbVnYjK3sVbo3rh=Y3tJ2O7LGX;ntCM}rMXFYiJ3Xoxrr6J<(VlZ8CFIH i1_c$h2q}fk-1O-Sl{tjj%nU$qdSVub)by

{% trans "Image" %}

diff --git a/netbox/templates/extras/object_imageattachments.html b/netbox/templates/extras/object_imageattachments.html new file mode 100644 index 000000000..0f6947a95 --- /dev/null +++ b/netbox/templates/extras/object_imageattachments.html @@ -0,0 +1,46 @@ +{% extends base_template %} +{% load helpers %} +{% load i18n %} +{% load render_table from django_tables2 %} +{% load thumbnail %} + +{% block extra_controls %} + {% if perms.extras.add_imageattachment %} + {% with viewname=object|viewname:"image-attachments" %} + + {% trans "Attach an Image" %} + + {% endwith %} + {% endif %} +{% endblock %} + +{% block content %} + {% if image_attachments %} +
+ {% for object in image_attachments %} +
+ {% thumbnail object.image "200x200" crop="center" as tn %} + + {{ object.description|default:object.name }} + + {% endthumbnail %} +
+ {{ object }} +
+
+ {% endfor %} +
+ {% else %} +
+ {% blocktrans with object_type=object|meta:"verbose_name" %} + No images have been attached to this {{ object_type }}. + {% endblocktrans %} +
+ {% endif %} +{% endblock %} diff --git a/requirements.txt b/requirements.txt index f8c1dc859..28481d445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ requests==2.32.4 rq==2.4.1 social-auth-app-django==5.5.1 social-auth-core==4.7.0 +sorl-thumbnail==12.11.0 strawberry-graphql==0.278.0 strawberry-graphql-django==0.65.1 svgwrite==1.4.3