From f48e1cb534065f694273c8b943d778decf1ba154 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Thu, 24 Jul 2025 14:16:02 -0500 Subject: [PATCH] Fixes: #19669 - Add an API endpoint to download image attachments --- netbox/extras/api/views.py | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 3f5bb172a..4f8dfaf35 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,5 +1,7 @@ +from django.conf import settings from django.http import Http404 from django.shortcuts import get_object_or_404 +from django.views.static import serve from django_rq.queues import get_connection from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import status @@ -32,6 +34,7 @@ class ExtrasRootView(APIRootView): """ Extras API root view """ + def get_view_name(self): return 'Extras' @@ -40,6 +43,7 @@ class ExtrasRootView(APIRootView): # EventRules # + class EventRuleViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = EventRule.objects.all() @@ -51,6 +55,7 @@ class EventRuleViewSet(NetBoxModelViewSet): # Webhooks # + class WebhookViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = Webhook.objects.all() @@ -62,6 +67,7 @@ class WebhookViewSet(NetBoxModelViewSet): # Custom fields # + class CustomFieldViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = CustomField.objects.select_related('choice_set') @@ -89,9 +95,7 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet): # Paginate data if page := self.paginate_queryset(choices): - data = [ - {'id': c[0], 'display': c[1]} for c in page - ] + data = [{'id': c[0], 'display': c[1]} for c in page] else: data = [] @@ -102,6 +106,7 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet): # Custom links # + class CustomLinkViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = CustomLink.objects.all() @@ -113,6 +118,7 @@ class CustomLinkViewSet(NetBoxModelViewSet): # Export templates # + class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = ExportTemplate.objects.all() @@ -124,6 +130,7 @@ class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet): # Saved filters # + class SavedFilterViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = SavedFilter.objects.all() @@ -135,6 +142,7 @@ class SavedFilterViewSet(NetBoxModelViewSet): # Table Configs # + class TableConfigViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = TableConfig.objects.all() @@ -146,6 +154,7 @@ class TableConfigViewSet(NetBoxModelViewSet): # Bookmarks # + class BookmarkViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = Bookmark.objects.all() @@ -157,6 +166,7 @@ class BookmarkViewSet(NetBoxModelViewSet): # Notifications & subscriptions # + class NotificationViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = Notification.objects.all() @@ -178,6 +188,7 @@ class SubscriptionViewSet(NetBoxModelViewSet): # Tags # + class TagViewSet(NetBoxModelViewSet): queryset = Tag.objects.all() serializer_class = serializers.TagSerializer @@ -194,17 +205,30 @@ class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet): # Image attachments # + class ImageAttachmentViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = ImageAttachment.objects.all() serializer_class = serializers.ImageAttachmentSerializer filterset_class = filtersets.ImageAttachmentFilterSet + @action( + methods=['GET'], + detail=True, + url_path='download', + url_name='download', + ) + def download(self, request, pk, *args, **kwargs): + obj = get_object_or_404(self.queryset, pk=pk) + # Render and return the elevation as an SVG drawing with the correct content type + return serve(request, obj.image.path, document_root=settings.MEDIA_ROOT) + # # Journal entries # + class JournalEntryViewSet(NetBoxModelViewSet): metadata_class = ContentTypeMetadata queryset = JournalEntry.objects.all() @@ -216,6 +240,7 @@ class JournalEntryViewSet(NetBoxModelViewSet): # Config contexts # + class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet): queryset = ConfigContext.objects.all() serializer_class = serializers.ConfigContextSerializer @@ -226,6 +251,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet): # Config templates # + class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet): queryset = ConfigTemplate.objects.all() serializer_class = serializers.ConfigTemplateSerializer @@ -247,6 +273,7 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo # Scripts # + @extend_schema_view( update=extend_schema(request=serializers.ScriptInputSerializer), partial_update=extend_schema(request=serializers.ScriptInputSerializer), @@ -287,10 +314,7 @@ class ScriptViewSet(ModelViewSet): raise PermissionDenied("This user does not have permission to run scripts.") script = self._get_script(pk) - input_serializer = serializers.ScriptInputSerializer( - data=request.data, - context={'script': script} - ) + input_serializer = serializers.ScriptInputSerializer(data=request.data, context={'script': script}) # Check that at least one RQ worker is running if not Worker.count(get_connection('default')): @@ -305,7 +329,7 @@ class ScriptViewSet(ModelViewSet): commit=input_serializer.data['commit'], job_timeout=script.python_class.job_timeout, schedule_at=input_serializer.validated_data.get('schedule_at'), - interval=input_serializer.validated_data.get('interval') + interval=input_serializer.validated_data.get('interval'), ) serializer = serializers.ScriptDetailSerializer(script, context={'request': request}) @@ -318,10 +342,12 @@ class ScriptViewSet(ModelViewSet): # Object types # + class ObjectTypeViewSet(ReadOnlyModelViewSet): """ Read-only list of ObjectTypes. """ + permission_classes = [IsAuthenticatedOrLoginNotRequired] queryset = ObjectType.objects.order_by('app_label', 'model') serializer_class = serializers.ObjectTypeSerializer @@ -332,6 +358,7 @@ class ObjectTypeViewSet(ReadOnlyModelViewSet): # User dashboard # + class DashboardView(RetrieveUpdateDestroyAPIView): queryset = Dashboard.objects.all() serializer_class = serializers.DashboardSerializer