Commit e793792a by christopher lee

POST for single EncodedVideo

POST for EncodedVideo implemented. Can now upload a single Video
with multiple EncodedVideos with profile_name as it's profile.
Cannot do any bulk operations. Serializers have been simplified.
Views are now using genericviews. Queries are now being tracked
in a few tests.

Other notes:
-Added django-debug-toolbar==1.2.1
-constants.py has been reorganized
-api.get_video_info uses new serializer
parent 266fa9f5
......@@ -5,7 +5,7 @@ The internal API for VAL
import logging
from edxval.models import Video
from edxval.serializers import EncodedVideoSetSerializer
from edxval.serializers import VideoSerializer
logger = logging.getLogger(__name__)
......@@ -57,6 +57,7 @@ def get_video_info(edx_video_id, location=None):
Returns all the Video object fields, and it's related EncodedVideo
objects in a list.
{
url: api url to the video
edx_video_id: ID of the video
duration: Length of video in seconds
client_video_id: client ID of video
......@@ -75,32 +76,27 @@ def get_video_info(edx_video_id, location=None):
ValInternalError: Raised for unknown errors
Example:
Given one EncodedVideo with edx_video_id "thisis12char-thisis7"
>>>
>>> get_video_info("thisis12char-thisis7",location)
Given one EncodedVideo with edx_video_id "example"
>>> get_video_info("example")
Returns (dict):
>>>{
>>> 'edx_video_id': u'thisis12char-thisis7',
>>> 'duration': 111.0,
>>> 'client_video_id': u'Thunder Cats S01E01',
>>> 'encoded_video': [
>>> {
>>> 'url': u'http://www.meowmix.com',
>>> 'file_size': 25556,
>>> 'bitrate': 9600,
>>> 'profile': {
>>> 'profile_name': u'mobile',
>>> 'extension': u'avi',
>>> 'width': 100,
>>> 'height': 101
>>> }
>>> },
>>> ]
>>>}
{
'url' : '/edxval/video/example'
'edx_video_id': u'example',
'duration': 111.0,
'client_video_id': u'The example video',
'encoded_video': [
{
'url': u'http://www.meowmix.com',
'file_size': 25556,
'bitrate': 9600,
'profile': u'mobile'
}
]
}
"""
try:
v = Video.objects.get(edx_video_id=edx_video_id)
result = EncodedVideoSetSerializer(v)
result = VideoSerializer(v)
except Video.DoesNotExist:
error_message = u"Video not found for edx_video_id: {0}".format(edx_video_id)
raise ValVideoNotFoundError(error_message)
......
......@@ -91,4 +91,4 @@ class EncodedVideo(models.Model):
).format(self)
def __unicode__(self):
return repr(self)
\ No newline at end of file
return repr(self)
......@@ -2,101 +2,9 @@
Serializers for Video Abstraction Layer
"""
from rest_framework import serializers
from rest_framework.serializers import ValidationError
from edxval.models import Profile, Video, EncodedVideo
class ProfileSerializer2(serializers.ModelSerializer):
class Meta:
model = Profile
fields = (
"profile_name",
"extension",
"width",
"height"
)
class EncodedVideoSerializer2(serializers.ModelSerializer):
profile = serializers.SlugRelatedField(slug_field="profile_name")
class Meta:
model = EncodedVideo
fields = (
"created",
"modified",
"url",
"file_size",
"bitrate",
"profile",
)
class VideoSerializer2(serializers.HyperlinkedModelSerializer):
encoded_videos = EncodedVideoSerializer2(many=True)
class Meta:
model = Video
lookup_field = "edx_video_id"
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 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
fields = (
"client_video_id",
"duration",
"edx_video_id"
)
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
......@@ -109,175 +17,24 @@ class ProfileSerializer(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 = 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)
profile = serializers.SlugRelatedField(slug_field="profile_name")
class Meta:
model = EncodedVideo
fields = (
"created",
"modified",
"url",
"file_size",
"bitrate",
"profile",
"video"
)
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 = EncodedVideoSerializer(required=False)
class VideoSerializer(serializers.HyperlinkedModelSerializer):
encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True)
class Meta:
model = Video
fields = (
"duration",
"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
lookup_field = "edx_video_id"
......@@ -36,6 +36,7 @@ VIDEO_DICT_NEGATIVE_DURATION = dict(
client_video_id="Thunder Cats S01E01",
duration=-111,
edx_video_id="thisis12char-thisis7",
encoded_videos=[]
)
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com",
......@@ -47,18 +48,32 @@ ENCODED_VIDEO_DICT_NEGATIVE_BITRATE = dict(
file_size=25556,
bitrate=-9600,
)
VIDEO_DICT_BEE_INVALID = dict(
client_video_id="Barking Bee",
duration=111.00,
edx_video_id="wa/sps",
)
VIDEO_DICT_INVALID_ID = dict(
client_video_id="SuperSloth",
duration=42,
edx_video_id="sloppy/sloth!!",
encoded_videos=[]
)
"""
Non-latin
Non-latin/invalid
"""
VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id="배고픈 햄스터",
client_video_id=u"배고픈 햄스터",
duration=42,
edx_video_id="ID"
edx_video_id="ID",
encoded_videos=[]
)
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘"
edx_video_id="밥줘",
encoded_videos=[]
)
PROFILE_DICT_NON_LATIN = dict(
profile_name=u"배고파",
......@@ -140,19 +155,12 @@ 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
profile="mobile"
)
COMPLETE_SET_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR,
ENCODED_VIDEO_DICT_STAR2
ENCODED_VIDEO_DICT_STAR
],
**VIDEO_DICT_STAR
)
......@@ -173,8 +181,8 @@ COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
url="https://www.vulturevideos.com",
file_size=101010,
bitrate=1234,
profile=1,
video="This should be overridden by parent videofield"
profile="mobile",
video="This should be overridden by parent video field"
)
],
**VIDEO_DICT_STAR
......@@ -185,14 +193,21 @@ Unsorted
VIDEO_DICT_COAT = dict(
client_video_id="Callous Coat",
duration=111.00,
edx_video_id="itchyjacket",
edx_video_id="itchyjacket"
)
VIDEO_DICT_AVERAGE = dict(
VIDEO_DICT_ANIMAL = dict(
client_video_id="Average Animal",
duration=111.00,
edx_video_id="mediocrity",
encoded_videos=[]
)
VIDEO_DICT_ZEBRA = dict(
client_video_id="Zesty Zebra",
duration=111.00,
edx_video_id="zestttt",
encoded_videos=[]
)
VIDEO_DICT_AVERAGE2 = dict(
VIDEO_DICT_UPDATE_ANIMAL = dict(
client_video_id="Lolcat",
duration=122.00,
edx_video_id="mediocrity",
......@@ -204,18 +219,6 @@ VIDEO_DICT_CRAYFISH = dict(
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(
client_video_id="SuperSloth",
duration=42,
edx_video_id="sloppy/sloth!!"
)
VIDEO_DICT_DUPLICATES = [
VIDEO_DICT_CRAYFISH,
VIDEO_DICT_CRAYFISH,
......@@ -234,7 +237,7 @@ COMPLETE_SETS_ONE_INVALID = [
VIDEO_DICT_SET_OF_THREE = [
VIDEO_DICT_COAT,
VIDEO_DICT_AVERAGE,
VIDEO_DICT_ANIMAL,
VIDEO_DICT_CRAYFISH
]
......
......@@ -10,7 +10,7 @@ from django.db import DatabaseError
from edxval.models import Profile, Video, EncodedVideo
from edxval import api as api
from edxval.serializers import EncodedVideoSetSerializer
from edxval.serializers import VideoSerializer
from edxval.tests import constants
......@@ -51,8 +51,11 @@ class GetVideoInfoTest(TestCase):
"""
Tests searching for a video that does not exist
"""
with self.assertRaises(api.ValVideoNotFoundError):
api.get_video_info("non_existant-video__")
with self.assertRaises(api.ValVideoNotFoundError):
api.get_video_info("non_existing-video__")
api.get_video_info("")
def test_unicode_input(self):
"""
......@@ -61,7 +64,7 @@ class GetVideoInfoTest(TestCase):
with self.assertRaises(api.ValVideoNotFoundError):
api.get_video_info(u"๓ﻉѻฝ๓ٱซ")
@mock.patch.object(EncodedVideoSetSerializer, '__init__')
@mock.patch.object(VideoSerializer, '__init__')
def test_force_internal_error(self, mock_init):
"""
Tests to see if an unknown error will be handled
......
......@@ -7,11 +7,8 @@ from django.test import TestCase
from edxval.serializers import (
EncodedVideoSerializer,
EncodedVideoSetSerializer,
ProfileSerializer,
VideoSerializer,
DisplayProfileName
)
from edxval.models import Profile, Video, EncodedVideo
from edxval.tests import constants
......@@ -23,15 +20,15 @@ class SerializerTests(TestCase):
"""
def setUp(self):
"""
Creates EncodedVideo objects in database
Creates Profile objects
"""
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):
def test_negative_fields_for_encoded_video_serializer(self):
"""
Tests negative inputs for OnlyEncodedSerializer
Tests negative inputs for EncodedVideoSerializer
Tests negative inputs for bitrate, file_size in EncodedVideo
"""
......@@ -45,26 +42,34 @@ class SerializerTests(TestCase):
self.assertEqual(b.get('file_size')[0],
u"Ensure this value is greater than or equal to 0.")
def test_negative_fields_video_set(self):
def test_negative_fields_for_video_serializer(self):
"""
Tests negative inputs for EncodedVideoSetSerializer
Tests negative inputs for VideoSerializer
Tests negative inputs for duration in model Video
"""
c = EncodedVideoSetSerializer(
c = VideoSerializer(
data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors
self.assertEqual(c.get('duration')[0],
u"Ensure this value is greater than or equal to 0.")
def test_unicode_inputs(self):
def test_non_latin_serialization(self):
"""
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="배고파"))
self.assertIsInstance(
ProfileSerializer(Profile.objects.get(profile_name="배고파")),
ProfileSerializer
)
def test_non_latin_deserialization(self):
"""
Tests deserialization of non-latin data
"""
#TODO write a test for this when we understand what we want
pass
def test_invalid_edx_video_id(self):
"""
Test the Video model regex validation for edx_video_id field
......@@ -84,11 +89,14 @@ class SerializerTests(TestCase):
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
)
result = VideoSerializer(video).data
# Check for 2 EncodedVideo entries
self.assertEqual(len(result.get("encoded_videos")), 2)
# Check for original Video data
self.assertDictContainsSubset(constants.VIDEO_DICT_COAT, result)
......@@ -14,5 +14,3 @@ urlpatterns = patterns('',
name="video-detail"),
url(r'^admin/', include(admin.site.urls)),
)
urlpatterns = format_suffix_patterns(urlpatterns)
\ No newline at end of file
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, generics
from rest_framework import generics
from edxval.models import Video
from edxval.serializers import (
VideoSerializer, EncodedVideoSetDeserializer, EncodedVideoSetSerializer,
VideoSerializer2,
VideoSerializer
)
# class VideoList(APIView):
# """
# <<<<<<< HEAD
# HTTP API for Video and EncodedVideo objects
# """
#
# def get(self, request, format=None):
# """
# Gets all videos
# """
# video = Video.objects.all()
# serializer = EncodedVideoSetSerializer(video, many=True)
# return Response(serializer.data)
#
# def post(self, request, format=None):
# """
# 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
# data_format (str): format of our data (JSON, XML, etc.)
#
# Returns:
# Response(message, HTTP status)
#
# """
# serializer = EncodedVideoSetDeserializer(data=request.DATA)
# if serializer.is_valid():
# serializer.save()
# return Response("Success", status=status.HTTP_201_CREATED)
# else:
# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class VideoList(generics.ListCreateAPIView):
queryset = Video.objects.all().select_related("encoded_videos")
"""
GETs or POST video objects
"""
queryset = Video.objects.all().prefetch_related("encoded_videos")
lookup_field = "edx_video_id"
serializer_class = VideoSerializer2
serializer_class = VideoSerializer
class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
"""
......@@ -53,4 +21,4 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
"""
lookup_field = "edx_video_id"
queryset = Video.objects.all()
serializer_class = VideoSerializer2
serializer_class = VideoSerializer
......@@ -14,5 +14,3 @@ urlpatterns = patterns('',
name="video_detail_view"),
url(r'^admin/', include(admin.site.urls)),
)
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