From 6985169db61937297e65a1d700a485e9fa00eac8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Jul 2025 08:18:06 -0400 Subject: [PATCH] Initial work on #19591 --- base_requirements.txt | 4 ++ netbox/netbox/models/features.py | 4 ++ netbox/netbox/settings.py | 1 + netbox/netbox/views/generic/feature_views.py | 49 ++++++++++++++++++- .../extras/object_imageattachments.html | 26 ++++++++++ requirements.txt | 1 + 6 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 netbox/templates/extras/object_imageattachments.html diff --git a/base_requirements.txt b/base_requirements.txt index f2ccfa989..6cbffe0f8 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -139,6 +139,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/netbox/models/features.py b/netbox/netbox/models/features.py index 79145ce70..85a907884 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -686,6 +686,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..28d4893df 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,52 @@ class ObjectChangeLogView(ConditionalLoginRequiredMixin, View): }) +class ObjectImageAttachmentsView(ConditionalLoginRequiredMixin, View): + """ + Render a list of all Job assigned to an object. For example: + + path( + 'data-sources//jobs/', + ObjectJobsView.as_view(), + name='datasource_jobs', + kwargs={'model': DataSource} + ) + + 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.imageattachments.count(), + permission='extras.view_imageattachment', + weight=6000 + ) + + def get_object(self, request, **kwargs): + return get_object_or_404(self.model.objects.restrict(request.user, 'view'), **kwargs) + + def get(self, request, model, **kwargs): + self.model = model + obj = self.get_object(request, **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/templates/extras/object_imageattachments.html b/netbox/templates/extras/object_imageattachments.html new file mode 100644 index 000000000..010ce236d --- /dev/null +++ b/netbox/templates/extras/object_imageattachments.html @@ -0,0 +1,26 @@ +{% extends base_template %} +{% load render_table from django_tables2 %} +{% load thumbnail %} + +{% block content %} +
+
+ {% for object in image_attachments %} +
+
+ {% thumbnail object.image "200x200" crop="center" as tn %} + + + + {% endthumbnail %} +
+
+ {{ object }} +
+
+
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/requirements.txt b/requirements.txt index 07cbf82f7..4604c16a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ requests==2.32.4 rq==2.4.0 social-auth-app-django==5.5.1 social-auth-core==4.7.0 +sorl-thumbnail==12.11.0 strawberry-graphql==0.276.0 strawberry-graphql-django==0.60.0 svgwrite==1.4.3