diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py index fe0964eae..6507a12be 100644 --- a/netbox/extras/api/serializers_/attachments.py +++ b/netbox/extras/api/serializers_/attachments.py @@ -24,10 +24,10 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): class Meta: model = ImageAttachment 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', ] - brief_fields = ('id', 'url', 'display', 'name', 'image') + brief_fields = ('id', 'url', 'display', 'name', 'image', 'description') def validate(self, data): diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 6fa81f8d3..e77c66959 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -456,7 +456,10 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet): def search(self, queryset, name, value): if not value.strip(): return queryset - return queryset.filter(name__icontains=value) + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) class JournalEntryFilterSet(NetBoxModelFilterSet): diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 5590dfa1a..fd333322b 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -744,14 +744,17 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): class ImageAttachmentForm(forms.ModelForm): fieldsets = ( - FieldSet(ObjectAttribute('parent'), 'name', 'image'), + FieldSet(ObjectAttribute('parent'), 'image', 'name', 'description'), ) class Meta: model = ImageAttachment fields = [ - 'name', 'image', + 'image', 'name', 'description', ] + help_texts = { + 'name': _("If no name is specified, the file name will be used.") + } class JournalEntryForm(NetBoxModelForm): diff --git a/netbox/extras/migrations/0130_imageattachment_description.py b/netbox/extras/migrations/0130_imageattachment_description.py new file mode 100644 index 000000000..a4de0c4e3 --- /dev/null +++ b/netbox/extras/migrations/0130_imageattachment_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-07-17 18:00 + +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), + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index aa5af892f..2fdc1ffe3 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -1,4 +1,5 @@ import json +import os import urllib.parse from django.conf import settings @@ -678,6 +679,11 @@ class ImageAttachment(ChangeLoggedModel): max_length=50, blank=True ) + description = models.CharField( + verbose_name=_('description'), + max_length=200, + blank=True + ) objects = RestrictedQuerySet.as_manager() @@ -692,10 +698,10 @@ class ImageAttachment(ChangeLoggedModel): verbose_name_plural = _('image attachments') def __str__(self): - if self.name: - return self.name - filename = self.image.name.rsplit('/', 1)[-1] - return filename.split('_', 2)[2] + return self.name or self.filename + + def get_absolute_url(self): + return reverse('extras:imageattachment', args=[self.pk]) def clean(self): super().clean() @@ -719,6 +725,10 @@ class ImageAttachment(ChangeLoggedModel): # before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.) self.image.name = _name + @property + def filename(self): + return os.path.basename(self.image.name).split('_', 2)[2] + @property def size(self): """ diff --git a/netbox/extras/search.py b/netbox/extras/search.py index feb235c29..bc6381512 100644 --- a/netbox/extras/search.py +++ b/netbox/extras/search.py @@ -14,6 +14,16 @@ class CustomFieldIndex(SearchIndex): display_attrs = ('description',) +@register_search +class ImageAttachmentIndex(SearchIndex): + model = models.ImageAttachment + fields = ( + ('name', 100), + ('description', 500), + ) + display_attrs = ('description',) + + @register_search class JournalEntryIndex(SearchIndex): model = models.JournalEntry diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index e6f488fde..1c512f408 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -249,10 +249,10 @@ class ImageAttachmentTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ImageAttachment fields = ( - 'pk', 'object_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', - 'last_updated', + 'pk', 'object_type', 'parent', 'image', 'name', 'description', 'image_height', 'image_width', 'size', + '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): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 43172139c..a2664a2c2 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1040,6 +1040,11 @@ class ImageAttachmentListView(generic.ObjectListView): 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, 'edit') 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')) 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): return { 'object_type': request.GET.get('object_type'), @@ -1067,9 +1069,6 @@ class ImageAttachmentEditView(generic.ObjectEditView): class ImageAttachmentDeleteView(generic.ObjectDeleteView): 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 diff --git a/netbox/templates/extras/imageattachment.html b/netbox/templates/extras/imageattachment.html index 1968344cc..9d0a4e479 100644 --- a/netbox/templates/extras/imageattachment.html +++ b/netbox/templates/extras/imageattachment.html @@ -1,4 +1,67 @@ {% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} +{% load i18n %} -{% block tabs %} +{% block content %} +
+
+
+

{% trans "Image Attachment" %}

+ + + + + + + + + + + + + +
{% trans "Parent Object" %}{{ object.parent|linkify }}
{% trans "Name" %}{{ object.name|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
+
+ {% plugin_left_page object %} +
+
+
+

{% trans "File" %}

+ + + + + + + + + + + + + +
{% trans "Filename" %} + {{ object.filename }} + +
{% trans "Dimensions" %}{{ object.image_width }} × {{ object.image_height }}
{% trans "Size" %} + {{ object.size|filesizeformat }} +
+
+ {% plugin_right_page object %} +
+
+
+
+
+

{% trans "Image" %}

+
+ + {{ object }} + +
+
+ {% plugin_full_width_page object %} +
+
{% endblock %}