Commit c921f2ac by Clinton Blackburn

Merge pull request #12011 from edx/clintonb/course-api-image

Exposing absolute/CDN image URLs via Course API
parents 9543989e 00b48e54
......@@ -8,12 +8,14 @@ from django.core.urlresolvers import reverse
from rest_framework import serializers
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.lib.api.fields import AbsoluteURLField
class _MediaSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Nested serializer to represent a media object.
"""
def __init__(self, uri_attribute, *args, **kwargs):
super(_MediaSerializer, self).__init__(*args, **kwargs)
self.uri_attribute = uri_attribute
......@@ -27,12 +29,25 @@ class _MediaSerializer(serializers.Serializer): # pylint: disable=abstract-meth
return getattr(course_overview, self.uri_attribute)
class ImageSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Collection of URLs pointing to images of various sizes.
The URLs will be absolute URLs with the host set to the host of the current request. If the values to be
serialized are already absolute URLs, they will be unchanged.
"""
raw = AbsoluteURLField()
small = AbsoluteURLField()
large = AbsoluteURLField()
class _CourseApiMediaCollectionSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Nested serializer to represent a collection of media objects
"""
course_image = _MediaSerializer(source='*', uri_attribute='course_image_url')
course_video = _MediaSerializer(source='*', uri_attribute='course_video_url')
image = ImageSerializer(source='image_urls')
class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-method
......
......@@ -35,6 +35,8 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
self.honor_user = self.create_user('honor', is_staff=False)
self.request_factory = APIRequestFactory()
image_path = u'/c4x/edX/toy/asset/just_a_test.jpg'
image_url = u'http://testserver' + image_path
self.expected_data = {
'id': u'edX/toy/2012_Fall',
'name': u'Toy Course',
......@@ -43,10 +45,15 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
'short_description': u'A course about toys.',
'media': {
'course_image': {
'uri': u'/c4x/edX/toy/asset/just_a_test.jpg',
'uri': image_path,
},
'course_video': {
'uri': u'http://www.youtube.com/watch?v=test_youtube_id',
},
'image': {
'raw': image_url,
'small': image_url,
'large': image_url,
}
},
'start': u'2015-07-17T12:00:00Z',
......
"""Fields useful for edX API implementations."""
from rest_framework.serializers import Field
from rest_framework.serializers import Field, URLField
class ExpandableField(Field):
......@@ -9,6 +9,7 @@ class ExpandableField(Field):
collapsed_serializer (Serializer): the serializer to use for a non-expanded representation.
expanded_serializer (Serializer): the serializer to use for an expanded representation.
"""
def __init__(self, **kwargs):
"""Sets up the ExpandableField with the collapsed and expanded versions of the serializer."""
assert 'collapsed_serializer' in kwargs and 'expanded_serializer' in kwargs
......@@ -32,3 +33,24 @@ class ExpandableField(Field):
self.expanded.context["expand"] = set(field.context.get("expand", []))
return field.to_representation(obj)
class AbsoluteURLField(URLField):
"""
Field that serializes values to absolute URLs based on the current request.
If the value to be serialized is already a URL, that value will returned.
"""
def to_representation(self, value):
request = self.context.get('request', None)
assert request is not None, (
"`%s` requires the request in the serializer context. "
"Add `context={'request': request}` when instantiating the serializer." % self.__class__.__name__
)
if value.startswith(('http:', 'https:')):
return value
return request.build_absolute_uri(value)
""" Tests for custom DRF fields. """
import ddt
from django.test import TestCase
from openedx.core.lib.api.fields import AbsoluteURLField
class MockRequest(object):
""" Mock request object. """
ROOT = 'http://example.com'
def build_absolute_uri(self, value):
""" Mocks `Request.build_absolute_uri`. """
return self.ROOT + value
@ddt.ddt
class AbsoluteURLFieldTests(TestCase):
""" Tests for the AbsoluteURLField. """
def setUp(self):
super(AbsoluteURLFieldTests, self).setUp()
self.field = AbsoluteURLField()
self.field._context = {'request': MockRequest()} # pylint:disable=protected-access
def test_to_representation_without_request(self):
""" Verify an AssertionError is raised if no request is passed as context to the field. """
self.field._context = {} # pylint:disable=protected-access
self.assertRaises(AssertionError, self.field.to_representation, '/image.jpg')
@ddt.data(
'http://example.com',
'https://example.org'
)
def test_to_representation_with_absolute_url(self, value):
""" Verify the method returns the passed value, if the value is an absolute URL. """
self.assertEqual(self.field.to_representation(value), value)
def test_to_representation(self):
""" Verify the method returns an absolute URL. """
self.assertEqual(self.field.to_representation('/image.jpg'), MockRequest.ROOT + '/image.jpg')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment