diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index b107555c4ba..e8cf4c70f64 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -1,6 +1,7 @@ import hashlib import logging import re +from base64 import urlsafe_b64decode from collections import OrderedDict from functools import reduce from random import sample @@ -14,11 +15,14 @@ from django.db.models import Subquery from django.db.models.aggregates import Count from django.http import Http404 +from django.http import HttpResponse +from django.urls import reverse from django.utils.cache import add_never_cache_headers from django.utils.decorators import method_decorator from django.utils.encoding import force_bytes from django.utils.encoding import iri_to_uri from django.utils.translation import gettext as _ +from django.views import View from django.views.decorators.cache import cache_page from django.views.decorators.cache import never_cache from django.views.decorators.http import etag @@ -291,6 +295,18 @@ def filter_available(self, queryset, name, value): return queryset.filter(root__available=value) +class ChannelThumbnailView(View): + def get(self, request, channel_id): + channel = get_object_or_404(models.ChannelMetadata, id=channel_id) + try: + header, b_64_thumbnail = channel.thumbnail.split(",", 1) + mimetype = header.split(":")[1].split(";")[0] + except ValueError: + raise Http404("No thumbnail available") + thumbnail = urlsafe_b64decode(b_64_thumbnail) + return HttpResponse(thumbnail, content_type=mimetype) + + class BaseChannelMetadataMixin(object): filter_backends = (DjangoFilterBackend,) filterset_class = ChannelMetadataFilter @@ -373,9 +389,21 @@ def filter_options(self, request, **kwargs): return Response(data) +def _create_channel_thumbnail_url(item): + return ( + reverse("kolibri:core:channel-thumbnail", args=[item["id"]]) + if item["thumbnail"] + else "" + ) + + @method_decorator(remote_metadata_cache, name="dispatch") class ChannelMetadataViewSet(BaseChannelMetadataMixin, RemoteViewSet): - pass + field_map = { + "thumbnail": _create_channel_thumbnail_url, + } + + field_map.update(BaseChannelMetadataMixin.field_map) MODALITIES = set(["QUIZ"]) diff --git a/kolibri/core/content/api_urls.py b/kolibri/core/content/api_urls.py index 5aaa1b095ac..0828fde7a75 100644 --- a/kolibri/core/content/api_urls.py +++ b/kolibri/core/content/api_urls.py @@ -1,8 +1,10 @@ from django.urls import include +from django.urls import path from django.urls import re_path from rest_framework import routers from .api import ChannelMetadataViewSet +from .api import ChannelThumbnailView from .api import ContentNodeBookmarksViewset from .api import ContentNodeGranularViewset from .api import ContentNodeProgressViewset @@ -46,4 +48,11 @@ ) router.register(r"remotechannel", RemoteChannelViewSet, basename="remotechannel") -urlpatterns = [re_path(r"^", include(router.urls))] +urlpatterns = [ + path( + "channel-thumbnail//", + ChannelThumbnailView.as_view(), + name="channel-thumbnail", + ), + re_path(r"^", include(router.urls)), +] diff --git a/kolibri/core/content/test/test_content_app.py b/kolibri/core/content/test/test_content_app.py index 494d9407278..7679d94fa0c 100644 --- a/kolibri/core/content/test/test_content_app.py +++ b/kolibri/core/content/test/test_content_app.py @@ -5,6 +5,7 @@ import time import unittest import uuid +from base64 import urlsafe_b64decode import mock import requests @@ -1998,3 +1999,43 @@ class PrefixedProxyContentMetadataTestCase(ProxyContentMetadataTestCase): @property def baseurl(self): return self.live_server_url + "/test/" + + +class ChannelThumbnailViewTestCase(APITestCase): + def setUp(self): + self.content_node = content.ContentNode.objects.create( + pk="6a406ac66b224106aa2e93f73a94333d", + channel_id="f8ec4a5d14cd4716890999da596032d2", + content_id="ded4a083e75f4689b386fd2b706e792a", + ) + self.thumbnail = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABjElEQVR42mNk" + self.channel_metadata = content.ChannelMetadata.objects.create( + id="63acff41781543828861ade41dbdd7ff", + name="no exercise channel metadata", + thumbnail=self.thumbnail, + root=self.content_node, + ) + + def test_channel_thumbnail_view(self): + response = self.client.get( + reverse("kolibri:core:channel-thumbnail", args=[self.channel_metadata.id]) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "image/png") + self.assertEqual( + response.content, urlsafe_b64decode(self.thumbnail.split(",")[1]) + ) + + def test_channel_thumbnail_view_not_found(self): + response = self.client.get( + reverse("kolibri:core:channel-thumbnail", args=["deadpool"]) + ) + self.assertEqual(response.status_code, 404) + + def test_channel_thumbnail_view_no_thumbnail(self): + self.channel_metadata.thumbnail = "" + self.channel_metadata.save() + response = self.client.get( + reverse("kolibri:core:channel-thumbnail", args=[self.channel_metadata.id]) + ) + self.assertEqual(response.status_code, 404)