Coverage for api/views/image_views.py: 85%
57 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 06:14 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 06:14 +0000
1import io
3from django.conf import settings
4from django.shortcuts import aget_object_or_404
5from rest_framework.decorators import action
6from rest_framework.response import Response
8from drf_spectacular.utils import extend_schema, extend_schema_view
9from PIL import Image as PILImage
11from api.constants.media_types import IMAGE_TYPE
12from api.docs.image_docs import (
13 detail,
14 oembed,
15 related,
16 report,
17 search,
18 stats,
19)
20from api.docs.image_docs import thumbnail as thumbnail_docs
21from api.models import Image
22from api.serializers.image_serializers import (
23 ImageReportRequestSerializer,
24 ImageSearchRequestSerializer,
25 ImageSerializer,
26 OembedRequestSerializer,
27 OembedSerializer,
28)
29from api.utils import image_proxy
30from api.utils.aiohttp import get_aiohttp_session
31from api.views.media_views import MediaViewSet
34@extend_schema(tags=["images"])
35@extend_schema_view(
36 list=search,
37 stats=stats,
38 retrieve=detail,
39 related=related,
40)
41class ImageViewSet(MediaViewSet):
42 """Viewset for all endpoints pertaining to images."""
44 model_class = Image
45 media_type = IMAGE_TYPE
46 query_serializer_class = ImageSearchRequestSerializer
47 default_index = settings.MEDIA_INDEX_MAPPING[IMAGE_TYPE]
49 serializer_class = ImageSerializer
51 OEMBED_HEADERS = {
52 "User-Agent": settings.OUTBOUND_USER_AGENT_TEMPLATE.format(purpose="OEmbed"),
53 }
55 def get_queryset(self):
56 return super().get_queryset().select_related("sensitive_image")
58 # Extra actions
60 @oembed
61 @action(
62 detail=False,
63 url_path="oembed",
64 url_name="oembed",
65 serializer_class=OembedSerializer,
66 )
67 async def oembed(self, request, *_, **__):
68 """
69 Retrieve the structured data for a specified image URL as per the
70 [oEmbed spec](https://oembed.com/).
72 This info can be used to embed the image on the consumer's website. Only
73 JSON format is supported.
74 """
76 params = OembedRequestSerializer(data=request.query_params)
77 params.is_valid(raise_exception=True)
78 identifier = params.validated_data["identifier"]
79 context = self.get_serializer_context()
81 image = await aget_object_or_404(Image, identifier=identifier)
83 if not (image.height and image.width): 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 session = await get_aiohttp_session()
86 async with session.get(
87 image.url, headers=self.OEMBED_HEADERS
88 ) as image_file:
89 image_content = await image_file.content.read()
91 with PILImage.open(io.BytesIO(image_content)) as image_file:
92 width, height = image_file.size
94 context |= {
95 "width": width,
96 "height": height,
97 }
99 serializer = self.get_serializer(image, context=context)
100 return Response(data=await serializer.adata)
102 async def get_image_proxy_media_info(self) -> image_proxy.MediaInfo:
103 image = await self.aget_object()
104 image_url = image.url
105 # Hotfix to use thumbnails for SMK images
106 # TODO: Remove when small thumbnail issues are resolved
107 if "iip.smk.dk" in image_url and image.thumbnail: 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 image_url = image.thumbnail
110 return image_proxy.MediaInfo(
111 media_identifier=image.identifier,
112 media_provider=image.provider,
113 image_url=image_url,
114 width=image.width,
115 )
117 @thumbnail_docs
118 @MediaViewSet.thumbnail_action
119 async def thumbnail(self, request, identifier):
120 """Retrieve the scaled down and compressed thumbnail of the image."""
121 return await super().thumbnail(request)
123 @report
124 @action(
125 detail=True,
126 methods=["post"],
127 serializer_class=ImageReportRequestSerializer,
128 )
129 def report(self, request, identifier):
130 """
131 Report an issue about a specified image to Openverse.
133 By using this endpoint, you can report an image if it infringes
134 copyright, contains mature or sensitive content or some other reason.
135 """
137 return super().report(request, identifier)