Jelajahi Sumber

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
Jeremy Stretch 6 bulan lalu
induk
melakukan
9a2fab1d48

+ 1 - 0
.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/*

+ 4 - 0
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

+ 14 - 0
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 <img> tag suitable for embedding in an HTML document.
+        """
+        return mark_safe('<img src="{url}" height="{height}" width="{width}" alt="{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):
         """

+ 0 - 2
netbox/media/devicetype-images/.gitignore

@@ -1,2 +0,0 @@
-*
-!.gitignore

+ 0 - 2
netbox/media/image-attachments/.gitignore

@@ -1,2 +0,0 @@
-*
-!.gitignore

+ 4 - 0
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'

+ 1 - 0
netbox/netbox/settings.py

@@ -424,6 +424,7 @@ INSTALLED_APPS = [
     'mptt',
     'rest_framework',
     'social_django',
+    'sorl.thumbnail',
     'taggit',
     'timezone_field',
     'core',

+ 37 - 1
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 "<app>/<model>.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

File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/netbox.css


+ 8 - 0
netbox/project-static/styles/custom/_misc.scss

@@ -81,6 +81,14 @@ img.plugin-icon {
   height: auto;
 }
 
+// Image attachment thumbnails
+.thumbnail {
+  max-width: 200px;
+  img {
+    border: 1px solid #606060;
+  }
+}
+
 body[data-bs-theme=dark] {
   // Assuming icon is black/white line art, invert it and tone down brightness
   img.plugin-icon {

+ 2 - 2
netbox/templates/extras/imageattachment.html

@@ -56,8 +56,8 @@
       <div class="card">
         <h2 class="card-header">{% trans "Image" %}</h2>
         <div class="card-body">
-          <a href="{{ object.image.url }}">
-            <img src="{{ object.image.url }}" height="{{ image.height }}" width="{{ image.width }}" alt="{{ object }}" />
+          <a href="{{ object.image.url }}" title="{{ object.name }}">
+            {{ object.html_tag }}
           </a>
         </div>
       </div>

+ 46 - 0
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" %}
+      <a href="{% url 'extras:imageattachment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% url viewname pk=object.pk %}" class="btn btn-primary">
+        <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Attach an Image" %}
+      </a>
+    {% endwith %}
+  {% endif %}
+{% endblock %}
+
+{% block content %}
+  {% if image_attachments %}
+    <div class="d-flex flex-wrap">
+      {% for object in image_attachments %}
+        <div class="thumbnail m-2">
+          {% thumbnail object.image "200x200" crop="center" as tn %}
+            <a href="{{ object.get_absolute_url }}" class="d-block" title="{{ object.name }}">
+              <img
+                src="{{ tn.url }}"
+                width="{{ tn.width }}"
+                height="{{ tn.height }}"
+                class="rounded"
+                alt="{{ object.description|default:object.name }}"
+              />
+            </a>
+          {% endthumbnail %}
+          <div class="text-center text-secondary text-truncate fs-5">
+            {{ object }}
+          </div>
+        </div>
+      {% endfor %}
+    </div>
+  {% else %}
+    <div class="alert alert-info">
+      {% blocktrans with object_type=object|meta:"verbose_name" %}
+        No images have been attached to this {{ object_type }}.
+      {% endblocktrans %}
+    </div>
+  {% endif %}
+{% endblock %}

+ 1 - 0
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

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini