Commit 8778a639 by Nimisha Asthagiri

Merge pull request #32 from edx/mobile/video_upload-val

Support for studio-based Video Pipeline.
parents e39fcbc8 19e0e23f
edx-val
=======
Note:
The naming convetion for the `profile_name` in `Profile` objects will be the
medium of the profile (a-z), an underscore, and then the quality (a-z).
Examples:
desktop_high
mobile_medium
somethingelse_low
......@@ -22,7 +22,7 @@ class CourseVideoInline(admin.TabularInline): # pylint: disable=C0111
class VideoAdmin(admin.ModelAdmin): # pylint: disable=C0111
list_display = (
'id', 'edx_video_id', 'client_video_id', 'duration'
'id', 'edx_video_id', 'client_video_id', 'duration', 'created', 'status'
)
list_display_links = ('id', 'edx_video_id')
search_fields = ('id', 'edx_video_id', 'client_video_id')
......
......@@ -51,6 +51,7 @@ class ValCannotCreateError(ValError):
"""
pass
def create_video(video_data):
"""
Called on to create Video objects in the database
......@@ -85,6 +86,7 @@ def create_video(video_data):
else:
raise ValCannotCreateError(serializer.errors)
def create_profile(profile_data):
"""
Used to create Profile objects in the database
......@@ -129,6 +131,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
{
url: api url to the video
edx_video_id: ID of the video
status: Status of the video as a string
duration: Length of video in seconds
client_video_id: client ID of video
encoded_video: a list of EncodedVideo dicts
......@@ -181,10 +184,19 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
raise ValInternalError(error_message)
return result.data # pylint: disable=E1101
def get_urls_for_profiles(edx_video_id, profiles):
"""Returns a dict mapping profiles to URLs.
"""
Returns a dict mapping profiles to URLs.
If the profiles or video is not found, urls will be blank.
Args:
edx_video_id (str): id of the video
profiles (list): list of profiles we want to search for
Returns:
profiles_to_urls (dict): A dict containing the profile to url pair
"""
profiles_to_urls = {profile: None for profile in profiles}
try:
......@@ -198,11 +210,23 @@ def get_urls_for_profiles(edx_video_id, profiles):
return profiles_to_urls
def get_url_for_profile(edx_video_id, profile):
"""
Uses get_urls_for_profile to obtain a single profile
Args:
edx_video_id (str): id of the video
profile (str): a string of the profile we are searching
Returns:
A dict containing the profile to url. The return type is the same as
get_urls_for_profiles for consistency.
"""
return get_urls_for_profiles(edx_video_id, [profile])[profile]
url = get_urls_for_profiles(edx_video_id, [profile])[profile]
return {profile: url}
def get_videos_for_course(course_id):
"""
......@@ -211,8 +235,24 @@ def get_videos_for_course(course_id):
videos = Video.objects.filter(courses__course_id=unicode(course_id))
return (VideoSerializer(video).data for video in videos)
def get_videos_for_ids(edx_video_ids):
"""
Returns an iterator of videos that match the given list of ids
Args:
edx_video_ids (list)
Returns:
A generator expression that contains the videos found
"""
videos = Video.objects.filter(edx_video_id__in=edx_video_ids)
return (VideoSerializer(video).data for video in videos)
def get_video_info_for_course_and_profile(course_id, profile_name):
"""Returns a dict mapping profiles to URLs.
"""
Returns a dict mapping profiles to URLs.
If the profiles or video is not found, urls will be blank.
"""
......
......@@ -85,7 +85,7 @@ class Migration(SchemaMigration):
models = {
'edxval.coursevideos': {
'edxval.coursevideo': {
'Meta': {'unique_together': "(('course_id', 'video'),)", 'object_name': 'CourseVideo'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
......
......@@ -24,7 +24,7 @@ class Migration(DataMigration):
).delete()
models = {
'edxval.coursevideos': {
'edxval.coursevideo': {
'Meta': {'unique_together': "(('course_id', 'video'),)", 'object_name': 'CourseVideo'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Video.created'
db.add_column('edxval_video', 'created',
self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2014, 11, 17, 0, 0), blank=True),
keep_default=False)
# Adding field 'Video.status'
db.add_column('edxval_video', 'status',
self.gf('django.db.models.fields.CharField')(default='File Complete', max_length=255, db_index=True),
keep_default=False)
# Changing field 'Video.edx_video_id'
db.alter_column('edxval_video', 'edx_video_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=100))
# Changing field 'EncodedVideo.url'
db.alter_column('edxval_encodedvideo', 'url', self.gf('django.db.models.fields.CharField')(max_length=200))
def backwards(self, orm):
# Deleting field 'Video.created'
db.delete_column('edxval_video', 'created')
# Deleting field 'Video.status'
db.delete_column('edxval_video', 'status')
# Changing field 'Video.edx_video_id'
db.alter_column('edxval_video', 'edx_video_id', self.gf('django.db.models.fields.CharField')(max_length=50, unique=True))
# Changing field 'EncodedVideo.url'
db.alter_column('edxval_encodedvideo', 'url', self.gf('django.db.models.fields.URLField')(max_length=200))
models = {
'edxval.coursevideo': {
'Meta': {'unique_together': "(('course_id', 'video'),)", 'object_name': 'CourseVideo'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'video': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'courses'", 'to': "orm['edxval.Video']"})
},
'edxval.encodedvideo': {
'Meta': {'object_name': 'EncodedVideo'},
'bitrate': ('django.db.models.fields.PositiveIntegerField', [], {}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'file_size': ('django.db.models.fields.PositiveIntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['edxval.Profile']"}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'video': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'encoded_videos'", 'to': "orm['edxval.Video']"})
},
'edxval.profile': {
'Meta': {'object_name': 'Profile'},
'extension': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'height': ('django.db.models.fields.PositiveIntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'profile_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'width': ('django.db.models.fields.PositiveIntegerField', [], {})
},
'edxval.subtitle': {
'Meta': {'object_name': 'Subtitle'},
'content': ('django.db.models.fields.TextField', [], {'default': "''"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'fmt': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'max_length': '8', 'db_index': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'video': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subtitles'", 'to': "orm['edxval.Video']"})
},
'edxval.video': {
'Meta': {'object_name': 'Video'},
'client_video_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'duration': ('django.db.models.fields.FloatField', [], {}),
'edx_video_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
}
}
complete_apps = ['edxval']
\ No newline at end of file
"""
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
When calling a serializers' .errors field, there is a priority in which the
errors are returned. This may cause a partial return of errors, starting with
the highest priority.
Example:
class Profile(models.Model)
......@@ -67,9 +68,14 @@ class Video(models.Model):
A video can have multiple formats. This model are the fields that represent
the collection of those videos that do not change across formats.
Attributes:
status: Used to keep track of the processing video as it goes through
the video pipeline, e.g., "Uploading", "File Complete"...
"""
created = models.DateTimeField(auto_now_add=True)
edx_video_id = models.CharField(
max_length=50,
max_length=100,
unique=True,
validators=[
RegexValidator(
......@@ -81,6 +87,7 @@ class Video(models.Model):
)
client_video_id = models.CharField(max_length=255, db_index=True, blank=True)
duration = models.FloatField(validators=[MinValueValidator(0)])
status = models.CharField(max_length=255, db_index=True)
def get_absolute_url(self):
"""
......@@ -107,8 +114,8 @@ class CourseVideo(models.Model):
"""
Model for the course_id associated with the video content.
Every course-semester has a unique course_id. A video can be paired with multiple
course_id's but each pair is unique together.
Every course-semester has a unique course_id. A video can be paired with
multiple course_id's but each pair is unique together.
"""
course_id = models.CharField(max_length=255)
video = models.ForeignKey(Video, related_name='courses')
......@@ -146,6 +153,10 @@ SUBTITLE_FORMATS = (
class Subtitle(models.Model):
"""
Subtitle for video
Attributes:
video: the video that the subtitles are for
fmt: the format of the subttitles file
"""
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
......
......@@ -30,4 +30,4 @@ class APIAuthTestCase(APITestCase):
username = password = 'unauthorized'
else:
username, password = self.username, self.password
print self.client.login(username=username, password=password)
self.client.login(username=username, password=password)
......@@ -45,6 +45,7 @@ VIDEO_DICT_NEGATIVE_DURATION = dict(
client_video_id="Thunder Cats S01E01",
duration=-111,
edx_video_id="thisis12char-thisis7",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -52,11 +53,13 @@ VIDEO_DICT_BEE_INVALID = dict(
client_video_id="Barking Bee",
duration=111.00,
edx_video_id="wa/sps",
status="test",
)
VIDEO_DICT_INVALID_ID = dict(
client_video_id="SuperSloth",
duration=42,
edx_video_id="sloppy/sloth!!",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -77,6 +80,7 @@ VIDEO_DICT_NON_LATIN_TITLE = dict(
client_video_id=u"배고픈 햄스터",
duration=42,
edx_video_id="ID",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -84,6 +88,7 @@ VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
duration=42,
edx_video_id="밥줘",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -140,12 +145,14 @@ Fish
VIDEO_DICT_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="super-soaker"
edx_video_id="super-soaker",
status="test",
)
VIDEO_DICT_DIFFERENT_ID_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="medium-soaker"
edx_video_id="medium-soaker",
status="test",
)
ENCODED_VIDEO_DICT_FISH_MOBILE = dict(
url="https://www.swordsingers.com",
......@@ -242,6 +249,7 @@ COMPLETE_SET_INVALID_VIDEO_FISH = dict(
client_video_id="Shallow Swordfish",
duration=122.00,
edx_video_id="super/soaker",
status="test",
encoded_videos=[
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
......@@ -259,7 +267,8 @@ Star
VIDEO_DICT_STAR = dict(
client_video_id="TWINKLE TWINKLE",
duration=122.00,
edx_video_id="little-star"
edx_video_id="little-star",
status="test",
)
ENCODED_VIDEO_DICT_STAR = dict(
url="https://www.howIwonder.com",
......@@ -317,6 +326,7 @@ VIDEO_DICT_ZEBRA = dict(
client_video_id="Zesty Zebra",
duration=111.00,
edx_video_id="zestttt",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -324,6 +334,7 @@ VIDEO_DICT_ANIMAL = dict(
client_video_id="Average Animal",
duration=111.00,
edx_video_id="mediocrity",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -331,6 +342,7 @@ VIDEO_DICT_UPDATE_ANIMAL = dict(
client_video_id="Above Average Animal",
duration=999.00,
edx_video_id="mediocrity",
status="test",
encoded_videos=[],
subtitles=[]
)
......@@ -201,6 +201,141 @@ class GetVideoInfoTest(TestCase):
)
class GetUrlsForProfileTest(TestCase):
"""
Tests the get_urls_for_profile(s) function in api.py
"""
def setUp(self):
"""
Creates EncodedVideo objects in database
"""
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
video = Video.objects.create(**constants.VIDEO_DICT_FISH)
EncodedVideo.objects.create(
video=Video.objects.get(
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_FISH.get("edx_video_id")
),
profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP
)
self.course_id = 'test-course'
CourseVideo.objects.create(video=video, course_id=self.course_id)
def test_get_urls_for_profiles(self):
"""
Tests when the profiles to the video are found
"""
profiles = ["mobile", "desktop"]
edx_video_id = constants.VIDEO_DICT_FISH['edx_video_id']
urls = api.get_urls_for_profiles(edx_video_id, profiles)
self.assertEqual(len(urls), 2)
self.assertEqual(urls["mobile"], u'http://www.meowmix.com')
self.assertEqual(urls["desktop"], u'http://www.meowmagic.com')
def test_get_urls_for_profiles_no_video(self):
"""
Tests when there is no video found.
"""
urls = api.get_urls_for_profiles("not found", ["mobile"])
self.assertEqual(urls["mobile"], None)
def test_get_urls_for_profiles_no_profiles(self):
"""
Tests when the video is found, but not hte profiles.
"""
profiles = ["not", "found"]
edx_video_id = constants.VIDEO_DICT_FISH['edx_video_id']
urls = api.get_urls_for_profiles(edx_video_id, profiles)
self.assertEqual(len(urls), 2)
self.assertEqual(urls["not"], None)
self.assertEqual(urls["found"], None)
def test_get_url_for_profile(self):
"""
Tests get_url_for_profile
"""
profile = "mobile"
edx_video_id = constants.VIDEO_DICT_FISH['edx_video_id']
urls = api.get_url_for_profile(edx_video_id, profile)
self.assertEqual(len(urls), 1)
self.assertEqual(urls["mobile"], u'http://www.meowmix.com')
class GetVideosForIds(TestCase):
"""
Tests the get_videos_for_ids function in api.py
"""
def setUp(self):
"""
Creates EncodedVideo objects in database
"""
Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
video = Video.objects.create(**constants.VIDEO_DICT_FISH)
EncodedVideo.objects.create(
video=Video.objects.get(
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_FISH.get("edx_video_id")
),
profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP
)
self.course_id = 'test-course'
CourseVideo.objects.create(video=video, course_id=self.course_id)
def test_get_videos_for_id(self):
"""
Tests retrieving videos for id
"""
edx_video_id = constants.VIDEO_DICT_FISH['edx_video_id']
videos = list(api.get_videos_for_ids([edx_video_id]))
self.assertEqual(len(videos), 1)
self.assertEqual(videos[0]['edx_video_id'], edx_video_id)
videos = list(api.get_videos_for_ids(['unknown']))
self.assertEqual(len(videos), 0)
def test_get_videos_for_ids(self):
"""
Tests retrieving videos for ids
"""
Video.objects.create(**constants.VIDEO_DICT_DIFFERENT_ID_FISH)
EncodedVideo.objects.create(
video=Video.objects.get(
edx_video_id=constants.VIDEO_DICT_DIFFERENT_ID_FISH.get("edx_video_id")
),
profile=Profile.objects.get(profile_name="mobile"),
**constants.ENCODED_VIDEO_DICT_MOBILE
)
edx_video_id = constants.VIDEO_DICT_FISH['edx_video_id']
edx_video_id_2 = constants.VIDEO_DICT_DIFFERENT_ID_FISH['edx_video_id']
videos = list(api.get_videos_for_ids([edx_video_id, edx_video_id_2]))
self.assertEqual(len(videos), 2)
def test_get_videos_for_ids_duplicates(self):
"""
Tests retrieving videos for ids when there are duplicate ids
"""
edx_video_id = constants.VIDEO_DICT_FISH['edx_video_id']
videos = list(api.get_videos_for_ids([edx_video_id, edx_video_id]))
self.assertEqual(len(videos), 1)
class GetVideoInfoTestWithHttpCalls(APIAuthTestCase):
"""
Tests for the get_info_video, using the HTTP requests to populate database
......
"""
Tests for Video Abstraction Layer models
"""
......@@ -72,7 +72,8 @@ class SerializerTests(TestCase):
message = error.get("edx_video_id")[0]
self.assertEqual(
message,
u"edx_video_id has invalid characters")
u"edx_video_id has invalid characters"
)
def test_encoded_video_set_output(self):
"""
......
......@@ -513,6 +513,7 @@ class VideoListTest(APIAuthTestCase):
"""
video = {
'edx_video_id': 'testing-youtube',
'status': 'test',
'encoded_videos': [
{
'profile': 'youtube',
......
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