Commit 64aa7637 by Christopher Lee

Merge pull request #39 from edx/clee/MA-77_auto_course_reruns

Clee/ma 77 auto course reruns
parents f4b3441f 6bba9e86
......@@ -6,4 +6,5 @@ include = edxval/*
omit =
**/__init__.py
**/tests/*
**/settings.py
\ No newline at end of file
**/settings.py
**/migrations*
......@@ -6,7 +6,7 @@ The internal API for VAL. This is not yet stable
from enum import Enum
import logging
from edxval.models import Video, EncodedVideo
from edxval.models import Video, EncodedVideo, CourseVideo
from edxval.serializers import VideoSerializer, ProfileSerializer
logger = logging.getLogger(__name__) # pylint: disable=C0103
......@@ -93,6 +93,7 @@ def create_video(video_data):
extension: 3 letter extension of video
width: horizontal pixel resolution
height: vertical pixel resolution
courses: Courses associated with this video
}
"""
serializer = VideoSerializer(data=video_data)
......@@ -119,7 +120,7 @@ def create_profile(profile_data):
}
Returns:
new_object.id (int): id of the newly created object
(int): id of the newly created object
Raises:
ValCannotCreateError: Raised if the serializer throws an error
......@@ -141,7 +142,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
edx_video_id (str): id for video content.
Returns:
result (dict): Deserialized Video Object with related field EncodedVideo
(dict): Deserialized Video Object with related field EncodedVideo
Returns all the Video object fields, and it's related EncodedVideo
objects in a list.
{
......@@ -180,7 +181,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
'client_video_id': u'The example video',
'encoded_videos': [
{
'url': u'http://www.meowmix.com',
'url': u'http://www.example.com',
'file_size': 25556,
'bitrate': 9600,
'profile': u'mobile'
......@@ -212,7 +213,7 @@ def get_urls_for_profiles(edx_video_id, profiles):
profiles (list): list of profiles we want to search for
Returns:
profiles_to_urls (dict): A dict containing the profile to url pair
(dict): A dict containing the profile to url pair
"""
profiles_to_urls = {profile: None for profile in profiles}
try:
......@@ -236,7 +237,7 @@ def get_url_for_profile(edx_video_id, profile):
profile (str): a string of the profile we are searching
Returns:
A string with the url
(str): A string with the url
"""
return get_urls_for_profiles(edx_video_id, [profile])[profile]
......@@ -277,17 +278,63 @@ def get_videos_for_ids(
return (VideoSerializer(video).data for video in videos)
def get_video_info_for_course_and_profile(course_id, profile_name):
def get_video_info_for_course_and_profiles(course_id, profiles):
"""
Returns a dict mapping profiles to URLs.
Returns a dict of edx_video_ids with a dict of requested profiles.
If the profiles or video is not found, urls will be blank.
Args:
course_id (str): id of the course
profiles (list): list of profile_names
Returns:
(dict): Returns all the profiles attached to a specific
edx_video_id
{
edx_video_id: {
'duration': length of the video in seconds,
'profiles': {
profile_name: {
'url': url of the encoding
'file_size': size of the file in bytes
},
}
},
}
Example:
Given two videos with two profiles each in course_id 'test_course':
{
u'edx_video_id_1': {
u'duration: 1111,
u'profiles': {
u'mobile': {
'url': u'http: //www.example.com/meow',
'file_size': 2222
},
u'desktop': {
'url': u'http: //www.example.com/woof',
'file_size': 4444
}
}
},
u'edx_video_id_2': {
u'duration: 2222,
u'profiles': {
u'mobile': {
'url': u'http: //www.example.com/roar',
'file_size': 6666
},
u'desktop': {
'url': u'http: //www.example.com/bzzz',
'file_size': 8888
}
}
}
}
"""
# In case someone passes in a key (VAL doesn't really understand opaque keys)
course_id = unicode(course_id)
try:
encoded_videos = EncodedVideo.objects.filter(
profile__profile_name=profile_name,
profile__profile_name__in=profiles,
video__courses__course_id=course_id
).select_related()
except Exception:
......@@ -296,11 +343,39 @@ def get_video_info_for_course_and_profile(course_id, profile_name):
raise ValInternalError(error_message)
# DRF serializers were causing extra queries for some reason...
return {
enc_vid.video.edx_video_id: {
"url": enc_vid.url,
"file_size": enc_vid.file_size,
"duration": enc_vid.video.duration,
}
for enc_vid in encoded_videos
}
return_dict = {}
for enc_vid in encoded_videos:
# Add duration to edx_video_id
return_dict.setdefault(enc_vid.video.edx_video_id, {}).update(
{
"duration": enc_vid.video.duration,
}
)
# Add profile information to edx_video_id's profiles
return_dict[enc_vid.video.edx_video_id].setdefault("profiles", {}).update(
{enc_vid.profile.profile_name: {
"url": enc_vid.url,
"file_size": enc_vid.file_size,
}}
)
return return_dict
def copy_course_videos(source_course_id, destination_course_id):
"""
Adds the destination_course_id to the videos taken from the source_course_id
Args:
source_course_id: The original course_id
destination_course_id: The new course_id where the videos will be copied
"""
if source_course_id == destination_course_id:
return
videos = Video.objects.filter(courses__course_id=unicode(source_course_id))
for video in videos:
CourseVideo.objects.get_or_create(
video=video,
course_id=destination_course_id
)
......@@ -30,13 +30,33 @@ Encoded_videos for test_api, does not have profile.
"""
ENCODED_VIDEO_DICT_MOBILE = dict(
url="http://www.meowmix.com",
file_size=4545,
bitrate=6767,
file_size=11,
bitrate=22,
)
ENCODED_VIDEO_DICT_DESKTOP = dict(
url="http://www.meowmagic.com",
file_size=1212,
bitrate=2323,
file_size=33,
bitrate=44,
)
ENCODED_VIDEO_DICT_MOBILE2 = dict(
url="http://www.woof.com",
file_size=55,
bitrate=66,
)
ENCODED_VIDEO_DICT_DESKTOP2 = dict(
url="http://www.bark.com",
file_size=77,
bitrate=88,
)
ENCODED_VIDEO_DICT_MOBILE3 = dict(
url="http://www.ssss.com",
file_size=1111,
bitrate=2222,
)
ENCODED_VIDEO_DICT_DESKTOP3 = dict(
url="http://www.hiss.com",
file_size=3333,
bitrate=4444,
)
"""
Validators
......@@ -322,6 +342,18 @@ COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
"""
Other
"""
VIDEO_DICT_TREE = dict(
client_video_id="Trees4lyfe",
duration=532.00,
edx_video_id="tree-hugger",
status="test",
)
VIDEO_DICT_PLANT = dict(
client_video_id="PlantzRule",
duration=876.00,
edx_video_id="fernmaster",
status="test",
)
VIDEO_DICT_ZEBRA = dict(
client_video_id="Zesty Zebra",
duration=111.00,
......
......@@ -14,10 +14,15 @@ from ddt import ddt, data
from edxval.models import Profile, Video, EncodedVideo, CourseVideo
from edxval import api as api
from edxval.api import SortDirection, ValCannotCreateError, VideoSortField
from edxval.api import (
SortDirection,
ValCannotCreateError,
VideoSortField,
)
from edxval.serializers import VideoSerializer
from edxval.tests import constants, APIAuthTestCase
@ddt
class CreateVideoTest(TestCase):
"""
......@@ -125,16 +130,12 @@ class GetVideoInfoTest(TestCase):
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")
),
video=video,
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")
),
video=video,
profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP
)
......@@ -269,6 +270,210 @@ class GetUrlsForProfileTest(TestCase):
self.assertEqual(url, u'http://www.meowmix.com')
class GetVideoForCourseProfiles(TestCase):
"""Tests get_video_info_for_course_and_profiles in api.py"""
def setUp(self):
"""
Creates two courses for testing
Creates two videos with 2 encoded videos for the first course, and then
2 videos with 1 encoded video for the second course.
"""
mobile_profile = Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
desktop_profile = Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
self.course_id = 'test-course'
# 1st video
video = Video.objects.create(**constants.VIDEO_DICT_FISH)
EncodedVideo.objects.create(
video=video,
profile=mobile_profile,
**constants.ENCODED_VIDEO_DICT_MOBILE
)
EncodedVideo.objects.create(
video=video,
profile=desktop_profile,
**constants.ENCODED_VIDEO_DICT_DESKTOP
)
CourseVideo.objects.create(video=video, course_id=self.course_id)
# 2nd video
video = Video.objects.create(**constants.VIDEO_DICT_STAR)
EncodedVideo.objects.create(
video=video,
profile=mobile_profile,
**constants.ENCODED_VIDEO_DICT_MOBILE2
)
EncodedVideo.objects.create(
video=video,
profile=desktop_profile,
**constants.ENCODED_VIDEO_DICT_DESKTOP2
)
CourseVideo.objects.create(video=video, course_id=self.course_id)
self.course_id2 = "test-course2"
# 3rd video different course
video = Video.objects.create(**constants.VIDEO_DICT_TREE)
EncodedVideo.objects.create(
video=video,
profile=mobile_profile,
**constants.ENCODED_VIDEO_DICT_MOBILE3
)
CourseVideo.objects.create(video=video, course_id=self.course_id2)
# 4th video different course
video = Video.objects.create(**constants.VIDEO_DICT_PLANT)
EncodedVideo.objects.create(
video=video,
profile=desktop_profile,
**constants.ENCODED_VIDEO_DICT_DESKTOP3
)
CourseVideo.objects.create(video=video, course_id=self.course_id2)
def _create_video_dict(self, video, encoding_dict):
"""
Creates a video dict object from given constants
"""
return {
video['edx_video_id']: {
"duration": video['duration'],
'profiles': {
profile_name: {
"url": encoding["url"],
"file_size": encoding["file_size"],
}
for (profile_name, encoding) in encoding_dict.iteritems()
}
}
}
def test_get_video_for_course_profiles_success_one_profile(self):
"""
Tests get_video_info_for_course_and_profiles for one profile
"""
videos = api.get_video_info_for_course_and_profiles(
self.course_id,
['mobile']
)
expected_dict = {}
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_FISH,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE
}
))
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_STAR,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE2
}))
self.assertEqual(videos, expected_dict)
def test_get_video_for_course_profiles_success_two_profiles(self):
"""
Tests get_video_info_for_course_and_profiles for two profile
"""
videos = api.get_video_info_for_course_and_profiles(
'test-course',
['mobile', 'desktop'])
expected_dict = {}
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_FISH,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE,
constants.PROFILE_DICT_DESKTOP["profile_name"]: constants.ENCODED_VIDEO_DICT_DESKTOP,
}
))
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_STAR,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE2,
constants.PROFILE_DICT_DESKTOP["profile_name"]: constants.ENCODED_VIDEO_DICT_DESKTOP2,
}
))
self.assertEqual(videos, expected_dict)
def test_get_video_for_course_profiles_no_profile(self):
"""Tests get_video_info_for_course_and_profiles with no such profile"""
videos = api.get_video_info_for_course_and_profiles(
'test-course',
['no_profile'])
self.assertEqual(len(videos), 0)
videos = api.get_video_info_for_course_and_profiles(
'test-course',
[])
self.assertEqual(len(videos), 0)
videos = api.get_video_info_for_course_and_profiles(
'test-course',
['mobile', 'no_profile'])
expected_dict = {}
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_FISH,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE
}
))
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_STAR,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE2
}
))
self.assertEqual(videos, expected_dict)
def test_get_video_for_course_profiles_video_with_one_profile(self):
"""
Tests get_video_info_for_course_and_profiles with one of two profiles
"""
videos = api.get_video_info_for_course_and_profiles(
'test-course2',
['mobile'])
expected_dict = {}
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_TREE,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE3
}
))
self.assertEqual(videos, expected_dict)
videos = api.get_video_info_for_course_and_profiles(
'test-course2',
['desktop'])
expected_dict = {}
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_PLANT,
{
constants.PROFILE_DICT_DESKTOP["profile_name"]: constants.ENCODED_VIDEO_DICT_DESKTOP3
}
))
self.assertEqual(videos, expected_dict)
def test_get_video_for_course_profiles_repeated_profile(self):
"""
Tests get_video_info_for_course_and_profiles with repeated profile
"""
videos = api.get_video_info_for_course_and_profiles(
'test-course',
['mobile', 'mobile'])
expected_dict = {}
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_FISH,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE,
}
))
expected_dict.update(self._create_video_dict(
constants.VIDEO_DICT_STAR,
{
constants.PROFILE_DICT_MOBILE["profile_name"]: constants.ENCODED_VIDEO_DICT_MOBILE2
}
))
self.assertEqual(videos, expected_dict)
class GetVideosForIds(TestCase):
"""
Tests the get_videos_for_ids function in api.py
......@@ -436,3 +641,57 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase):
with self.assertNumQueries(4):
api.get_video_info(constants.VIDEO_DICT_ZEBRA.get("edx_video_id"))
class TestCopyCourse(TestCase):
"""Tests copy_course_videos in api.py"""
def setUp(self):
"""
Creates a course with 2 videos and a course with 1 video
"""
self.course_id = 'test-course'
# 1st video
video = Video.objects.create(**constants.VIDEO_DICT_FISH)
CourseVideo.objects.create(video=video, course_id=self.course_id)
# 2nd video
video = Video.objects.create(**constants.VIDEO_DICT_STAR)
CourseVideo.objects.create(video=video, course_id=self.course_id)
self.course_id2 = "test-course2"
# 3rd video different course
video = Video.objects.create(**constants.VIDEO_DICT_TREE)
CourseVideo.objects.create(video=video, course_id=self.course_id2)
def test_successful_copy(self):
"""Tests a successful copy course"""
api.copy_course_videos('test-course', 'course-copy1')
original_videos = Video.objects.filter(courses__course_id='test-course')
copied_videos = Video.objects.filter(courses__course_id='course-copy1')
other_course = Video.objects.filter(courses__course_id='test-course2')
self.assertEqual(len(original_videos), 2)
self.assertEqual(
{original_video.edx_video_id for original_video in original_videos},
{constants.VIDEO_DICT_FISH["edx_video_id"], constants.VIDEO_DICT_STAR["edx_video_id"]}
)
self.assertTrue(set(original_videos) == set(copied_videos))
def test_same_course_ids(self):
"""
Tests when the destination course id name is the same as the source
"""
original_videos = Video.objects.filter(courses__course_id='test-course')
api.copy_course_videos('test-course', 'test-course')
copied_videos = Video.objects.filter(courses__course_id='test-course')
self.assertEqual(len(original_videos), 2)
self.assertTrue(set(original_videos) == set(copied_videos))
def test_existing_destination_course_id(self):
"""Test when the destination course id already exists"""
api.copy_course_videos('test-course', 'test-course2')
original_videos = Video.objects.filter(courses__course_id='test-course')
copied_videos = Video.objects.filter(courses__course_id='test-course2')
self.assertEqual(len(original_videos), 2)
self.assertLessEqual(set(original_videos), set(copied_videos))
self.assertEqual(len(copied_videos), 3)
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