Commit 7a910438 by christopher lee

added POST/GET for video models

Initial part of the upload portion of the API. Video Models can
be uploaded via POST as a list of dictionaries. Matching entries
will be updated and new entries will be created. Errors are
returned in a list of tuples with the message and input. Single
entries can be returned by edx_video_id via GET.

Other notes:
-Django Rest Framework upgraded for APIView 2.3.5->2.3.14
-Field "client_title" -> "client_video_id" in the Video Model
parent 2bb8cd01
[run]
branch = True
[report]
include = edxval/*
omit =
**/__init__.py
**/tests/*
**/settings.py
\ No newline at end of file
language: python
python:
- "2.7"
install:
- "pip install -r requirements.txt"
- "pip install -r test-requirements.txt"
- "pip install coveralls"
script:
- "python manage.py test"
after_success:
coveralls
\ No newline at end of file
Christopher Lee <clee@edx.org>
\ No newline at end of file
This diff is collapsed. Click to expand it.
...@@ -59,7 +59,7 @@ def get_video_info(edx_video_id, location=None): ...@@ -59,7 +59,7 @@ def get_video_info(edx_video_id, location=None):
{ {
edx_video_id: ID of the video edx_video_id: ID of the video
duration: Length of video in seconds duration: Length of video in seconds
client_title: human readable ID client_video_id: client ID of video
encoded_video: a list of EncodedVideo dicts encoded_video: a list of EncodedVideo dicts
url: url of the video url: url of the video
file_size: size of the video in bytes file_size: size of the video in bytes
...@@ -82,7 +82,7 @@ def get_video_info(edx_video_id, location=None): ...@@ -82,7 +82,7 @@ def get_video_info(edx_video_id, location=None):
>>>{ >>>{
>>> 'edx_video_id': u'thisis12char-thisis7', >>> 'edx_video_id': u'thisis12char-thisis7',
>>> 'duration': 111.0, >>> 'duration': 111.0,
>>> 'client_title': u'Thunder Cats S01E01', >>> 'client_video_id': u'Thunder Cats S01E01',
>>> 'encoded_video': [ >>> 'encoded_video': [
>>> { >>> {
>>> 'url': u'http://www.meowmix.com', >>> 'url': u'http://www.meowmix.com',
......
...@@ -3,6 +3,7 @@ Django models for videos for Video Abstraction Layer (VAL) ...@@ -3,6 +3,7 @@ Django models for videos for Video Abstraction Layer (VAL)
""" """
from django.db import models from django.db import models
from django.core.validators import MinValueValidator, RegexValidator
class Profile(models.Model): class Profile(models.Model):
...@@ -30,13 +31,23 @@ class Video(models.Model): ...@@ -30,13 +31,23 @@ class Video(models.Model):
A video can have multiple formats. This model is the collection of those A video can have multiple formats. This model is the collection of those
videos with fields that do not change across formats. videos with fields that do not change across formats.
""" """
edx_video_id = models.CharField(max_length=50, unique=True) edx_video_id = models.CharField(
client_title = models.CharField(max_length=255, db_index=True) max_length=50,
duration = models.FloatField() unique=True,
validators=[
RegexValidator(
regex='^[a-zA-Z0-9\-]*$',
message='edx_video_id has invalid characters',
code='invalid edx_video_id'
),
]
)
client_video_id = models.CharField(max_length=255, db_index=True)
duration = models.FloatField(validators=[MinValueValidator(0)])
def __repr__(self): def __repr__(self):
return ( return (
u"Video(client_title={0.client_title}, duration={0.duration})" u"Video(client_video_id={0.client_video_id}, duration={0.duration})"
).format(self) ).format(self)
def __unicode__(self): def __unicode__(self):
...@@ -72,7 +83,7 @@ class EncodedVideo(models.Model): ...@@ -72,7 +83,7 @@ class EncodedVideo(models.Model):
def __repr__(self): def __repr__(self):
return ( return (
u"EncodedVideo(video={0.video.client_title}, " u"EncodedVideo(video={0.video.client_video_id}, "
u"profile={0.profile.profile_name})" u"profile={0.profile.profile_name})"
).format(self) ).format(self)
......
...@@ -2,16 +2,37 @@ ...@@ -2,16 +2,37 @@
Serializers for Video Abstraction Layer Serializers for Video Abstraction Layer
""" """
from rest_framework import serializers from rest_framework import serializers
from django.core.validators import MinValueValidator
from edxval.models import Profile from edxval.models import Profile, Video, EncodedVideo
class VideoSerializer(serializers.Serializer): class VideoSerializer(serializers.ModelSerializer):
edx_video_id = serializers.CharField(required=True, max_length=50)
duration = serializers.FloatField()
client_title = serializers.CharField(max_length=255)
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 ProfileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
...@@ -24,21 +45,31 @@ class ProfileSerializer(serializers.ModelSerializer): ...@@ -24,21 +45,31 @@ class ProfileSerializer(serializers.ModelSerializer):
) )
class OnlyEncodedVideoSerializer(serializers.Serializer): class OnlyEncodedVideoSerializer(serializers.ModelSerializer):
""" """
Used to serialize the EncodedVideo fir the EncodedVideoSetSerializer Used to serialize the EncodedVideo for the EncodedVideoSetSerializer
""" """
url = serializers.URLField(max_length=200) profile = ProfileSerializer(required=False)
file_size = serializers.IntegerField(validators=[MinValueValidator(1)]) class Meta:
bitrate = serializers.IntegerField(validators=[MinValueValidator(1)]) model = EncodedVideo
profile = ProfileSerializer() fields = (
"url",
"file_size",
"bitrate"
)
class EncodedVideoSetSerializer(serializers.Serializer): class EncodedVideoSetSerializer(serializers.ModelSerializer):
""" """
Used to serialize a list of EncodedVideo objects it's foreign key Video Object. Used to serialize a list of EncodedVideo objects it's foreign key Video Object.
""" """
edx_video_id = serializers.CharField(max_length=50) edx_video_id = serializers.CharField(max_length=50)
client_title = serializers.CharField(max_length=255)
duration = serializers.FloatField(validators=[MinValueValidator(1)])
encoded_videos = OnlyEncodedVideoSerializer() encoded_videos = OnlyEncodedVideoSerializer()
class Meta:
model = Video
fields = (
"duration",
"client_video_id"
)
...@@ -121,6 +121,7 @@ INSTALLED_APPS = ( ...@@ -121,6 +121,7 @@ INSTALLED_APPS = (
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'edxval', 'edxval',
'django_nose', 'django_nose',
'rest_framework',
# Uncomment the next line to enable the admin: # Uncomment the next line to enable the admin:
'django.contrib.admin', 'django.contrib.admin',
# Uncomment the next line to enable admin documentation: # Uncomment the next line to enable admin documentation:
......
...@@ -44,13 +44,86 @@ PROFILE_DICT_NON_LATIN = dict( ...@@ -44,13 +44,86 @@ PROFILE_DICT_NON_LATIN = dict(
height=300 height=300
) )
VIDEO_DICT_CATS = dict( VIDEO_DICT_CATS = dict(
client_title="Thunder Cats S01E01", client_video_id="Thunder Cats S01E01",
duration=111.00, duration=111.00,
edx_video_id="thisis12char-thisis7", edx_video_id="thisis12char-thisis7",
) )
VIDEO_DICT_LION = dict(
client_video_id="Lolcat",
duration=111.00,
edx_video_id="caw",
)
VIDEO_DICT_LION2 = dict(
client_video_id="Lolcat",
duration=122.00,
edx_video_id="caw",
)
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",
)
]
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",
),
]
VIDEO_DICT_NEGATIVE_DURATION = dict( VIDEO_DICT_NEGATIVE_DURATION = dict(
client_title="Thunder Cats S01E01", client_video_id="Thunder Cats S01E01",
duration=-111, duration=-111,
edx_video_id="thisis12char-thisis7", edx_video_id="thisis12char-thisis7",
)
VIDEO_DICT_INVALID_ID = dict(
client_video_id="SuperSloth",
duration=42,
edx_video_id="sloppy/sloth!!"
)
VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id="배고픈 햄스터",
duration=42,
edx_video_id="ID"
)
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘"
) )
\ No newline at end of file
...@@ -16,6 +16,9 @@ from edxval.tests import constants ...@@ -16,6 +16,9 @@ from edxval.tests import constants
class GetVideoInfoTest(TestCase): class GetVideoInfoTest(TestCase):
#TODO When upload portion is finished, do not forget to create tests for validating
#TODO regex for models. Currently, objects are created manually and validators
#TODO are not triggered.
def setUp(self): def setUp(self):
""" """
Creates EncodedVideo objects in database Creates EncodedVideo objects in database
......
...@@ -8,7 +8,8 @@ from django.test import TestCase ...@@ -8,7 +8,8 @@ from django.test import TestCase
from edxval.serializers import ( from edxval.serializers import (
OnlyEncodedVideoSerializer, OnlyEncodedVideoSerializer,
EncodedVideoSetSerializer, EncodedVideoSetSerializer,
ProfileSerializer ProfileSerializer,
VideoSerializer
) )
from edxval.models import Profile from edxval.models import Profile
from edxval.tests import constants from edxval.tests import constants
...@@ -25,25 +26,31 @@ class SerializerTests(TestCase): ...@@ -25,25 +26,31 @@ class SerializerTests(TestCase):
Profile.objects.create(**constants.PROFILE_DICT_MOBILE) Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_NON_LATIN) Profile.objects.create(**constants.PROFILE_DICT_NON_LATIN)
def test_negative_fields(self): def test_negative_fields_only_encoded_video(self):
""" """
Tests negative inputs for a serializer Tests negative inputs for OnlyEncodedSerializer
Tests negative inputs for bitrate, file_size in EncodedVideo, Tests negative inputs for bitrate, file_size in EncodedVideo
and duration in Video
""" """
a = OnlyEncodedVideoSerializer( a = OnlyEncodedVideoSerializer(
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors
self.assertEqual(a.get('bitrate')[0], self.assertEqual(a.get('bitrate')[0],
u"Ensure this value is greater than or equal to 1.") u"Ensure this value is greater than or equal to 0.")
b = OnlyEncodedVideoSerializer( b = OnlyEncodedVideoSerializer(
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE).errors data=constants.ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE).errors
self.assertEqual(b.get('file_size')[0], self.assertEqual(b.get('file_size')[0],
u"Ensure this value is greater than or equal to 1.") u"Ensure this value is greater than or equal to 0.")
def test_negative_fields_video_set(self):
"""
Tests negative inputs for EncodedVideoSetSerializer
Tests negative inputs for duration in model Video
"""
c = EncodedVideoSetSerializer( c = EncodedVideoSetSerializer(
data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors
self.assertEqual(c.get('duration')[0], self.assertEqual(c.get('duration')[0],
u"Ensure this value is greater than or equal to 1.") u"Ensure this value is greater than or equal to 0.")
def test_unicode_inputs(self): def test_unicode_inputs(self):
""" """
...@@ -51,4 +58,14 @@ class SerializerTests(TestCase): ...@@ -51,4 +58,14 @@ class SerializerTests(TestCase):
""" """
self.assertIsNotNone( self.assertIsNotNone(
ProfileSerializer(Profile.objects.get(profile_name="배고파")) ProfileSerializer(Profile.objects.get(profile_name="배고파"))
) )
\ No newline at end of file
def test_invalid_edx_video_id(self):
"""
Test the Video model regex validation for edx_video_id field
"""
error = VideoSerializer(data=constants.VIDEO_DICT_INVALID_ID).errors
message = error.get("edx_video_id")[0]
self.assertEqual(
message,
u"edx_video_id has invalid characters")
\ No newline at end of file
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from edxval.tests import constants
class VideoListTest(APITestCase):
"""
Tests the creations of Videos via POST/GET
"""
def test_post_video(self):
"""
Tests creating a new Video object via POST
"""
url = reverse('video_view')
response = self.client.post(
url, [constants.VIDEO_DICT_LION], format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_get_all_videos(self):
"""
Tests getting all Video objects
"""
url = reverse('video_view')
self.client.post(url, [constants.VIDEO_DICT_LION], format='json')
self.client.post(url, [constants.VIDEO_DICT_CATS], format='json')
videos = len(self.client.get("/edxval/video/").data)
self.assertEqual(videos, 2)
def test_post_multiple_valid_video_creation(self):
"""
Tests the creation of more than one video
"""
url = reverse('video_view')
response = self.client.post(
url, constants.VIDEO_DICT_TIGERS_BEARS, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
videos = len(self.client.get("/edxval/video/").data)
self.assertEqual(videos, 2)
def test_post_invalid_video_entry(self):
"""
Tests for invalid video entry for POST
"""
url = reverse('video_view')
response = self.client.post(url, [constants.VIDEO_DICT_INVALID_ID], format='json')
error = len(response.data)
self.assertEqual(error, 1)
def test_post_invalid_entry(self):
"""
Tests when a non list POST request is made
"""
url = reverse('video_view')
response = self.client.post(url, constants.VIDEO_DICT_CATS, format='json')
self.assertEqual(response.data, "Not a list: <type 'dict'>")
def test_post_invalid_video_dict_list(self):
"""
Tests when there are valid and invalid dicts in list
"""
url = reverse('video_view')
response = self.client.post(url, constants.VIDEO_DICT_INVALID_SET, format='json')
errors = len(response.data)
self.assertEqual(errors, 2)
videos = len(self.client.get("/edxval/video/").data)
self.assertEqual(videos, 1)
def test_post_valid_video_dict_list_duplicates(self):
"""
Tests when valid duplicate dicts are submitted in a list
"""
url = reverse('video_view')
response = self.client.post(url, constants.VIDEO_DICT_DUPLICATES, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
videos = len(self.client.get("/edxval/video/").data)
self.assertEqual(videos, 1)
def test_post_non_latin_dict(self):
"""
Tests a non-latin character input
"""
url = reverse('video_view')
response = self.client.post(url, [constants.VIDEO_DICT_NON_LATIN_ID], format='json')
errors = len(response.data)
self.assertEqual(errors, 1)
response = self.client.post(url, [constants.VIDEO_DICT_NON_LATIN_TITLE], format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_post_update_video(self):
"""
Tests video update
"""
url = reverse('video_view')
self.client.post(url, [constants.VIDEO_DICT_LION], format='json')
response = self.client.post(url, [constants.VIDEO_DICT_LION2], format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
videos = len(self.client.get("/edxval/video/").data)
self.assertEqual(videos, 1)
old_duration = constants.VIDEO_DICT_LION.get("duration")
new_duration = constants.VIDEO_DICT_LION2.get("duration")
self.assertNotEqual(new_duration, old_duration)
class VideoDetailTest(APITestCase):
"""
Tests for the VideoDetail class
"""
def test_get_video(self):
"""
Tests retrieving a particular Video Object
"""
url = reverse('video_view')
self.client.post(url, [constants.VIDEO_DICT_LION], format='json')
search = "/edxval/video/{0}".format(constants.VIDEO_DICT_LION.get("edx_video_id"))
response = self.client.get(search)
a = response.data.get("edx_video_id")
b = constants.VIDEO_DICT_LION.get("edx_video_id")
self.assertEqual(a, b)
\ No newline at end of file
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from rest_framework.urlpatterns import format_suffix_patterns
from edxval import views
# Uncomment the next two lines to enable the admin:
from django.contrib import admin from django.contrib import admin
admin.autodiscover() admin.autodiscover()
urlpatterns = patterns('', urlpatterns = patterns('',
# Examples: url(r'^edxval/video/$', views.VideoList.as_view(),
# url(r'^$', 'edxval.views.home', name='home'), name="video_view"),
# url(r'^edxval/', include('edxval.foo.urls')), url(r'^edxval/video/(?P<edx_video_id>\w+)',
views.VideoDetail.as_view(),
# Uncomment the admin/doc line below to enable admin documentation: name="video_detail_view"),
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
) )
\ No newline at end of file
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 edxval.models import Video
from edxval.serializers import VideoSerializer
class VideoList(APIView):
"""
HTTP API for 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)
class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Gets a video instance given its edx_video_id
"""
lookup_field = "edx_video_id"
queryset = Video.objects.all()
serializer_class = VideoSerializer
\ No newline at end of file
django>=1.4,<1.5 django>=1.4,<1.5
djangorestframework==2.3.5 djangorestframework==2.3.14
\ No newline at end of file \ No newline at end of file
from django.conf.urls import patterns, include, url
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
url(r'^edxval/', include('edxval.urls')),
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