Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimise Library page load time when channels have large Thumbnail #12530

Merged
30 changes: 29 additions & 1 deletion kolibri/core/content/api.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -291,6 +295,18 @@ def filter_available(self, queryset, name, value):
return queryset.filter(root__available=value)


class ChannelThumbnailView(View):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For writing unit test for this is there any existing setUp method that will set up a channel metadata model?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look in the test_content_app.py file in the content app, you should see some examples.

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
Expand Down Expand Up @@ -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,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you would add here:

field_map = {
    "thumbnail": _create_channel_thumbnail_url,
}

field_map.update(BaseChannelMetadataMixin.field_map)

Then in the module scope you can define this function:

def _create_channel_thumbnail_url(item):
    return reverse("kolibri:core:channel-thumbnail", args=[item["id"]])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this works, can you share django/python docs covering field_map?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a Django thing - so the documentation for it is in this code comment: https://github.com/learningequality/kolibri/blob/develop/kolibri/core/api.py#L139

field_map.update(BaseChannelMetadataMixin.field_map)


MODALITIES = set(["QUIZ"])
Expand Down
11 changes: 10 additions & 1 deletion kolibri/core/content/api_urls.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -46,4 +48,11 @@
)
router.register(r"remotechannel", RemoteChannelViewSet, basename="remotechannel")

urlpatterns = [re_path(r"^", include(router.urls))]
urlpatterns = [
path(
"channel-thumbnail/<channel_id>/",
ChannelThumbnailView.as_view(),
name="channel-thumbnail",
),
re_path(r"^", include(router.urls)),
]
41 changes: 41 additions & 0 deletions kolibri/core/content/test/test_content_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time
import unittest
import uuid
from base64 import urlsafe_b64decode

import mock
import requests
Expand Down Expand Up @@ -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)