Commit 5af8bb10 by christopher lee

added create_video/profile to api.py for populating database

create_video and create_profile  will be used to populate database
for internal (not via HTTP requests) use.

Other notes:
-profile_name now has regex validator
-ddt==0.8.0
-100 percent coverage
parent d22f37b5
# pylint: disable=E1101
# -*- coding: utf-8 -*-
"""
The internal API for VAL
......@@ -5,9 +6,9 @@ The internal API for VAL
import logging
from edxval.models import Video
from edxval.serializers import VideoSerializer
from edxval.serializers import VideoSerializer, ProfileSerializer
logger = logging.getLogger(__name__) # pylint: disable=C0103
logger = logging.getLogger(__name__) # pylint: disable=C0103
class ValError(Exception):
......@@ -44,7 +45,76 @@ class ValVideoNotFoundError(ValError):
pass
def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
class ValCannotCreateError(ValError):
"""
This error is raised when an object cannot be created
"""
pass
def create_video(video_data):
"""
Called on to create Video objects in the database
create_video is used to create Video objects whose children are EncodedVideo
objects which are linked to Profile objects. This is an alternative to the HTTP
requests so it can be used internally. The VideoSerializer is used to
deserialize this object. If there are duplicate profile_names, the entire
creation will be rejected. If the profile is not found in the database, the
video will not be created.
Args:
data (dict):
{
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
encoded_video: a list of EncodedVideo dicts
url: url of the video
file_size: size of the video in bytes
profile: a dict of encoding details
profile_name: ID of the profile
extension: 3 letter extension of video
width: horizontal pixel resolution
height: vertical pixel resolution
}
"""
serializer = VideoSerializer(data=video_data)
if serializer.is_valid():
serializer.save()
return video_data.get("edx_video_id")
else:
raise ValCannotCreateError(serializer.errors)
def create_profile(profile_data):
"""
Used to create Profile objects in the database
A profile needs to exists before an EncodedVideo object can be created.
Args:
data (dict):
{
profile_name: ID of the profile
extension: 3 letter extension of video
width: horizontal pixel resolution
height: vertical pixel resolution
}
Returns:
new_object.id (int): id of the newly created object
Raises:
ValCannotCreateError: Raised if the serializer throws an error
"""
serializer = ProfileSerializer(data=profile_data)
if serializer.is_valid():
serializer.save()
return profile_data.get("profile_name")
else:
raise ValCannotCreateError(serializer.errors)
def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
"""
Retrieves all encoded videos of a video found with given video edx_video_id
......@@ -80,11 +150,11 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
>>> get_video_info("example")
Returns (dict):
{
'url' : '/edxval/video/example'
'url' : '/edxval/video/example',
'edx_video_id': u'example',
'duration': 111.0,
'client_video_id': u'The example video',
'encoded_video': [
'encoded_videos': [
{
'url': u'http://www.meowmix.com',
'file_size': 25556,
......
"""
Django models for videos for Video Abstraction Layer (VAL)
When calling a serializers' .errors function for objects, there is an
order in which the errors are returned. This may cause a partial return of errors
Example:
class Profile(models.Model)
profile_name = models.CharField(
max_length=50,
unique=True,
validators=[
RegexValidator(
regex=r'^[a-zA-Z0-9\-]*$',
message='profile_name has invalid characters',
code='invalid profile_name'
),
]
)
extension = models.CharField(max_length=10)
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
Missing a field, having an input type (expected an int, not a str),
nested serialization errors, or any similar errors will be returned by
themselves. After these are resolved, errors such as a negative height, or
invalid profile_name will be returned.
"""
from django.db import models
from django.core.validators import MinValueValidator, RegexValidator
url_regex = r'^[a-zA-Z0-9\-]*$'
class Profile(models.Model):
"""
Details for pre-defined encoding format
The profile_name has a regex validator because in case this field will be
used in a url.
"""
profile_name = models.CharField(
max_length=50,
unique=True,
validators=[
RegexValidator(
regex=url_regex,
message='profile_name has invalid characters',
code='invalid profile_name'
),
]
)
extension = models.CharField(max_length=10)
width = models.PositiveIntegerField()
......@@ -31,7 +69,7 @@ class Video(models.Model):
unique=True,
validators=[
RegexValidator(
regex=r'^[a-zA-Z0-9\-]*$',
regex=url_regex,
message='edx_video_id has invalid characters',
code='invalid edx_video_id'
),
......@@ -51,7 +89,7 @@ class CourseVideos(models.Model):
course_id = models.CharField(max_length=255)
video = models.ForeignKey(Video)
class Meta: # pylint: disable=C1001
class Meta:
"""
course_id is listed first in this composite index
"""
......
"""
Serializers for Video Abstraction Layer
Serialization is usually sent through the VideoSerializer which uses the
EncodedVideoSerializer which uses the profile_name as it's profile field.
"""
from rest_framework import serializers
from django.core.exceptions import ValidationError
......@@ -11,7 +14,7 @@ class ProfileSerializer(serializers.ModelSerializer):
"""
Serializer for Profile object.
"""
class Meta: # pylint: disable=C1001, C0111
class Meta: # pylint: disable= C0111
model = Profile
fields = (
"profile_name",
......@@ -29,7 +32,7 @@ class EncodedVideoSerializer(serializers.ModelSerializer):
"""
profile = serializers.SlugRelatedField(slug_field="profile_name")
class Meta: # pylint: disable=C1001, C0111
class Meta: # pylint: disable= C0111
model = EncodedVideo
fields = (
"created",
......@@ -56,12 +59,14 @@ class VideoSerializer(serializers.HyperlinkedModelSerializer):
"""
encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True)
class Meta: # pylint: disable=C1001,C0111
class Meta: # pylint: disable=C0111
model = Video
lookup_field = "edx_video_id"
def restore_fields(self, data, files):
"""
Overridden function used to check against duplicate profile names.
Converts a dictionary of data into a dictionary of deserialized fields. Also
checks if there are duplicate profile_name(s). If there is, the deserialization
is rejected.
......@@ -71,10 +76,14 @@ class VideoSerializer(serializers.HyperlinkedModelSerializer):
if data is not None and not isinstance(data, dict):
self._errors['non_field_errors'] = ['Invalid data']
return None
profiles = [ev["profile"] for ev in data.get("encoded_videos", [])]
if len(profiles) != len(set(profiles)):
self._errors['non_field_errors'] = ['Invalid data: duplicate profiles']
try:
profiles = [ev["profile"] for ev in data.get("encoded_videos", [])]
if len(profiles) != len(set(profiles)):
self._errors['non_field_errors'] = ['Invalid data: duplicate profiles']
except KeyError:
raise ValidationError("profile required for deserializing")
except TypeError:
raise ValidationError("profile field needs to be a profile_name (str)")
for field_name, field in self.fields.items():
field.initialize(parent=self, field_name=field_name)
......
# pylint: disable=E1103, W0105
# -*- coding: utf-8 -*-
"""
Constants used for tests.
......@@ -82,6 +83,34 @@ PROFILE_DICT_NON_LATIN = dict(
width=100,
height=300
)
PROFILE_DICT_INVALID_NAME = dict(
profile_name="lo/lol",
extension="mew",
width=100,
height=300
)
PROFILE_DICT_NEGATIVE_WIDTH = dict(
profile_name="mobile",
extension="mew",
width=-100,
height=300
)
PROFILE_DICT_NEGATIVE_HEIGHT = dict(
profile_name="mobile",
extension="mew",
width=100,
height=-300
)
PROFILE_DICT_MISSING_EXTENSION = dict(
profile_name="mobile",
width=100,
height=300
)
PROFILE_DICT_MANY_INVALID = dict(
profile_name="hh/ff",
width=-100,
height="lol",
)
"""
Fish
"""
......
......@@ -8,18 +8,114 @@ import mock
from django.test import TestCase
from django.db import DatabaseError
from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError
from rest_framework import status
from rest_framework.test import APITestCase
from ddt import ddt, data
from edxval.models import Profile, Video, EncodedVideo
from edxval import api as api
from edxval.api import ValCannotCreateError
from edxval.serializers import VideoSerializer
from edxval.tests import constants
@ddt
class CreateVideoTest(TestCase):
"""
Tests the create_video function in api.py.
This function requires that a Profile object exist.
"""
def setUp(self):
"""
Creation of Profile objects that will be used to test video creation
"""
api.create_profile(constants.PROFILE_DICT_DESKTOP)
api.create_profile(constants.PROFILE_DICT_MOBILE)
def test_create_video(self):
"""
Tests the creation of a video
"""
video_data = dict(
encoded_videos=[
constants.ENCODED_VIDEO_DICT_FISH_MOBILE
],
**constants.VIDEO_DICT_FISH
)
result = api.create_video(video_data)
videos = Video.objects.all()
self.assertEqual(len(videos), 1)
self.assertEqual("super-soaker", result)
@data(
constants.VIDEO_DICT_FISH,
constants.VIDEO_DICT_NEGATIVE_DURATION,
constants.VIDEO_DICT_INVALID_ID
)
def test_create_invalid_video(self, data): # pylint: disable=W0621
"""
Tests the creation of a video with invalid data
"""
with self.assertRaises(ValCannotCreateError):
api.create_video(data)
def test_invalid_profile(self):
"""
Tests inputting bad profile type
"""
video_data = dict(
encoded_videos=[
dict(
profile = constants.PROFILE_DICT_MOBILE,
**constants.ENCODED_VIDEO_DICT_MOBILE
)
],
**constants.VIDEO_DICT_FISH
)
with self.assertRaises(ValidationError):
api.create_video(video_data)
@ddt
class CreateProfileTest(TestCase):
"""
Tests the create_profile function in the api.py
"""
def test_create_profile(self):
"""
Tests the creation of a profile
"""
result = api.create_profile(constants.PROFILE_DICT_DESKTOP)
profiles = Profile.objects.all()
self.assertEqual(len(profiles), 1)
self.assertEqual(
profiles[0].profile_name,
constants.PROFILE_DICT_DESKTOP.get('profile_name')
)
self.assertEqual(len(profiles), 1)
self.assertEqual("desktop", result)
@data(
constants.PROFILE_DICT_NEGATIVE_WIDTH,
constants.PROFILE_DICT_NEGATIVE_HEIGHT,
constants.PROFILE_DICT_MISSING_EXTENSION,
constants.PROFILE_DICT_MANY_INVALID,
constants.PROFILE_DICT_INVALID_NAME,
)
def test_invalid_create_profile(self, data): # pylint: disable=W0621
"""
Tests the creation of invalid profile data
"""
with self.assertRaises(ValCannotCreateError):
api.create_profile(data)
class GetVideoInfoTest(TestCase):
"""
Tests for our get_video_indo function in api.py
Tests for our get_video_info function in api.py
"""
def setUp(self):
......@@ -155,3 +251,4 @@ class GetVideoInfoTestWithHttpCalls(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(2):
api.get_video_info(constants.VIDEO_DICT_ZEBRA.get("edx_video_id"))
# pylint: disable=E1101
# -*- coding: utf-8 -*-
"""
Tests the serializers for the Video Abstraction Layer
......@@ -9,6 +10,7 @@ from edxval.serializers import (
EncodedVideoSerializer,
ProfileSerializer,
VideoSerializer,
ValidationError,
)
from edxval.models import Profile, Video, EncodedVideo
from edxval.tests import constants
......@@ -92,3 +94,29 @@ class SerializerTests(TestCase):
self.assertEqual(len(result.get("encoded_videos")), 2)
# Check for original Video data
self.assertDictContainsSubset(constants.VIDEO_DICT_FISH, result)
def test_no_profile_validation(self):
"""
Tests when there are no profiles to validation when deserializing
"""
data = dict(
encoded_videos=[
constants.ENCODED_VIDEO_DICT_MOBILE
],
**constants.VIDEO_DICT_FISH
)
serializer = VideoSerializer(data=data)
with self.assertRaises(ValidationError):
serializer.is_valid()
def test_wrong_input_type(self):
"""
Tests an non dict input in the VideoSerializer
"""
data = "hello"
serializer = VideoSerializer(data=data)
self.assertEqual(
serializer.errors.get("non_field_errors")[0],
"Invalid data"
)
......@@ -2,7 +2,7 @@
Url file for django app edxval.
"""
from django.conf.urls import patterns, include, url
from django.conf.urls import patterns, url
from edxval import views
......
......@@ -19,6 +19,7 @@ class VideoList(generics.ListCreateAPIView):
lookup_field = "edx_video_id"
serializer_class = VideoSerializer
class ProfileList(generics.ListCreateAPIView):
"""
GETs or POST video objects
......
......@@ -3,3 +3,4 @@ coverage==3.7.1
mock==1.0.1
django-debug-toolbar==1.2.1
pylint==1.3.0
ddt==0.8.0
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