Commit 49c58413 by Christopher Lee

Merge pull request #8 from edx/clee/generic_views

POST for single EncodedVideo
parents 7a910438 e793792a
......@@ -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)
......@@ -108,4 +104,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()
......@@ -88,4 +91,4 @@ class EncodedVideo(models.Model):
).format(self)
def __unicode__(self):
return repr(self)
\ No newline at end of file
return repr(self)
......@@ -6,34 +6,6 @@ from rest_framework import serializers
from edxval.models import Profile, Video, EncodedVideo
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 ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
......@@ -45,31 +17,24 @@ class ProfileSerializer(serializers.ModelSerializer):
)
class OnlyEncodedVideoSerializer(serializers.ModelSerializer):
"""
Used to serialize the EncodedVideo for the EncodedVideoSetSerializer
"""
profile = ProfileSerializer(required=False)
class EncodedVideoSerializer(serializers.ModelSerializer):
profile = serializers.SlugRelatedField(slug_field="profile_name")
class Meta:
model = EncodedVideo
fields = (
"created",
"modified",
"url",
"file_size",
"bitrate"
"bitrate",
"profile",
)
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()
class VideoSerializer(serializers.HyperlinkedModelSerializer):
encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True)
class Meta:
model = Video
fields = (
"duration",
"client_video_id"
)
lookup_field = "edx_video_id"
......@@ -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_videos=[]
)
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com",
file_size=-25556,
......@@ -23,19 +48,32 @@ ENCODED_VIDEO_DICT_NEGATIVE_BITRATE = dict(
file_size=25556,
bitrate=-9600,
)
PROFILE_DICT_MOBILE = dict(
profile_name="mobile",
extension="avi",
width=100,
height=101
VIDEO_DICT_BEE_INVALID = dict(
client_video_id="Barking Bee",
duration=111.00,
edx_video_id="wa/sps",
)
PROFILE_DICT_DESKTOP = dict(
profile_name="desktop",
extension="mp4",
width=200,
height=2001
VIDEO_DICT_INVALID_ID = dict(
client_video_id="SuperSloth",
duration=42,
edx_video_id="sloppy/sloth!!",
encoded_videos=[]
)
"""
Non-latin/invalid
"""
VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id=u"배고픈 햄스터",
duration=42,
edx_video_id="ID",
encoded_videos=[]
)
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘",
encoded_videos=[]
)
PROFILE_DICT_NON_LATIN = dict(
profile_name=u"배고파",
......@@ -43,87 +81,168 @@ 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="mobile"
)
VIDEO_DICT_NEGATIVE_DURATION = dict(
client_video_id="Thunder Cats S01E01",
duration=-111,
edx_video_id="thisis12char-thisis7",
COMPLETE_SET_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR
],
**VIDEO_DICT_STAR
)
VIDEO_DICT_INVALID_ID = dict(
client_video_id="SuperSloth",
duration=42,
edx_video_id="sloppy/sloth!!"
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_NON_LATIN_TITLE = dict(
client_video_id="배고픈 햄스터",
duration=42,
edx_video_id="ID"
COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
encoded_videos=[
dict(
url="https://www.vulturevideos.com",
file_size=101010,
bitrate=1234,
profile="mobile",
video="This should be overridden by parent video field"
)
],
**VIDEO_DICT_STAR
)
"""
Unsorted
"""
VIDEO_DICT_COAT = dict(
client_video_id="Callous Coat",
duration=111.00,
edx_video_id="itchyjacket"
)
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_UPDATE_ANIMAL = dict(
client_video_id="Lolcat",
duration=122.00,
edx_video_id="mediocrity",
)
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘"
)
\ No newline at end of file
VIDEO_DICT_CRAYFISH = dict(
client_video_id="Crazy Crayfish",
duration=111.00,
edx_video_id="craycray",
)
VIDEO_DICT_DUPLICATES = [
VIDEO_DICT_CRAYFISH,
VIDEO_DICT_CRAYFISH,
VIDEO_DICT_CRAYFISH
]
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_ANIMAL,
VIDEO_DICT_CRAYFISH
]
VIDEO_DICT_INVALID_SET = [
VIDEO_DICT_COAT,
VIDEO_DICT_INVALID_ID,
VIDEO_DICT_BEE_INVALID
]
......@@ -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
......@@ -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
......@@ -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
......@@ -77,4 +80,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,11 @@ Tests the serializers for the Video Abstraction Layer
from django.test import TestCase
from edxval.serializers import (
OnlyEncodedVideoSerializer,
EncodedVideoSetSerializer,
EncodedVideoSerializer,
ProfileSerializer,
VideoSerializer
VideoSerializer,
)
from edxval.models import Profile
from edxval.models import Profile, Video, EncodedVideo
from edxval.tests import constants
......@@ -21,45 +20,56 @@ 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
"""
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.")
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
"""
self.assertIsNotNone(
ProfileSerializer(Profile.objects.get(profile_name="배고파"))
#TODO not the best test. Need to understand what result we want
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
......@@ -68,4 +78,25 @@ 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 = 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)
......@@ -8,11 +8,9 @@ admin.autodiscover()
urlpatterns = patterns('',
url(r'^edxval/video/$', views.VideoList.as_view(),
name="video_view"),
name="video-list"),
url(r'^edxval/video/(?P<edx_video_id>\w+)',
views.VideoDetail.as_view(),
name="video_detail_view"),
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
from edxval.serializers import (
VideoSerializer
)
class VideoList(APIView):
class VideoList(generics.ListCreateAPIView):
"""
HTTP API for Video objects
GETs or POST video objects
"""
def get(self, request, format=None):
"""
Gets all videos
"""
video = Video.objects.all()
serializer = VideoSerializer(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.
Args:
request (object): Object where we get our information for POST
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)
else:
return Response(status=status.HTTP_201_CREATED)
queryset = Video.objects.all().prefetch_related("encoded_videos")
lookup_field = "edx_video_id"
serializer_class = VideoSerializer
class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
......@@ -62,4 +21,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
)
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