Commit 112da93f by christopher lee

POST EncodedVideo set

EncodedVideo part of the upload. Complete EncodedVideo dicts may
now be uploaded. Rejects all EncodedVideos of a Video when there is
an invalid EncodedVideo. Does not reject other Video when a differet
Video is invalid. Currently, EncodedVideo cannot be updated, they can
only be created.

Other notes:
-Added django-debug-toolbar==1.2.1
-constants.py has been reorganized
parent 7a910438
......@@ -108,4 +108,4 @@ def get_video_info(edx_video_id, location=None):
error_message = u"Could not get edx_video_id: {0}".format(edx_video_id)
logger.exception(error_message)
raise ValInternalError(error_message)
return result.data
\ No newline at end of file
return result.data
......@@ -10,7 +10,10 @@ class Profile(models.Model):
"""
Details for pre-defined encoding format
"""
profile_name = models.CharField(max_length=50, unique=True)
profile_name = models.CharField(
max_length=50,
unique=True,
)
extension = models.CharField(max_length=10)
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
......
......@@ -2,29 +2,60 @@
Serializers for Video Abstraction Layer
"""
from rest_framework import serializers
from rest_framework.serializers import ValidationError
from edxval.models import Profile, Video, EncodedVideo
class VideoSerializer(serializers.ModelSerializer):
class DisplayProfileName(serializers.WritableField):
"""
Takes a Profile Object and returns only it's profile_name
"""
def from_native(self, data):
try:
if isinstance(data, int):
profile = Profile.objects.get(pk=data)
elif isinstance(data, unicode):
profile = Profile.objects.get(profile_name=str(data))
return profile
except Profile.DoesNotExist:
error_message = "Profile does not exist: {0}".format(data)
raise ValidationError(error_message)
def restore_object(self, attrs, instance=None):
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
"""
if instance is not None:
instance.edx_video_id = attrs.get(
'edx_video_id', instance.edx_video_id
)
instance.duration = attrs.get(
'duration', instance.duration
)
instance.client_video_id = attrs.get(
'client_video_id', instance.client_video_id
)
return instance
return Video(**attrs)
def to_native(self, data):
return data.profile_name
class DisplayVideoName(serializers.WritableField):
"""
Takes a Video Object and returns only it's profile_name
"""
def from_native(self, data):
#TODO change doc/function name or modify this function, currently
#TODO takes a video pk and converts it to video Object.
try:
if isinstance(data, int):
video = Video.objects.get(pk=data)
return video
except Video.DoesNotExist:
error_message = "Video does not exist: {0}".format(data)
raise ValidationError(error_message)
def to_native(self, data):
return data.edx_video_id
class ListField(serializers.WritableField):
"""
Allows the use of a list as a serializer field.
"""
def from_native(self, data):
if isinstance(data, list):
return data
else:
error_message = "Expecting a list: {0}".format(type(data))
raise ValidationError(error_message)
class Meta:
model = Video
......@@ -45,17 +76,59 @@ class ProfileSerializer(serializers.ModelSerializer):
)
class OnlyEncodedVideoSerializer(serializers.ModelSerializer):
class VideoSerializer(serializers.ModelSerializer):
def restore_object(self, attrs, instance=None):
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
"""
if instance is not None:
instance.edx_video_id = attrs.get('edx_video_id', instance.edx_video_id)
instance.duration = attrs.get('duration', instance.duration)
instance.client_video_id = attrs.get('client_video_id', instance.client_video_id)
return instance
return Video(**attrs)
class Meta:
model = Video
fields = (
"client_video_id",
"duration",
"edx_video_id"
)
class EncodedVideoSerializer(serializers.ModelSerializer):
"""
Used to serialize the EncodedVideo for the EncodedVideoSetSerializer
"""
profile = ProfileSerializer(required=False)
profile = DisplayProfileName()
video = DisplayVideoName()
def restore_object(self, attrs, instance=None):
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
"""
if instance is not None:
#TODO Currently not updating Encodedvideo Object.
instance.url = attrs.get('url', instance.url)
instance.file_size = attrs.get('file_size', instance.file_size)
instance.bitrate = attrs.get('bitrate', instance.bitrate)
#TODO profile instance.profile = attrs.get('profile', instance.profile)
return instance
return EncodedVideo(**attrs)
class Meta:
model = EncodedVideo
fields = (
"url",
"file_size",
"bitrate"
"bitrate",
"profile",
"video"
)
......@@ -64,12 +137,114 @@ class EncodedVideoSetSerializer(serializers.ModelSerializer):
Used to serialize a list of EncodedVideo objects it's foreign key Video Object.
"""
edx_video_id = serializers.CharField(max_length=50)
encoded_videos = OnlyEncodedVideoSerializer()
encoded_videos = EncodedVideoSerializer(required=False)
class Meta:
model = Video
fields = (
"duration",
"client_video_id"
"client_video_id",
"edx_video_id",
"encoded_videos"
)
class EncodedVideoSetDeserializer(serializers.Serializer):
"""
Deserializes a dict of Video fields and list of EncodedVideos.
Example:
>>>data = dict(
... client_video_id="Shallow Swordfish",
... duration=122.00,
... edx_video_id="supersoaker",
... encoded_videos=[
... dict(
... url="https://www.swordsingers.com",
... file_size=9000,
... bitrate=42,
... profile=1,
... ),
... dict(
... url="https://www.swordsingers.com",
... file_size=9000,
... bitrate=42,
... profile=2,
... )
... ]
... )
>>>serilizer = EncodedVideoSetDeserializer(data=data)
>>>if serilizer.is_valid()
>>> serializer.save()
>>>video = Video.objects.get(edx_video_id="supersoaker")
>>>print EncodedVideoSetDeserializer(video).data
{
'duration': 122.0,
'client_video_id': u'ShallowSwordfish',
'edx_video_id': u'supersoaker',
'encoded_videos': [
{
'url': u'https: //www.swordsingers.com',
'file_size': 9000,
'bitrate': 42,
'profile': u'mobile',
'video': u'supersoaker'
},
{
'url': u'https: //www.swordsingers.com',
'file_size': 9000,
'bitrate': 42,
'profile': u'desktop',
'video': u'supersoaker'
}
]
}
"""
client_video_id = serializers.CharField(max_length=50)
duration = serializers.FloatField()
edx_video_id = serializers.CharField(max_length=50)
encoded_videos = ListField(required=False)
def restore_object(self, attrs, instance=None):
"""
Updates or creates video object and creates valid EncodedVideo objects.
If the Video parameters are not valid, the errors are returns and the
Video object is not created and the desrialization ends. If any of the
EncodedVideo Parameters are invalid, the errors are returns and the
EncodedVideos objects are not created.
"""
#Get or create the Video object, else return errors
try:
instance = Video.objects.get(edx_video_id=attrs.get("edx_video_id"))
except Video.DoesNotExist:
instance = None
video = VideoSerializer(
data=dict(
edx_video_id=attrs.get("edx_video_id"),
duration=attrs.get("duration"),
client_video_id=attrs.get("client_video_id")
),
instance=instance
)
if video.is_valid():
video.save()
else:
for key in video.errors:
self.errors[key] = video.errors[key]
return
#Point encoded_videos to parent video
if not "encoded_videos" in attrs:
return video
for item in attrs.get("encoded_videos"):
item[u"video"] = Video.objects.get(edx_video_id=attrs.get("edx_video_id")).pk
#Serialize EncodedVideos, else raise errors
ev = EncodedVideoSerializer(data=attrs.get("encoded_videos"), many=True)
if ev.is_valid():
ev.save()
else:
self.errors["encoded_videos"] = ev.errors
return video
\ No newline at end of file
......@@ -124,6 +124,7 @@ INSTALLED_APPS = (
'rest_framework',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
'debug_toolbar'
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
)
......@@ -165,4 +166,4 @@ LOGGING = {
'propagate': True,
},
}
}
\ No newline at end of file
}
# -*- coding: utf-8 -*-
EDX_VIDEO_ID = "thisis12char-thisis7"
EDX_VIDEO_ID = "itchyjacket"
"""
Generic Profiles for manually creating profile objects
"""
PROFILE_DICT_MOBILE = dict(
profile_name="mobile",
extension="avi",
width=100,
height=101
)
PROFILE_DICT_DESKTOP = dict(
profile_name="desktop",
extension="mp4",
width=200,
height=2001
)
"""
Encoded_videos for test_api, does not have profile.
"""
ENCODED_VIDEO_DICT_MOBILE = dict(
url="http://www.meowmix.com",
file_size=25556,
bitrate=9600,
file_size=4545,
bitrate=6767,
)
ENCODED_VIDEO_DICT_DESKTOP = dict(
url="http://www.meowmagic.com",
file_size=25556,
bitrate=9600,
file_size=1212,
bitrate=2323,
)
"""
Validators
"""
VIDEO_DICT_NEGATIVE_DURATION = dict(
client_video_id="Thunder Cats S01E01",
duration=-111,
edx_video_id="thisis12char-thisis7",
)
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com",
file_size=-25556,
......@@ -23,19 +47,18 @@ ENCODED_VIDEO_DICT_NEGATIVE_BITRATE = dict(
file_size=25556,
bitrate=-9600,
)
PROFILE_DICT_MOBILE = dict(
profile_name="mobile",
extension="avi",
width=100,
height=101
"""
Non-latin
"""
VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id="배고픈 햄스터",
duration=42,
edx_video_id="ID"
)
PROFILE_DICT_DESKTOP = dict(
profile_name="desktop",
extension="mp4",
width=200,
height=2001
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘"
)
PROFILE_DICT_NON_LATIN = dict(
profile_name=u"배고파",
......@@ -43,71 +66,148 @@ PROFILE_DICT_NON_LATIN = dict(
width=100,
height=300
)
VIDEO_DICT_CATS = dict(
client_video_id="Thunder Cats S01E01",
duration=111.00,
edx_video_id="thisis12char-thisis7",
"""
Fish
"""
VIDEO_DICT_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="supersoaker"
)
VIDEO_DICT_LION = dict(
client_video_id="Lolcat",
duration=111.00,
edx_video_id="caw",
ENCODED_VIDEO_DICT_FISH_MOBILE = dict(
url="https://www.swordsingers.com",
file_size=9000,
bitrate=42,
profile="mobile",
)
VIDEO_DICT_LION2 = dict(
client_video_id="Lolcat",
ENCODED_VIDEO_DICT_FISH_DESKTOP = dict(
url="https://www.swordsplints.com",
file_size=1234,
bitrate=4222,
profile="desktop",
)
ENCODED_VIDEO_DICT_FISH_INVALID_PROFILE = dict(
url="https://www.swordsplints.com",
file_size=1234,
bitrate=4222,
profile=11,
)
COMPLETE_SET_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_INVALID_PROFILE
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_INVALID_VIDEO_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="caw",
edx_video_id="super/soaker",
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
]
)
VIDEO_DICT_TIGERS_BEARS = [
dict(
client_video_id="Tipsy Tiger",
duration=111.00,
edx_video_id="meeeeeow",
),
dict(
client_video_id="Boring Bear",
duration=111.00,
edx_video_id="hithar",
)
COMPLETE_SETS_ALL_INVALID = [
COMPLETE_SET_INVALID_VIDEO_FISH,
COMPLETE_SET_INVALID_VIDEO_FISH
]
VIDEO_DICT_INVALID_SET = [
dict(
client_video_id="Average Animal",
duration=111.00,
edx_video_id="mediocrity",
),
dict(
client_video_id="Barking Bee",
duration=111.00,
edx_video_id="wa/sps",
),
dict(
client_video_id="Callous Coat",
duration=111.00,
edx_video_id="not an animal",
)
]
VIDEO_DICT_DUPLICATES = [
dict(
client_video_id="Gaggling gopher",
duration=111.00,
edx_video_id="gg",
),
dict(
client_video_id="Gaggling gopher",
duration=111.00,
edx_video_id="gg",
"""
Star
"""
VIDEO_DICT_STAR = dict(
client_video_id="TWINKLE TWINKLE",
duration=122.00,
edx_video_id="little-star"
)
ENCODED_VIDEO_DICT_STAR = dict(
url="https://www.howIwonder.com",
file_size=9000,
bitrate=42,
profile=1
)
ENCODED_VIDEO_DICT_STAR2 = dict(
url="https://www.whatyouare.com",
file_size=1111,
bitrate=2333,
profile=2
)
COMPLETE_SET_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR,
ENCODED_VIDEO_DICT_STAR2
],
**VIDEO_DICT_STAR
)
COMPLETE_SET_NOT_A_LIST = dict(
encoded_videos=dict(
url="https://www.howIwonder.com",
file_size=9000,
bitrate=42,
profile=1
),
]
**VIDEO_DICT_STAR
)
VIDEO_DICT_NEGATIVE_DURATION = dict(
client_video_id="Thunder Cats S01E01",
duration=-111,
edx_video_id="thisis12char-thisis7",
COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
encoded_videos=[
dict(
url="https://www.vulturevideos.com",
file_size=101010,
bitrate=1234,
profile=1,
video="This should be overridden by parent videofield"
)
],
**VIDEO_DICT_STAR
)
"""
Unsorted
"""
VIDEO_DICT_COAT = dict(
client_video_id="Callous Coat",
duration=111.00,
edx_video_id="itchyjacket",
)
VIDEO_DICT_AVERAGE = dict(
client_video_id="Average Animal",
duration=111.00,
edx_video_id="mediocrity",
)
VIDEO_DICT_AVERAGE2 = dict(
client_video_id="Lolcat",
duration=122.00,
edx_video_id="mediocrity",
)
VIDEO_DICT_CRAYFISH = dict(
client_video_id="Crazy Crayfish",
duration=111.00,
edx_video_id="craycray",
)
VIDEO_DICT_BEE_INVALID = dict(
client_video_id="Barking Bee",
duration=111.00,
edx_video_id="wa/sps",
)
VIDEO_DICT_INVALID_ID = dict(
......@@ -116,14 +216,30 @@ VIDEO_DICT_INVALID_ID = dict(
edx_video_id="sloppy/sloth!!"
)
VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id="배고픈 햄스터",
duration=42,
edx_video_id="ID"
)
VIDEO_DICT_DUPLICATES = [
VIDEO_DICT_CRAYFISH,
VIDEO_DICT_CRAYFISH,
VIDEO_DICT_CRAYFISH
]
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘"
)
\ No newline at end of file
COMPLETE_SETS = [
COMPLETE_SET_STAR,
COMPLETE_SET_FISH
]
COMPLETE_SETS_ONE_INVALID = [
COMPLETE_SET_STAR,
COMPLETE_SET_INVALID_VIDEO_FISH
]
VIDEO_DICT_SET_OF_THREE = [
VIDEO_DICT_COAT,
VIDEO_DICT_AVERAGE,
VIDEO_DICT_CRAYFISH
]
VIDEO_DICT_INVALID_SET = [
VIDEO_DICT_COAT,
VIDEO_DICT_INVALID_ID,
VIDEO_DICT_BEE_INVALID
]
......@@ -25,17 +25,17 @@ class GetVideoInfoTest(TestCase):
"""
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
Video.objects.create(**constants.VIDEO_DICT_CATS)
Video.objects.create(**constants.VIDEO_DICT_COAT)
EncodedVideo.objects.create(
video=Video.objects.get(
edx_video_id=constants.VIDEO_DICT_CATS.get("edx_video_id")
edx_video_id=constants.VIDEO_DICT_COAT.get("edx_video_id")
),
profile=Profile.objects.get(profile_name="mobile"),
**constants.ENCODED_VIDEO_DICT_MOBILE
)
EncodedVideo.objects.create(
video=Video.objects.get(
edx_video_id=constants.VIDEO_DICT_CATS.get("edx_video_id")
edx_video_id=constants.VIDEO_DICT_COAT.get("edx_video_id")
),
profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP
......@@ -77,4 +77,4 @@ class GetVideoInfoTest(TestCase):
"""
mock_get.side_effect = DatabaseError("DatabaseError")
with self.assertRaises(api.ValInternalError):
api.get_video_info(constants.EDX_VIDEO_ID)
\ No newline at end of file
api.get_video_info(constants.EDX_VIDEO_ID)
......@@ -6,12 +6,14 @@ Tests the serializers for the Video Abstraction Layer
from django.test import TestCase
from edxval.serializers import (
OnlyEncodedVideoSerializer,
EncodedVideoSerializer,
EncodedVideoSetSerializer,
ProfileSerializer,
VideoSerializer
VideoSerializer,
DisplayProfileName
)
from edxval.models import Profile
from edxval.models import Profile, Video, EncodedVideo
from edxval.tests import constants
......@@ -24,6 +26,7 @@ class SerializerTests(TestCase):
Creates EncodedVideo objects in database
"""
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
Profile.objects.create(**constants.PROFILE_DICT_NON_LATIN)
def test_negative_fields_only_encoded_video(self):
......@@ -32,11 +35,12 @@ class SerializerTests(TestCase):
Tests negative inputs for bitrate, file_size in EncodedVideo
"""
a = OnlyEncodedVideoSerializer(
a = EncodedVideoSerializer(
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors
self.assertEqual(a.get('bitrate')[0],
u"Ensure this value is greater than or equal to 0.")
b = OnlyEncodedVideoSerializer(
b = EncodedVideoSerializer(
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE).errors
self.assertEqual(b.get('file_size')[0],
u"Ensure this value is greater than or equal to 0.")
......@@ -56,6 +60,7 @@ class SerializerTests(TestCase):
"""
Tests if the serializers can accept non-latin chars
"""
#TODO not the best test. Need to understand what result we want
self.assertIsNotNone(
ProfileSerializer(Profile.objects.get(profile_name="배고파"))
)
......@@ -68,4 +73,22 @@ class SerializerTests(TestCase):
message = error.get("edx_video_id")[0]
self.assertEqual(
message,
u"edx_video_id has invalid characters")
\ No newline at end of file
u"edx_video_id has invalid characters")
def test_encoded_video_set_output(self):
"""
Tests for basic structure of EncodedVideoSetSerializer
"""
video = Video.objects.create(**constants.VIDEO_DICT_COAT)
EncodedVideo.objects.create(
video=video,
profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP
)
EncodedVideo.objects.create(
video=video,
profile=Profile.objects.get(profile_name="mobile"),
**constants.ENCODED_VIDEO_DICT_MOBILE
)
result = EncodedVideoSetSerializer(video).data
self.assertEqual(len(result.get("encoded_videos")), 2)
......@@ -3,12 +3,15 @@ from rest_framework.response import Response
from rest_framework import status, generics
from edxval.models import Video
from edxval.serializers import VideoSerializer
from edxval.serializers import VideoSerializer, EncodedVideoSetDeserializer, EncodedVideoSetSerializer
class VideoList(APIView):
"""
HTTP API for Video objects
<<<<<<< HEAD
HTTP API for Video and EncodedVideo objects
"""
def get(self, request, format=None):
......@@ -16,44 +19,27 @@ class VideoList(APIView):
Gets all videos
"""
video = Video.objects.all()
serializer = VideoSerializer(video, many=True)
serializer = EncodedVideoSetSerializer(video, many=True)
return Response(serializer.data)
def post(self, request, format=None):
"""
Takes an object (where we get our list of dict) and creates the objects
Request.DATA is a list of dictionaries. Each item is individually validated
and if valid, saved. All invalid dicts are returned in the error message.
Takes a Video dict of a list of EncodedVideo dicts and creates the objects
Args:
request (object): Object where we get our information for POST
format (str): format of our data (JSON, XML, etc.)
data_format (str): format of our data (JSON, XML, etc.)
Returns:
Response(message, HTTP status)
"""
if not isinstance(request.DATA, list):
error_message = "Not a list: {0}".format(type(request.DATA))
return Response(error_message, status=status.HTTP_400_BAD_REQUEST)
invalid_videos = []
for item in request.DATA:
try:
instance = Video.objects.get(
edx_video_id=item.get("edx_video_id")
)
except Video.DoesNotExist:
instance = None
serializer = VideoSerializer(instance, data=item)
if serializer.is_valid():
serializer.save()
else:
invalid_videos.append((serializer.errors, item))
if invalid_videos:
return Response(invalid_videos, status=status.HTTP_400_BAD_REQUEST)
serializer = EncodedVideoSetDeserializer(data=request.DATA)
if serializer.is_valid():
serializer.save()
return Response("Success", status=status.HTTP_201_CREATED)
else:
return Response(status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
......@@ -62,4 +48,4 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
"""
lookup_field = "edx_video_id"
queryset = Video.objects.all()
serializer_class = VideoSerializer
\ No newline at end of file
serializer_class = VideoSerializer
django-nose==1.2
coverage==3.7.1
mock==1.0.1
\ No newline at end of file
mock==1.0.1
django-debug-toolbar==1.2.1
from django.conf.urls import patterns, include, url
from rest_framework.urlpatterns import format_suffix_patterns
from edxval import views
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
url(r'^edxval/', include('edxval.urls')),
url(r'^edxval/video/$', views.VideoList.as_view(),
name="video_view"),
url(r'^edxval/video/(?P<edx_video_id>\w+)',
views.VideoDetail.as_view(),
name="video_detail_view"),
url(r'^admin/', include(admin.site.urls)),
)
\ No newline at end of file
)
urlpatterns = format_suffix_patterns(urlpatterns)
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