Просмотр исходного кода

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 месяцев назад
Родитель
Сommit
9a2fab1d48

+ 1 - 0
.gitignore

@@ -9,6 +9,7 @@ yarn-error.log*
 /netbox/netbox/configuration.py
 /netbox/netbox/configuration.py
 /netbox/netbox/ldap_config.py
 /netbox/netbox/ldap_config.py
 /netbox/local/*
 /netbox/local/*
+/netbox/media
 /netbox/reports/*
 /netbox/reports/*
 !/netbox/reports/__init__.py
 !/netbox/reports/__init__.py
 /netbox/scripts/*
 /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
 # https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
 social-auth-core
 social-auth-core
 
 
+# Image thumbnail generation
+# https://github.com/jazzband/sorl-thumbnail/blob/master/CHANGES.rst
+sorl-thumbnail
+
 # Strawberry GraphQL
 # Strawberry GraphQL
 # https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
 # https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
 strawberry-graphql
 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.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 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 django.utils.translation import gettext_lazy as _
 from rest_framework.utils.encoders import JSONEncoder
 from rest_framework.utils.encoders import JSONEncoder
 
 
@@ -728,6 +730,18 @@ class ImageAttachment(ChangeLoggedModel):
     def filename(self):
     def filename(self):
         return os.path.basename(self.image.name).split('_', 2)[2]
         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
     @property
     def size(self):
     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})(
             register_model_view(model, 'jobs', kwargs={'model': model})(
                 'netbox.views.generic.ObjectJobsView'
                 '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):
         if issubclass(model, SyncedDataMixin):
             register_model_view(model, 'sync', kwargs={'model': model})(
             register_model_view(model, 'sync', kwargs={'model': model})(
                 'netbox.views.generic.ObjectSyncDataView'
                 'netbox.views.generic.ObjectSyncDataView'

+ 1 - 0
netbox/netbox/settings.py

@@ -424,6 +424,7 @@ INSTALLED_APPS = [
     'mptt',
     'mptt',
     'rest_framework',
     'rest_framework',
     'social_django',
     'social_django',
+    'sorl.thumbnail',
     'taggit',
     'taggit',
     'timezone_field',
     'timezone_field',
     'core',
     '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.models import Job, ObjectChange
 from core.tables import JobTable, ObjectChangeTable
 from core.tables import JobTable, ObjectChangeTable
 from extras.forms import JournalEntryForm
 from extras.forms import JournalEntryForm
-from extras.models import JournalEntry
+from extras.models import ImageAttachment, JournalEntry
 from extras.tables import JournalEntryTable
 from extras.tables import JournalEntryTable
 from tenancy.models import ContactAssignment
 from tenancy.models import ContactAssignment
 from tenancy.tables import ContactAssignmentTable
 from tenancy.tables import ContactAssignmentTable
@@ -25,6 +25,7 @@ __all__ = (
     'BulkSyncDataView',
     'BulkSyncDataView',
     'ObjectChangeLogView',
     'ObjectChangeLogView',
     'ObjectContactsView',
     'ObjectContactsView',
+    'ObjectImageAttachmentsView',
     'ObjectJobsView',
     'ObjectJobsView',
     'ObjectJournalView',
     'ObjectJournalView',
     'ObjectSyncDataView',
     '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):
 class ObjectJournalView(ConditionalLoginRequiredMixin, View):
     """
     """
     Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this
     Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this

Разница между файлами не показана из-за своего большого размера
+ 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;
   height: auto;
 }
 }
 
 
+// Image attachment thumbnails
+.thumbnail {
+  max-width: 200px;
+  img {
+    border: 1px solid #606060;
+  }
+}
+
 body[data-bs-theme=dark] {
 body[data-bs-theme=dark] {
   // Assuming icon is black/white line art, invert it and tone down brightness
   // Assuming icon is black/white line art, invert it and tone down brightness
   img.plugin-icon {
   img.plugin-icon {

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

@@ -56,8 +56,8 @@
       <div class="card">
       <div class="card">
         <h2 class="card-header">{% trans "Image" %}</h2>
         <h2 class="card-header">{% trans "Image" %}</h2>
         <div class="card-body">
         <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>
           </a>
         </div>
         </div>
       </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
 rq==2.4.1
 social-auth-app-django==5.5.1
 social-auth-app-django==5.5.1
 social-auth-core==4.7.0
 social-auth-core==4.7.0
+sorl-thumbnail==12.11.0
 strawberry-graphql==0.278.0
 strawberry-graphql==0.278.0
 strawberry-graphql-django==0.65.1
 strawberry-graphql-django==0.65.1
 svgwrite==1.4.3
 svgwrite==1.4.3

Некоторые файлы не были показаны из-за большого количества измененных файлов