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):
{
edx_video_id: ID of the video
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
url: url of the video
file_size: size of the video in bytes
......@@ -82,7 +82,7 @@ def get_video_info(edx_video_id, location=None):
>>>{
>>> 'edx_video_id': u'thisis12char-thisis7',
>>> 'duration': 111.0,
>>> 'client_title': u'Thunder Cats S01E01',
>>> 'client_video_id': u'Thunder Cats S01E01',
>>> 'encoded_video': [
>>> {
>>> 'url': u'http://www.meowmix.com',
......
......@@ -3,6 +3,7 @@ Django models for videos for Video Abstraction Layer (VAL)
"""
from django.db import models
from django.core.validators import MinValueValidator, RegexValidator
class Profile(models.Model):
......@@ -30,13 +31,23 @@ class Video(models.Model):
A video can have multiple formats. This model is the collection of those
videos with fields that do not change across formats.
"""
edx_video_id = models.CharField(max_length=50, unique=True)
client_title = models.CharField(max_length=255, db_index=True)
duration = models.FloatField()
edx_video_id = models.CharField(
max_length=50,
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):
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)
def __unicode__(self):
......@@ -72,7 +83,7 @@ class EncodedVideo(models.Model):
def __repr__(self):
return (
u"EncodedVideo(video={0.video.client_title}, "
u"EncodedVideo(video={0.video.client_video_id}, "
u"profile={0.profile.profile_name})"
).format(self)
......
......@@ -2,16 +2,37 @@
Serializers for Video Abstraction Layer
"""
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):
edx_video_id = serializers.CharField(required=True, max_length=50)
duration = serializers.FloatField()
client_title = serializers.CharField(max_length=255)
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:
......@@ -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)
file_size = serializers.IntegerField(validators=[MinValueValidator(1)])
bitrate = serializers.IntegerField(validators=[MinValueValidator(1)])
profile = ProfileSerializer()
profile = ProfileSerializer(required=False)
class Meta:
model = EncodedVideo
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.
"""
edx_video_id = serializers.CharField(max_length=50)
client_title = serializers.CharField(max_length=255)
duration = serializers.FloatField(validators=[MinValueValidator(1)])
encoded_videos = OnlyEncodedVideoSerializer()
class Meta:
model = Video
fields = (
"duration",
"client_video_id"
)
......@@ -121,6 +121,7 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'edxval',
'django_nose',
'rest_framework',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
......
......@@ -44,13 +44,86 @@ PROFILE_DICT_NON_LATIN = dict(
height=300
)
VIDEO_DICT_CATS = dict(
client_title="Thunder Cats S01E01",
client_video_id="Thunder Cats S01E01",
duration=111.00,
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(
client_title="Thunder Cats S01E01",
client_video_id="Thunder Cats S01E01",
duration=-111,
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
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):
"""
Creates EncodedVideo objects in database
......
......@@ -8,7 +8,8 @@ from django.test import TestCase
from edxval.serializers import (
OnlyEncodedVideoSerializer,
EncodedVideoSetSerializer,
ProfileSerializer
ProfileSerializer,
VideoSerializer
)
from edxval.models import Profile
from edxval.tests import constants
......@@ -25,25 +26,31 @@ class SerializerTests(TestCase):
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
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,
and duration in Video
Tests negative inputs for bitrate, file_size in EncodedVideo
"""
a = OnlyEncodedVideoSerializer(
data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors
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(
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 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(
data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors
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):
"""
......@@ -51,4 +58,14 @@ class SerializerTests(TestCase):
"""
self.assertIsNotNone(
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 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
admin.autodiscover()
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'edxval.views.home', name='home'),
# url(r'^edxval/', include('edxval.foo.urls')),
# Uncomment the admin/doc line below to enable admin documentation:
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
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)
\ 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
djangorestframework==2.3.5
\ No newline at end of file
djangorestframework==2.3.14
\ 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