Commit 025ad8df by Dave St.Germain

Merge pull request #11 from edx/dcs/subtitle-support

Added subtitle model and API
parents df4728fc 7ab8ff9b
...@@ -3,9 +3,9 @@ Admin file for django app edxval. ...@@ -3,9 +3,9 @@ Admin file for django app edxval.
""" """
from django.contrib import admin from django.contrib import admin
from .models import Video, Profile, EncodedVideo from .models import Video, Profile, EncodedVideo, Subtitle
admin.site.register(Video) admin.site.register(Video)
admin.site.register(Profile) admin.site.register(Profile)
admin.site.register(EncodedVideo) admin.site.register(EncodedVideo)
admin.site.register(Subtitle)
...@@ -139,6 +139,11 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613 ...@@ -139,6 +139,11 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
extension: 3 letter extension of video extension: 3 letter extension of video
width: horizontal pixel resolution width: horizontal pixel resolution
height: vertical pixel resolution height: vertical pixel resolution
subtitles: a list of Subtitle dicts
fmt: file format (SRT or SJSON)
language: language code
content_url: url of file
url: api url to subtitle
} }
Raises: Raises:
...@@ -174,4 +179,4 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613 ...@@ -174,4 +179,4 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
error_message = u"Could not get edx_video_id: {0}".format(edx_video_id) error_message = u"Could not get edx_video_id: {0}".format(edx_video_id)
logger.exception(error_message) logger.exception(error_message)
raise ValInternalError(error_message) raise ValInternalError(error_message)
return result.data # pylint: disable=E1101 return result.data # pylint: disable=E1101
...@@ -29,6 +29,7 @@ invalid profile_name will be returned. ...@@ -29,6 +29,7 @@ invalid profile_name will be returned.
from django.db import models from django.db import models
from django.core.validators import MinValueValidator, RegexValidator from django.core.validators import MinValueValidator, RegexValidator
from django.core.urlresolvers import reverse
url_regex = r'^[a-zA-Z0-9\-]*$' url_regex = r'^[a-zA-Z0-9\-]*$'
...@@ -78,6 +79,9 @@ class Video(models.Model): ...@@ -78,6 +79,9 @@ class Video(models.Model):
client_video_id = models.CharField(max_length=255, db_index=True) client_video_id = models.CharField(max_length=255, db_index=True)
duration = models.FloatField(validators=[MinValueValidator(0)]) duration = models.FloatField(validators=[MinValueValidator(0)])
def __str__(self):
return self.edx_video_id
class CourseVideos(models.Model): class CourseVideos(models.Model):
""" """
...@@ -89,7 +93,7 @@ class CourseVideos(models.Model): ...@@ -89,7 +93,7 @@ class CourseVideos(models.Model):
course_id = models.CharField(max_length=255) course_id = models.CharField(max_length=255)
video = models.ForeignKey(Video) video = models.ForeignKey(Video)
class Meta: class Meta: # pylint: disable=C1001
""" """
course_id is listed first in this composite index course_id is listed first in this composite index
""" """
...@@ -108,3 +112,34 @@ class EncodedVideo(models.Model): ...@@ -108,3 +112,34 @@ class EncodedVideo(models.Model):
profile = models.ForeignKey(Profile, related_name="+") profile = models.ForeignKey(Profile, related_name="+")
video = models.ForeignKey(Video, related_name="encoded_videos") video = models.ForeignKey(Video, related_name="encoded_videos")
SUBTITLE_FORMATS = (
('srt', 'SubRip'),
('sjson', 'SRT JSON')
)
class Subtitle(models.Model):
"""
Subtitle for video
"""
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
video = models.ForeignKey(Video, related_name="subtitles")
fmt = models.CharField(max_length=20, db_index=True, choices=SUBTITLE_FORMATS)
language = models.CharField(max_length=8, db_index=True)
content = models.TextField(default='')
def __str__(self):
return '%s Subtitle for %s' % (self.language, self.video)
def get_absolute_url(self):
return reverse('subtitle-content', args=[self.video.edx_video_id, self.language])
@property
def content_type(self):
if self.fmt == 'sjson':
return 'application/json'
else:
return 'text/plain'
...@@ -7,7 +7,7 @@ EncodedVideoSerializer which uses the profile_name as it's profile field. ...@@ -7,7 +7,7 @@ EncodedVideoSerializer which uses the profile_name as it's profile field.
from rest_framework import serializers from rest_framework import serializers
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from edxval.models import Profile, Video, EncodedVideo from edxval.models import Profile, Video, EncodedVideo, Subtitle
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
...@@ -51,6 +51,39 @@ class EncodedVideoSerializer(serializers.ModelSerializer): ...@@ -51,6 +51,39 @@ class EncodedVideoSerializer(serializers.ModelSerializer):
return data.get('profile', None) return data.get('profile', None)
class SubtitleSerializer(serializers.ModelSerializer):
"""
Serializer for Subtitle objects
"""
content_url = serializers.CharField(source='get_absolute_url', read_only=True)
content = serializers.CharField(write_only=True)
def validate_content(self, attrs, source):
"""
Validate that the subtitle is in the correct format
"""
value = attrs[source]
if attrs.get('fmt') == 'sjson':
import json
try:
loaded = json.loads(value)
except ValueError:
raise serializers.ValidationError("Not in JSON format")
else:
attrs[source] = json.dumps(loaded)
return attrs
class Meta: # pylint: disable=C1001, C0111
model = Subtitle
lookup_field = "id"
fields = (
"fmt",
"language",
"content_url",
"content",
)
class VideoSerializer(serializers.HyperlinkedModelSerializer): class VideoSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for Video object Serializer for Video object
...@@ -58,6 +91,7 @@ class VideoSerializer(serializers.HyperlinkedModelSerializer): ...@@ -58,6 +91,7 @@ class VideoSerializer(serializers.HyperlinkedModelSerializer):
encoded_videos takes a list of dicts EncodedVideo data. encoded_videos takes a list of dicts EncodedVideo data.
""" """
encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True) encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True)
subtitles = SubtitleSerializer(many=True, allow_add_remove=True, required=False)
class Meta: # pylint: disable=C0111 class Meta: # pylint: disable=C0111
model = Video model = Video
......
...@@ -39,7 +39,8 @@ VIDEO_DICT_NEGATIVE_DURATION = dict( ...@@ -39,7 +39,8 @@ VIDEO_DICT_NEGATIVE_DURATION = dict(
client_video_id="Thunder Cats S01E01", client_video_id="Thunder Cats S01E01",
duration=-111, duration=-111,
edx_video_id="thisis12char-thisis7", edx_video_id="thisis12char-thisis7",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
VIDEO_DICT_BEE_INVALID = dict( VIDEO_DICT_BEE_INVALID = dict(
client_video_id="Barking Bee", client_video_id="Barking Bee",
...@@ -50,7 +51,8 @@ VIDEO_DICT_INVALID_ID = dict( ...@@ -50,7 +51,8 @@ VIDEO_DICT_INVALID_ID = dict(
client_video_id="SuperSloth", client_video_id="SuperSloth",
duration=42, duration=42,
edx_video_id="sloppy/sloth!!", edx_video_id="sloppy/sloth!!",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict( ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com", url="http://www.meowmix.com",
...@@ -69,13 +71,15 @@ VIDEO_DICT_NON_LATIN_TITLE = dict( ...@@ -69,13 +71,15 @@ VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id=u"배고픈 햄스터", client_video_id=u"배고픈 햄스터",
duration=42, duration=42,
edx_video_id="ID", edx_video_id="ID",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
VIDEO_DICT_NON_LATIN_ID = dict( VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster", client_video_id="Hungry Hamster",
duration=42, duration=42,
edx_video_id="밥줘", edx_video_id="밥줘",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
PROFILE_DICT_NON_LATIN = dict( PROFILE_DICT_NON_LATIN = dict(
profile_name=u"배고파", profile_name=u"배고파",
...@@ -112,6 +116,19 @@ PROFILE_DICT_MANY_INVALID = dict( ...@@ -112,6 +116,19 @@ PROFILE_DICT_MANY_INVALID = dict(
height="lol", height="lol",
) )
""" """
Subtitles
"""
SUBTITLE_DICT_SRT = dict(
fmt="srt",
language="en",
content="0:0:0\nhello"
)
SUBTITLE_DICT_SJSON = dict(
fmt="sjson",
language="fr",
content='{"start": "00:00:00"}'
)
"""
Fish Fish
""" """
VIDEO_DICT_FISH = dict( VIDEO_DICT_FISH = dict(
...@@ -159,6 +176,7 @@ COMPLETE_SET_FISH = dict( ...@@ -159,6 +176,7 @@ COMPLETE_SET_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE, ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP ENCODED_VIDEO_DICT_FISH_DESKTOP
], ],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_FISH **VIDEO_DICT_FISH
) )
COMPLETE_SET_TWO_MOBILE_FISH = dict( COMPLETE_SET_TWO_MOBILE_FISH = dict(
...@@ -166,6 +184,7 @@ COMPLETE_SET_TWO_MOBILE_FISH = dict( ...@@ -166,6 +184,7 @@ COMPLETE_SET_TWO_MOBILE_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE, ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_MOBILE ENCODED_VIDEO_DICT_FISH_MOBILE
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH **VIDEO_DICT_FISH
) )
COMPLETE_SET_UPDATE_FISH = dict( COMPLETE_SET_UPDATE_FISH = dict(
...@@ -173,6 +192,7 @@ COMPLETE_SET_UPDATE_FISH = dict( ...@@ -173,6 +192,7 @@ COMPLETE_SET_UPDATE_FISH = dict(
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE, ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH **VIDEO_DICT_FISH
) )
COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict( COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict(
...@@ -180,6 +200,7 @@ COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict( ...@@ -180,6 +200,7 @@ COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict(
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE, ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
], ],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_DIFFERENT_ID_FISH **VIDEO_DICT_DIFFERENT_ID_FISH
) )
COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict( COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict(
...@@ -187,12 +208,14 @@ COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict( ...@@ -187,12 +208,14 @@ COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict(
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE, ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP ENCODED_VIDEO_DICT_FISH_DESKTOP
], ],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_FISH **VIDEO_DICT_FISH
) )
COMPLETE_SET_UPDATE_ONLY_DESKTOP_FISH = dict( COMPLETE_SET_UPDATE_ONLY_DESKTOP_FISH = dict(
encoded_videos=[ encoded_videos=[
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH **VIDEO_DICT_FISH
) )
COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict( COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
...@@ -200,6 +223,7 @@ COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict( ...@@ -200,6 +223,7 @@ COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE, ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_INVALID_PROFILE ENCODED_VIDEO_DICT_FISH_INVALID_PROFILE
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH **VIDEO_DICT_FISH
) )
COMPLETE_SET_INVALID_VIDEO_FISH = dict( COMPLETE_SET_INVALID_VIDEO_FISH = dict(
...@@ -209,7 +233,8 @@ COMPLETE_SET_INVALID_VIDEO_FISH = dict( ...@@ -209,7 +233,8 @@ COMPLETE_SET_INVALID_VIDEO_FISH = dict(
encoded_videos=[ encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE, ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP ENCODED_VIDEO_DICT_FISH_DESKTOP
] ],
subtitles=[SUBTITLE_DICT_SRT]
) )
COMPLETE_SETS_ALL_INVALID = [ COMPLETE_SETS_ALL_INVALID = [
...@@ -240,12 +265,14 @@ COMPLETE_SET_STAR = dict( ...@@ -240,12 +265,14 @@ COMPLETE_SET_STAR = dict(
encoded_videos=[ encoded_videos=[
ENCODED_VIDEO_DICT_STAR ENCODED_VIDEO_DICT_STAR
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR **VIDEO_DICT_STAR
) )
COMPLETE_SET_UPDATE_STAR = dict( COMPLETE_SET_UPDATE_STAR = dict(
encoded_videos=[ encoded_videos=[
ENCODED_VIDEO_UPDATE_DICT_STAR ENCODED_VIDEO_UPDATE_DICT_STAR
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR **VIDEO_DICT_STAR
) )
COMPLETE_SET_NOT_A_LIST = dict( COMPLETE_SET_NOT_A_LIST = dict(
...@@ -255,6 +282,7 @@ COMPLETE_SET_NOT_A_LIST = dict( ...@@ -255,6 +282,7 @@ COMPLETE_SET_NOT_A_LIST = dict(
bitrate=42, bitrate=42,
profile=1 profile=1
), ),
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR **VIDEO_DICT_STAR
) )
COMPLETE_SET_EXTRA_VIDEO_FIELD = dict( COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
...@@ -267,6 +295,7 @@ COMPLETE_SET_EXTRA_VIDEO_FIELD = dict( ...@@ -267,6 +295,7 @@ COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
video="This should be overridden by parent video field" video="This should be overridden by parent video field"
) )
], ],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR **VIDEO_DICT_STAR
) )
""" """
...@@ -276,17 +305,20 @@ VIDEO_DICT_ZEBRA = dict( ...@@ -276,17 +305,20 @@ VIDEO_DICT_ZEBRA = dict(
client_video_id="Zesty Zebra", client_video_id="Zesty Zebra",
duration=111.00, duration=111.00,
edx_video_id="zestttt", edx_video_id="zestttt",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
VIDEO_DICT_ANIMAL = dict( VIDEO_DICT_ANIMAL = dict(
client_video_id="Average Animal", client_video_id="Average Animal",
duration=111.00, duration=111.00,
edx_video_id="mediocrity", edx_video_id="mediocrity",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
VIDEO_DICT_UPDATE_ANIMAL = dict( VIDEO_DICT_UPDATE_ANIMAL = dict(
client_video_id="Above Average Animal", client_video_id="Above Average Animal",
duration=999.00, duration=999.00,
edx_video_id="mediocrity", edx_video_id="mediocrity",
encoded_videos=[] encoded_videos=[],
subtitles=[]
) )
...@@ -225,7 +225,7 @@ class GetVideoInfoTestWithHttpCalls(APITestCase): ...@@ -225,7 +225,7 @@ class GetVideoInfoTestWithHttpCalls(APITestCase):
""" """
Tests number of queries for a Video/EncodedVideo(1) pair Tests number of queries for a Video/EncodedVideo(1) pair
""" """
with self.assertNumQueries(4): with self.assertNumQueries(7):
api.get_video_info(constants.COMPLETE_SET_FISH.get("edx_video_id")) api.get_video_info(constants.COMPLETE_SET_FISH.get("edx_video_id"))
def test_get_info_queries_for_one_encoded_video(self): def test_get_info_queries_for_one_encoded_video(self):
...@@ -237,7 +237,7 @@ class GetVideoInfoTestWithHttpCalls(APITestCase): ...@@ -237,7 +237,7 @@ class GetVideoInfoTestWithHttpCalls(APITestCase):
url, constants.COMPLETE_SET_STAR, format='json' url, constants.COMPLETE_SET_STAR, format='json'
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(3): with self.assertNumQueries(5):
api.get_video_info(constants.COMPLETE_SET_STAR.get("edx_video_id")) api.get_video_info(constants.COMPLETE_SET_STAR.get("edx_video_id"))
def test_get_info_queries_for_only_video(self): def test_get_info_queries_for_only_video(self):
...@@ -249,6 +249,6 @@ class GetVideoInfoTestWithHttpCalls(APITestCase): ...@@ -249,6 +249,6 @@ class GetVideoInfoTestWithHttpCalls(APITestCase):
url, constants.VIDEO_DICT_ZEBRA, format='json' url, constants.VIDEO_DICT_ZEBRA, format='json'
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(2): with self.assertNumQueries(3):
api.get_video_info(constants.VIDEO_DICT_ZEBRA.get("edx_video_id")) api.get_video_info(constants.VIDEO_DICT_ZEBRA.get("edx_video_id"))
...@@ -34,11 +34,11 @@ class SerializerTests(TestCase): ...@@ -34,11 +34,11 @@ class SerializerTests(TestCase):
Tests negative inputs for bitrate, file_size in EncodedVideo Tests negative inputs for bitrate, file_size in EncodedVideo
""" """
errors = EncodedVideoSerializer( # pylint: disable=E1101 errors = EncodedVideoSerializer( # pylint: disable=E1101
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors
self.assertEqual(errors.get('bitrate')[0], self.assertEqual(errors.get('bitrate')[0],
u"Ensure this value is greater than or equal to 0.") u"Ensure this value is greater than or equal to 0.")
errors = EncodedVideoSerializer( # pylint: disable=E1101 errors = EncodedVideoSerializer( # pylint: disable=E1101
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE).errors data=constants.ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE).errors
self.assertEqual(errors.get('file_size')[0], self.assertEqual(errors.get('file_size')[0],
u"Ensure this value is greater than or equal to 0.") u"Ensure this value is greater than or equal to 0.")
...@@ -49,7 +49,7 @@ class SerializerTests(TestCase): ...@@ -49,7 +49,7 @@ class SerializerTests(TestCase):
Tests negative inputs for duration in model Video Tests negative inputs for duration in model Video
""" """
errors = VideoSerializer( # pylint: disable=E1101 errors = VideoSerializer( # pylint: disable=E1101
data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors
self.assertEqual(errors.get('duration')[0], self.assertEqual(errors.get('duration')[0],
u"Ensure this value is greater than or equal to 0.") u"Ensure this value is greater than or equal to 0.")
...@@ -58,7 +58,7 @@ class SerializerTests(TestCase): ...@@ -58,7 +58,7 @@ class SerializerTests(TestCase):
""" """
Tests if the serializers can accept non-latin chars Tests if the serializers can accept non-latin chars
""" """
#TODO not the best test. Need to understand what result we want # TODO not the best test. Need to understand what result we want
self.assertIsInstance( self.assertIsInstance(
ProfileSerializer(Profile.objects.get(profile_name="배고파")), ProfileSerializer(Profile.objects.get(profile_name="배고파")),
ProfileSerializer ProfileSerializer
...@@ -68,7 +68,7 @@ class SerializerTests(TestCase): ...@@ -68,7 +68,7 @@ class SerializerTests(TestCase):
""" """
Test the Video model regex validation for edx_video_id field Test the Video model regex validation for edx_video_id field
""" """
error = VideoSerializer(data=constants.VIDEO_DICT_INVALID_ID).errors # pylint: disable=E1101 error = VideoSerializer(data=constants.VIDEO_DICT_INVALID_ID).errors # pylint: disable=E1101
message = error.get("edx_video_id")[0] message = error.get("edx_video_id")[0]
self.assertEqual( self.assertEqual(
message, message,
...@@ -89,7 +89,7 @@ class SerializerTests(TestCase): ...@@ -89,7 +89,7 @@ class SerializerTests(TestCase):
profile=Profile.objects.get(profile_name="mobile"), profile=Profile.objects.get(profile_name="mobile"),
**constants.ENCODED_VIDEO_DICT_MOBILE **constants.ENCODED_VIDEO_DICT_MOBILE
) )
result = VideoSerializer(video).data # pylint: disable=E1101 result = VideoSerializer(video).data # pylint: disable=E1101
# Check for 2 EncodedVideo entries # Check for 2 EncodedVideo entries
self.assertEqual(len(result.get("encoded_videos")), 2) self.assertEqual(len(result.get("encoded_videos")), 2)
# Check for original Video data # Check for original Video data
......
# pylint: disable=E1103, W0106 # pylint: disable=E1103, W0106
""" """
Tests for Video Abstraction Layer views Tests for Video Abstraction Layer views
""" """
...@@ -22,7 +22,7 @@ class VideoDetail(APITestCase): ...@@ -22,7 +22,7 @@ class VideoDetail(APITestCase):
Profile.objects.create(**constants.PROFILE_DICT_MOBILE) Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP) Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
#Tests for successful PUT requests. # Tests for successful PUT requests.
def test_update_video(self): def test_update_video(self):
""" """
...@@ -91,7 +91,7 @@ class VideoDetail(APITestCase): ...@@ -91,7 +91,7 @@ class VideoDetail(APITestCase):
'video-detail', 'video-detail',
kwargs={"edx_video_id": constants.COMPLETE_SET_FISH.get("edx_video_id")} kwargs={"edx_video_id": constants.COMPLETE_SET_FISH.get("edx_video_id")}
) )
response = self.client.patch( # pylint: disable=E1101 response = self.client.patch( # pylint: disable=E1101
path=url, path=url,
data=constants.COMPLETE_SET_UPDATE_FISH, data=constants.COMPLETE_SET_UPDATE_FISH,
format='json' format='json'
...@@ -140,6 +140,7 @@ class VideoDetail(APITestCase): ...@@ -140,6 +140,7 @@ class VideoDetail(APITestCase):
Tests PUTting one of two EncodedVideo(s) and then a single EncodedVideo PUT back. Tests PUTting one of two EncodedVideo(s) and then a single EncodedVideo PUT back.
""" """
url = reverse('video-list') url = reverse('video-list')
response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json') response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
url = reverse( url = reverse(
...@@ -216,7 +217,7 @@ class VideoDetail(APITestCase): ...@@ -216,7 +217,7 @@ class VideoDetail(APITestCase):
constants.ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP.get("url") constants.ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP.get("url")
) )
#Tests for bad PUT requests. # Tests for bad PUT requests.
def test_update_an_invalid_encoded_videos(self): def test_update_an_invalid_encoded_videos(self):
""" """
...@@ -304,7 +305,7 @@ class VideoListTest(APITestCase): ...@@ -304,7 +305,7 @@ class VideoListTest(APITestCase):
def test_complete_set_two_encoded_video_post(self): def test_complete_set_two_encoded_video_post(self):
""" """
Tests POSTing Video and EncodedVideo pair Tests POSTing Video and EncodedVideo pair
""" #pylint: disable=R0801 """ # pylint: disable=R0801
url = reverse('video-list') url = reverse('video-list')
response = self.client.post( response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json' url, constants.COMPLETE_SET_FISH, format='json'
...@@ -434,7 +435,7 @@ class VideoListTest(APITestCase): ...@@ -434,7 +435,7 @@ class VideoListTest(APITestCase):
Tests number of queries for a Video with no Encoded Videos Tests number of queries for a Video with no Encoded Videos
""" """
url = reverse('video-list') url = reverse('video-list')
with self.assertNumQueries(3): with self.assertNumQueries(4):
self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json') self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
def test_queries_for_two_encoded_video(self): def test_queries_for_two_encoded_video(self):
...@@ -442,7 +443,7 @@ class VideoListTest(APITestCase): ...@@ -442,7 +443,7 @@ class VideoListTest(APITestCase):
Tests number of queries for a Video/EncodedVideo(2) pair Tests number of queries for a Video/EncodedVideo(2) pair
""" """
url = reverse('video-list') url = reverse('video-list')
with self.assertNumQueries(11): with self.assertNumQueries(16):
self.client.post(url, constants.COMPLETE_SET_FISH, format='json') self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
def test_queries_for_single_encoded_videos(self): def test_queries_for_single_encoded_videos(self):
...@@ -450,7 +451,7 @@ class VideoListTest(APITestCase): ...@@ -450,7 +451,7 @@ class VideoListTest(APITestCase):
Tests number of queries for a Video/EncodedVideo(1) pair Tests number of queries for a Video/EncodedVideo(1) pair
""" """
url = reverse('video-list') url = reverse('video-list')
with self.assertNumQueries(7): with self.assertNumQueries(10):
self.client.post(url, constants.COMPLETE_SET_STAR, format='json') self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
...@@ -486,13 +487,94 @@ class VideoDetailTest(APITestCase): ...@@ -486,13 +487,94 @@ class VideoDetailTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json') response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(2): with self.assertNumQueries(4):
self.client.get("/edxval/video/").data self.client.get("/edxval/video/").data
response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json') response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(4): with self.assertNumQueries(9):
self.client.get("/edxval/video/").data self.client.get("/edxval/video/").data
response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json') response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(5): with self.assertNumQueries(12):
self.client.get("/edxval/video/").data self.client.get("/edxval/video/").data
class SubtitleDetailTest(APITestCase):
"""
Tests for subtitle API
"""
def setUp(self):
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
def test_get_subtitle_content(self):
"""
Get subtitle content
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
video = self.client.get("/edxval/video/").data
self.assertEqual(len(video), 1)
self.assertEqual(len(video[0].get("subtitles")), 2)
st = video[0]['subtitles'][0]
response = self.client.get(st['content_url'])
self.assertEqual(response.content, constants.SUBTITLE_DICT_SRT['content'])
self.assertEqual(response['Content-Type'], 'text/plain')
st = video[0]['subtitles'][1]
response = self.client.get(st['content_url'])
self.assertEqual(response.content, constants.SUBTITLE_DICT_SJSON['content'])
self.assertEqual(response['Content-Type'], 'application/json')
def test_update_subtitle(self):
"""
Update an SRT subtitle
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
video = response.data
st = video['subtitles'][0]
url = reverse('subtitle-detail', kwargs={'video__edx_video_id': video['edx_video_id'], 'language': st['language']})
st['content'] = 'testing 123'
response = self.client.put(
url, st, format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.client.get(st['content_url']).content, 'testing 123')
def test_update_json_subtitle(self):
"""
Update a JSON subtitle
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
video = response.data
st = video['subtitles'][1]
url = reverse('subtitle-detail', kwargs={'video__edx_video_id': video['edx_video_id'], 'language': st['language']})
st['content'] = 'testing 123'
response = self.client.put(
url, st, format='json'
)
# not in json format
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
st['content'] = """{"start": "00:00:00"
}"""
response = self.client.put(
url, st, format='json'
)
self.assertEqual(self.client.get(st['content_url']).content, '{"start": "00:00:00"}')
...@@ -14,8 +14,18 @@ urlpatterns = patterns( ...@@ -14,8 +14,18 @@ urlpatterns = patterns(
name="video-list" name="video-list"
), ),
url( url(
r'^edxval/video/(?P<edx_video_id>[-\w]+)', r'^edxval/video/(?P<edx_video_id>[-\w]+)$',
views.VideoDetail.as_view(), views.VideoDetail.as_view(),
name="video-detail" name="video-detail"
), ),
url(
r'^edxval/video/(?P<video__edx_video_id>[-\w]+)/(?P<language>[-_\w]+)$',
views.SubtitleDetail.as_view(),
name="subtitle-detail"
),
url(
r'^edxval/video/(?P<edx_video_id>[-\w]+)/(?P<language>[-_\w]+)/subtitle$',
views.get_subtitle,
name="subtitle-content"
),
) )
...@@ -3,13 +3,30 @@ Views file for django app edxval. ...@@ -3,13 +3,30 @@ Views file for django app edxval.
""" """
from rest_framework import generics from rest_framework import generics
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import last_modified
from edxval.models import Video, Profile from edxval.models import Video, Profile, Subtitle
from edxval.serializers import ( from edxval.serializers import (
VideoSerializer, VideoSerializer,
ProfileSerializer ProfileSerializer,
SubtitleSerializer
) )
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter) # Lookup the object
class VideoList(generics.ListCreateAPIView): class VideoList(generics.ListCreateAPIView):
""" """
...@@ -36,3 +53,25 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView): ...@@ -36,3 +53,25 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
lookup_field = "edx_video_id" lookup_field = "edx_video_id"
queryset = Video.objects.all() queryset = Video.objects.all()
serializer_class = VideoSerializer serializer_class = VideoSerializer
class SubtitleDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
"""
Gets a subtitle instance given its id
"""
lookup_fields = ("video__edx_video_id", "language")
queryset = Subtitle.objects.all()
serializer_class = SubtitleSerializer
def _last_modified_subtitle(request, edx_video_id, language):
return Subtitle.objects.get(video__edx_video_id=edx_video_id, language=language).modified
@last_modified(last_modified_func=_last_modified_subtitle)
def get_subtitle(request, edx_video_id, language):
"""
Return content of subtitle by id
"""
sub = Subtitle.objects.get(video__edx_video_id=edx_video_id, language=language)
response = HttpResponse(sub.content, content_type=sub.content_type)
return response
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