Fixes: #19669 - Add an API endpoint to download image attachments

This commit is contained in:
Daniel Sheppard 2025-07-24 14:16:02 -05:00
parent ffa9a52667
commit f48e1cb534

View File

@ -1,5 +1,7 @@
from django.conf import settings
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.static import serve
from django_rq.queues import get_connection from django_rq.queues import get_connection
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status from rest_framework import status
@ -32,6 +34,7 @@ class ExtrasRootView(APIRootView):
""" """
Extras API root view Extras API root view
""" """
def get_view_name(self): def get_view_name(self):
return 'Extras' return 'Extras'
@ -40,6 +43,7 @@ class ExtrasRootView(APIRootView):
# EventRules # EventRules
# #
class EventRuleViewSet(NetBoxModelViewSet): class EventRuleViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = EventRule.objects.all() queryset = EventRule.objects.all()
@ -51,6 +55,7 @@ class EventRuleViewSet(NetBoxModelViewSet):
# Webhooks # Webhooks
# #
class WebhookViewSet(NetBoxModelViewSet): class WebhookViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = Webhook.objects.all() queryset = Webhook.objects.all()
@ -62,6 +67,7 @@ class WebhookViewSet(NetBoxModelViewSet):
# Custom fields # Custom fields
# #
class CustomFieldViewSet(NetBoxModelViewSet): class CustomFieldViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = CustomField.objects.select_related('choice_set') queryset = CustomField.objects.select_related('choice_set')
@ -89,9 +95,7 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
# Paginate data # Paginate data
if page := self.paginate_queryset(choices): if page := self.paginate_queryset(choices):
data = [ data = [{'id': c[0], 'display': c[1]} for c in page]
{'id': c[0], 'display': c[1]} for c in page
]
else: else:
data = [] data = []
@ -102,6 +106,7 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
# Custom links # Custom links
# #
class CustomLinkViewSet(NetBoxModelViewSet): class CustomLinkViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = CustomLink.objects.all() queryset = CustomLink.objects.all()
@ -113,6 +118,7 @@ class CustomLinkViewSet(NetBoxModelViewSet):
# Export templates # Export templates
# #
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet): class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = ExportTemplate.objects.all() queryset = ExportTemplate.objects.all()
@ -124,6 +130,7 @@ class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
# Saved filters # Saved filters
# #
class SavedFilterViewSet(NetBoxModelViewSet): class SavedFilterViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = SavedFilter.objects.all() queryset = SavedFilter.objects.all()
@ -135,6 +142,7 @@ class SavedFilterViewSet(NetBoxModelViewSet):
# Table Configs # Table Configs
# #
class TableConfigViewSet(NetBoxModelViewSet): class TableConfigViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = TableConfig.objects.all() queryset = TableConfig.objects.all()
@ -146,6 +154,7 @@ class TableConfigViewSet(NetBoxModelViewSet):
# Bookmarks # Bookmarks
# #
class BookmarkViewSet(NetBoxModelViewSet): class BookmarkViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = Bookmark.objects.all() queryset = Bookmark.objects.all()
@ -157,6 +166,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
# Notifications & subscriptions # Notifications & subscriptions
# #
class NotificationViewSet(NetBoxModelViewSet): class NotificationViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = Notification.objects.all() queryset = Notification.objects.all()
@ -178,6 +188,7 @@ class SubscriptionViewSet(NetBoxModelViewSet):
# Tags # Tags
# #
class TagViewSet(NetBoxModelViewSet): class TagViewSet(NetBoxModelViewSet):
queryset = Tag.objects.all() queryset = Tag.objects.all()
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer
@ -194,17 +205,30 @@ class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet):
# Image attachments # Image attachments
# #
class ImageAttachmentViewSet(NetBoxModelViewSet): class ImageAttachmentViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
serializer_class = serializers.ImageAttachmentSerializer serializer_class = serializers.ImageAttachmentSerializer
filterset_class = filtersets.ImageAttachmentFilterSet 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 # Journal entries
# #
class JournalEntryViewSet(NetBoxModelViewSet): class JournalEntryViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = JournalEntry.objects.all() queryset = JournalEntry.objects.all()
@ -216,6 +240,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
# Config contexts # Config contexts
# #
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet): class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
serializer_class = serializers.ConfigContextSerializer serializer_class = serializers.ConfigContextSerializer
@ -226,6 +251,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
# Config templates # Config templates
# #
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet): class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
queryset = ConfigTemplate.objects.all() queryset = ConfigTemplate.objects.all()
serializer_class = serializers.ConfigTemplateSerializer serializer_class = serializers.ConfigTemplateSerializer
@ -247,6 +273,7 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
# Scripts # Scripts
# #
@extend_schema_view( @extend_schema_view(
update=extend_schema(request=serializers.ScriptInputSerializer), update=extend_schema(request=serializers.ScriptInputSerializer),
partial_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.") raise PermissionDenied("This user does not have permission to run scripts.")
script = self._get_script(pk) script = self._get_script(pk)
input_serializer = serializers.ScriptInputSerializer( input_serializer = serializers.ScriptInputSerializer(data=request.data, context={'script': script})
data=request.data,
context={'script': script}
)
# Check that at least one RQ worker is running # Check that at least one RQ worker is running
if not Worker.count(get_connection('default')): if not Worker.count(get_connection('default')):
@ -305,7 +329,7 @@ class ScriptViewSet(ModelViewSet):
commit=input_serializer.data['commit'], commit=input_serializer.data['commit'],
job_timeout=script.python_class.job_timeout, job_timeout=script.python_class.job_timeout,
schedule_at=input_serializer.validated_data.get('schedule_at'), 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}) serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
@ -318,10 +342,12 @@ class ScriptViewSet(ModelViewSet):
# Object types # Object types
# #
class ObjectTypeViewSet(ReadOnlyModelViewSet): class ObjectTypeViewSet(ReadOnlyModelViewSet):
""" """
Read-only list of ObjectTypes. Read-only list of ObjectTypes.
""" """
permission_classes = [IsAuthenticatedOrLoginNotRequired] permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = ObjectType.objects.order_by('app_label', 'model') queryset = ObjectType.objects.order_by('app_label', 'model')
serializer_class = serializers.ObjectTypeSerializer serializer_class = serializers.ObjectTypeSerializer
@ -332,6 +358,7 @@ class ObjectTypeViewSet(ReadOnlyModelViewSet):
# User dashboard # User dashboard
# #
class DashboardView(RetrieveUpdateDestroyAPIView): class DashboardView(RetrieveUpdateDestroyAPIView):
queryset = Dashboard.objects.all() queryset = Dashboard.objects.all()
serializer_class = serializers.DashboardSerializer serializer_class = serializers.DashboardSerializer