Commit 595d4f28 by christopher lee

implemented correct identities for PUT

Assigned profile_name to be the identifier for EncodedVideos.
Tests for PUT have been added to check for expected behavior.
Attempting to PUT multiple EncodedVideos with the same profile
will return an error.

Other notes:
-url now accepts dashes
-reduced number of unique constants
-updated api.py tests with HTTP requests
-removed unused repr
parent e793792a
......@@ -13,26 +13,18 @@ class Profile(models.Model):
profile_name = models.CharField(
max_length=50,
unique=True,
)
)
extension = models.CharField(max_length=10)
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
def __repr__(self):
return (
u"Profile(profile_name={0.profile_name})"
).format(self)
def __unicode__(self):
return repr(self)
class Video(models.Model):
"""
Model for a Video group with the same content.
A video can have multiple formats. This model is the collection of those
videos with fields that do not change across formats.
A video can have multiple formats. This model are the fields that represent
the collection of those videos that do not change across formats.
"""
edx_video_id = models.CharField(
max_length=50,
......@@ -48,14 +40,6 @@ class Video(models.Model):
client_video_id = models.CharField(max_length=255, db_index=True)
duration = models.FloatField(validators=[MinValueValidator(0)])
def __repr__(self):
return (
u"Video(client_video_id={0.client_video_id}, duration={0.duration})"
).format(self)
def __unicode__(self):
return repr(self)
class CourseVideos(models.Model):
"""
......@@ -83,12 +67,3 @@ class EncodedVideo(models.Model):
profile = models.ForeignKey(Profile, related_name="+")
video = models.ForeignKey(Video, related_name="encoded_videos")
def __repr__(self):
return (
u"EncodedVideo(video={0.video.client_video_id}, "
u"profile={0.profile.profile_name})"
).format(self)
def __unicode__(self):
return repr(self)
......@@ -2,6 +2,7 @@
Serializers for Video Abstraction Layer
"""
from rest_framework import serializers
from django.core.exceptions import ValidationError
from edxval.models import Profile, Video, EncodedVideo
......@@ -31,10 +32,44 @@ class EncodedVideoSerializer(serializers.ModelSerializer):
"profile",
)
def get_identity(self, data):
"""
This hook is required for bulk update.
We need to override the default, to use the slug as the identity.
"""
return data.get('profile', None)
class VideoSerializer(serializers.HyperlinkedModelSerializer):
encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True)
encoded_videos = EncodedVideoSerializer(
many=True,
allow_add_remove=True
)
class Meta:
model = Video
lookup_field = "edx_video_id"
def restore_fields(self, data, files):
"""
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.
"""
reverted_data = {}
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']
for field_name, field in self.fields.items():
field.initialize(parent=self, field_name=field_name)
try:
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
self._errors[field_name] = list(err.messages)
return reverted_data
......@@ -23,7 +23,6 @@ ENCODED_VIDEO_DICT_MOBILE = dict(
file_size=4545,
bitrate=6767,
)
ENCODED_VIDEO_DICT_DESKTOP = dict(
url="http://www.meowmagic.com",
file_size=1212,
......@@ -38,28 +37,27 @@ VIDEO_DICT_NEGATIVE_DURATION = dict(
edx_video_id="thisis12char-thisis7",
encoded_videos=[]
)
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com",
file_size=-25556,
bitrate=9600,
)
ENCODED_VIDEO_DICT_NEGATIVE_BITRATE = dict(
url="http://www.meowmix.com",
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=[]
)
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com",
file_size=-25556,
bitrate=9600,
)
ENCODED_VIDEO_DICT_NEGATIVE_BITRATE = dict(
url="http://www.meowmix.com",
file_size=25556,
bitrate=-9600,
)
"""
Non-latin/invalid
"""
......@@ -87,30 +85,43 @@ Fish
VIDEO_DICT_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="supersoaker"
edx_video_id="super-soaker"
)
VIDEO_DICT_DIFFERENT_ID_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="medium-soaker"
)
ENCODED_VIDEO_DICT_FISH_MOBILE = dict(
url="https://www.swordsingers.com",
file_size=9000,
bitrate=42,
profile="mobile",
)
ENCODED_VIDEO_DICT_FISH_DESKTOP = dict(
url="https://www.swordsplints.com",
file_size=1234,
bitrate=4222,
profile="desktop",
)
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE = dict(
url="https://www.fishfellow.com",
file_size=1,
bitrate=1,
profile="mobile",
)
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP = dict(
url="https://www.furryfish.com",
file_size=2,
bitrate=2,
profile="desktop",
)
ENCODED_VIDEO_DICT_FISH_INVALID_PROFILE = dict(
url="https://www.swordsplints.com",
file_size=1234,
bitrate=4222,
profile=11,
profile="bird"
)
COMPLETE_SET_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
......@@ -118,7 +129,40 @@ COMPLETE_SET_FISH = dict(
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_TWO_MOBILE_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_MOBILE
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_UPDATE_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
],
**VIDEO_DICT_DIFFERENT_ID_FISH
)
COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_UPDATE_ONLY_DESKTOP_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
......@@ -126,7 +170,6 @@ COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
],
**VIDEO_DICT_FISH
)
COMPLETE_SET_INVALID_VIDEO_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
......@@ -141,8 +184,6 @@ COMPLETE_SETS_ALL_INVALID = [
COMPLETE_SET_INVALID_VIDEO_FISH,
COMPLETE_SET_INVALID_VIDEO_FISH
]
"""
Star
"""
......@@ -157,14 +198,24 @@ ENCODED_VIDEO_DICT_STAR = dict(
bitrate=42,
profile="mobile"
)
ENCODED_VIDEO_UPDATE_DICT_STAR = dict(
url="https://www.whatyouare.com",
file_size=9000,
bitrate=42,
profile="mobile"
)
COMPLETE_SET_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR
],
**VIDEO_DICT_STAR
)
COMPLETE_SET_UPDATE_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_UPDATE_DICT_STAR
],
**VIDEO_DICT_STAR
)
COMPLETE_SET_NOT_A_LIST = dict(
encoded_videos=dict(
url="https://www.howIwonder.com",
......@@ -174,7 +225,6 @@ COMPLETE_SET_NOT_A_LIST = dict(
),
**VIDEO_DICT_STAR
)
COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
encoded_videos=[
dict(
......@@ -188,12 +238,13 @@ COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
**VIDEO_DICT_STAR
)
"""
Unsorted
Other
"""
VIDEO_DICT_COAT = dict(
client_video_id="Callous Coat",
VIDEO_DICT_ZEBRA = dict(
client_video_id="Zesty Zebra",
duration=111.00,
edx_video_id="itchyjacket"
edx_video_id="zestttt",
encoded_videos=[]
)
VIDEO_DICT_ANIMAL = dict(
client_video_id="Average Animal",
......@@ -201,48 +252,9 @@ VIDEO_DICT_ANIMAL = dict(
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,
client_video_id="Above Average Animal",
duration=999.00,
edx_video_id="mediocrity",
encoded_videos=[]
)
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
]
......@@ -7,6 +7,9 @@ import mock
from django.test import TestCase
from django.db import DatabaseError
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from edxval.models import Profile, Video, EncodedVideo
from edxval import api as api
......@@ -16,26 +19,23 @@ 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
"""
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
Video.objects.create(**constants.VIDEO_DICT_COAT)
Video.objects.create(**constants.VIDEO_DICT_FISH)
EncodedVideo.objects.create(
video=Video.objects.get(
edx_video_id=constants.VIDEO_DICT_COAT.get("edx_video_id")
edx_video_id=constants.VIDEO_DICT_FISH.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_COAT.get("edx_video_id")
edx_video_id=constants.VIDEO_DICT_FISH.get("edx_video_id")
),
profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP
......@@ -45,7 +45,9 @@ class GetVideoInfoTest(TestCase):
"""
Tests for successful video request
"""
self.assertIsNotNone(api.get_video_info(constants.EDX_VIDEO_ID))
self.assertIsNotNone(api.get_video_info(
constants.VIDEO_DICT_FISH.get("edx_video_id"))
)
def test_no_such_video(self):
"""
......@@ -71,7 +73,9 @@ class GetVideoInfoTest(TestCase):
"""
mock_init.side_effect = Exception("Mock error")
with self.assertRaises(api.ValInternalError):
api.get_video_info(constants.EDX_VIDEO_ID)
api.get_video_info(
constants.VIDEO_DICT_FISH.get("edx_video_id")
)
@mock.patch.object(Video.objects, 'get')
def test_force_database_error(self, mock_get):
......@@ -80,4 +84,66 @@ class GetVideoInfoTest(TestCase):
"""
mock_get.side_effect = DatabaseError("DatabaseError")
with self.assertRaises(api.ValInternalError):
api.get_video_info(constants.EDX_VIDEO_ID)
api.get_video_info(
constants.VIDEO_DICT_FISH.get("edx_video_id")
)
class GetVideoInfoTestWithHttpCalls(APITestCase):
def setUp(self):
"""
Creates EncodedVideo objects in database with HTTP requests.
The tests are similar to the GetVideoInfoTest class. This class
is to tests that we have the same results, using a populated
database via HTTP uploads.
"""
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_get_video_found(self):
"""
Tests for successful video request
"""
self.assertIsNotNone(
api.get_video_info(
constants.COMPLETE_SET_FISH.get("edx_video_id")
)
)
def test_get_info_queries_for_two_encoded_video(self):
"""
Tests number of queries for a Video/EncodedVideo(1) pair
"""
with self.assertNumQueries(4):
api.get_video_info(constants.COMPLETE_SET_FISH.get("edx_video_id"))
def test_get_info_queries_for_one_encoded_video(self):
"""
Tests number of queries for a Video/EncodedVideo(1) pair
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_STAR, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(3):
api.get_video_info(constants.COMPLETE_SET_STAR.get("edx_video_id"))
def test_get_info_queries_for_only_video(self):
"""
Tests number of queries for a Video with no Encoded Videopair
"""
url = reverse('video-list')
response = self.client.post(
url, constants.VIDEO_DICT_ZEBRA, format='json'
)
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"))
......@@ -63,13 +63,6 @@ class SerializerTests(TestCase):
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,7 +77,7 @@ class SerializerTests(TestCase):
"""
Tests for basic structure of EncodedVideoSetSerializer
"""
video = Video.objects.create(**constants.VIDEO_DICT_COAT)
video = Video.objects.create(**constants.VIDEO_DICT_FISH)
EncodedVideo.objects.create(
video=video,
profile=Profile.objects.get(profile_name="desktop"),
......@@ -99,4 +92,4 @@ class SerializerTests(TestCase):
# 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)
self.assertDictContainsSubset(constants.VIDEO_DICT_FISH, result)
......@@ -9,7 +9,7 @@ admin.autodiscover()
urlpatterns = patterns('',
url(r'^edxval/video/$', views.VideoList.as_view(),
name="video-list"),
url(r'^edxval/video/(?P<edx_video_id>\w+)',
url(r'^edxval/video/(?P<edx_video_id>[-\w]+)',
views.VideoDetail.as_view(),
name="video-detail"),
url(r'^admin/', include(admin.site.urls)),
......
from rest_framework import generics
from edxval.models import Video
from edxval.models import Video, Profile
from edxval.serializers import (
VideoSerializer
VideoSerializer,
ProfileSerializer
)
......@@ -14,6 +15,14 @@ class VideoList(generics.ListCreateAPIView):
lookup_field = "edx_video_id"
serializer_class = VideoSerializer
class ProfileList(generics.ListCreateAPIView):
"""
GETs or POST video objects
"""
queryset = Profile.objects.all()
lookup_field = "profile_name"
serializer_class = ProfileSerializer
class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
"""
......
from django.conf.urls import patterns, include, url
from rest_framework.urlpatterns import format_suffix_patterns
from edxval import views
......
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