Closes #18990: Add description field to ImageAttachment model (#19907)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run

This commit is contained in:
Jeremy Stretch 2025-07-18 10:58:54 -04:00 committed by GitHub
parent cebc56e5cc
commit 4e0e4598b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 125 additions and 21 deletions

View File

@ -24,10 +24,10 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment
fields = [ fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'description',
'image_height', 'image_width', 'created', 'last_updated', 'image_height', 'image_width', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'image') brief_fields = ('id', 'url', 'display', 'name', 'image', 'description')
def validate(self, data): def validate(self, data):

View File

@ -451,12 +451,15 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment
fields = ('id', 'object_type_id', 'object_id', 'name', 'image_width', 'image_height') fields = ('id', 'object_type_id', 'object_id', 'name', 'description', 'image_width', 'image_height')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter(name__icontains=value) return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class JournalEntryFilterSet(NetBoxModelFilterSet): class JournalEntryFilterSet(NetBoxModelFilterSet):

View File

@ -744,14 +744,17 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
class ImageAttachmentForm(forms.ModelForm): class ImageAttachmentForm(forms.ModelForm):
fieldsets = ( fieldsets = (
FieldSet(ObjectAttribute('parent'), 'name', 'image'), FieldSet(ObjectAttribute('parent'), 'image', 'name', 'description'),
) )
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment
fields = [ fields = [
'name', 'image', 'image', 'name', 'description',
] ]
help_texts = {
'name': _("If no name is specified, the file name will be used.")
}
class JournalEntryForm(NetBoxModelForm): class JournalEntryForm(NetBoxModelForm):

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0129_fix_script_paths'),
]
operations = [
migrations.AddField(
model_name='imageattachment',
name='description',
field=models.CharField(blank=True, max_length=200),
),
]

View File

@ -1,4 +1,5 @@
import json import json
import os
import urllib.parse import urllib.parse
from django.conf import settings from django.conf import settings
@ -678,6 +679,11 @@ class ImageAttachment(ChangeLoggedModel):
max_length=50, max_length=50,
blank=True blank=True
) )
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@ -692,10 +698,10 @@ class ImageAttachment(ChangeLoggedModel):
verbose_name_plural = _('image attachments') verbose_name_plural = _('image attachments')
def __str__(self): def __str__(self):
if self.name: return self.name or self.filename
return self.name
filename = self.image.name.rsplit('/', 1)[-1] def get_absolute_url(self):
return filename.split('_', 2)[2] return reverse('extras:imageattachment', args=[self.pk])
def clean(self): def clean(self):
super().clean() super().clean()
@ -719,6 +725,10 @@ class ImageAttachment(ChangeLoggedModel):
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.) # before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
self.image.name = _name self.image.name = _name
@property
def filename(self):
return os.path.basename(self.image.name).split('_', 2)[2]
@property @property
def size(self): def size(self):
""" """

View File

@ -14,6 +14,16 @@ class CustomFieldIndex(SearchIndex):
display_attrs = ('description',) display_attrs = ('description',)
@register_search
class ImageAttachmentIndex(SearchIndex):
model = models.ImageAttachment
fields = (
('name', 100),
('description', 500),
)
display_attrs = ('description',)
@register_search @register_search
class JournalEntryIndex(SearchIndex): class JournalEntryIndex(SearchIndex):
model = models.JournalEntry model = models.JournalEntry

View File

@ -249,10 +249,10 @@ class ImageAttachmentTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ImageAttachment model = ImageAttachment
fields = ( fields = (
'pk', 'object_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', 'pk', 'object_type', 'parent', 'image', 'name', 'description', 'image_height', 'image_width', 'size',
'last_updated', 'created', 'last_updated',
) )
default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created') default_columns = ('object_type', 'parent', 'image', 'name', 'description', 'size', 'created')
class SavedFilterTable(NetBoxTable): class SavedFilterTable(NetBoxTable):

View File

@ -579,7 +579,7 @@ class ImageAttachmentTest(
APIViewTestCases.GraphQLTestCase APIViewTestCases.GraphQLTestCase
): ):
model = ImageAttachment model = ImageAttachment
brief_fields = ['display', 'id', 'image', 'name', 'url'] brief_fields = ['description', 'display', 'id', 'image', 'name', 'url']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -1040,6 +1040,11 @@ class ImageAttachmentListView(generic.ObjectListView):
actions = (BulkExport,) actions = (BulkExport,)
@register_model_view(ImageAttachment)
class ImageAttachmentView(generic.ObjectView):
queryset = ImageAttachment.objects.all()
@register_model_view(ImageAttachment, 'add', detail=False) @register_model_view(ImageAttachment, 'add', detail=False)
@register_model_view(ImageAttachment, 'edit') @register_model_view(ImageAttachment, 'edit')
class ImageAttachmentEditView(generic.ObjectEditView): class ImageAttachmentEditView(generic.ObjectEditView):
@ -1053,9 +1058,6 @@ class ImageAttachmentEditView(generic.ObjectEditView):
instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id')) instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
return instance return instance
def get_return_url(self, request, obj=None):
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
def get_extra_addanother_params(self, request): def get_extra_addanother_params(self, request):
return { return {
'object_type': request.GET.get('object_type'), 'object_type': request.GET.get('object_type'),
@ -1067,9 +1069,6 @@ class ImageAttachmentEditView(generic.ObjectEditView):
class ImageAttachmentDeleteView(generic.ObjectDeleteView): class ImageAttachmentDeleteView(generic.ObjectDeleteView):
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
def get_return_url(self, request, obj=None):
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
# #
# Journal entries # Journal entries

View File

@ -1,4 +1,67 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block tabs %} {% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Image Attachment" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Parent Object" %}</th>
<td>{{ object.parent|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "File" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Filename" %}</th>
<td>
<a href="{{ object.image.url }}" target="_blank">{{ object.filename }}</a>
<i class="mdi mdi-open-in-new"></i>
</td>
</tr>
<tr>
<th scope="row">{% trans "Dimensions" %}</th>
<td>{{ object.image_width }} × {{ object.image_height }}</td>
</tr>
<tr>
<th scope="row">{% trans "Size" %}</th>
<td>
<span title="{{ object.size }} {% trans "bytes" %}">{{ object.size|filesizeformat }}</span>
</td>
</tr>
</table>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<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>
</div>
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %} {% endblock %}