Commit 37a0874c by Greg Price

Add XML export and import API functions

These will be used to support export/import of courses containing video
modules that rely on VAL being populated. This does not allow updating
a video (including adding encodings) via import.

JIRA: MA-110
parent 72f76ecf
...@@ -5,6 +5,7 @@ The internal API for VAL. This is not yet stable ...@@ -5,6 +5,7 @@ The internal API for VAL. This is not yet stable
""" """
import logging import logging
from lxml.etree import Element, SubElement
from enum import Enum from enum import Enum
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
...@@ -123,12 +124,28 @@ def create_profile(profile_name): ...@@ -123,12 +124,28 @@ def create_profile(profile_name):
raise ValCannotCreateError(err.message_dict) raise ValCannotCreateError(err.message_dict)
def get_video_info(edx_video_id, location=None): # pylint: disable=W0613 def _get_video(edx_video_id):
"""
Get a Video instance, prefetching encoded video and course information.
Raises ValVideoNotFoundError if the video cannot be retrieved.
"""
try:
return Video.objects.prefetch_related("encoded_videos", "courses").get(edx_video_id=edx_video_id)
except Video.DoesNotExist:
error_message = u"Video not found for edx_video_id: {0}".format(edx_video_id)
raise ValVideoNotFoundError(error_message)
except Exception:
error_message = u"Could not get edx_video_id: {0}".format(edx_video_id)
logger.exception(error_message)
raise ValInternalError(error_message)
def get_video_info(edx_video_id):
""" """
Retrieves all encoded videos of a video found with given video edx_video_id Retrieves all encoded videos of a video found with given video edx_video_id
Args: Args:
location (str): geographic locations used determine CDN
edx_video_id (str): id for video content. edx_video_id (str): id for video content.
Returns: Returns:
...@@ -175,17 +192,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613 ...@@ -175,17 +192,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
] ]
} }
""" """
try: return VideoSerializer(_get_video(edx_video_id)).data
video = Video.objects.prefetch_related("encoded_videos", "courses").get(edx_video_id=edx_video_id)
result = VideoSerializer(video)
except Video.DoesNotExist:
error_message = u"Video not found for edx_video_id: {0}".format(edx_video_id)
raise ValVideoNotFoundError(error_message)
except Exception:
error_message = u"Could not get edx_video_id: {0}".format(edx_video_id)
logger.exception(error_message)
raise ValInternalError(error_message)
return result.data # pylint: disable=E1101
def get_urls_for_profiles(edx_video_id, profiles): def get_urls_for_profiles(edx_video_id, profiles):
...@@ -365,3 +372,96 @@ def copy_course_videos(source_course_id, destination_course_id): ...@@ -365,3 +372,96 @@ def copy_course_videos(source_course_id, destination_course_id):
video=video, video=video,
course_id=destination_course_id course_id=destination_course_id
) )
def export_to_xml(edx_video_id):
"""
Exports data about the given edx_video_id into the given xml object.
Args:
edx_video_id (str): The ID of the video to export
Returns:
An lxml video_asset element containing export data
Raises:
ValVideoNotFoundError: if the video does not exist
"""
video = _get_video(edx_video_id)
video_el = Element(
'video_asset',
attrib={
'client_video_id': video.client_video_id,
'duration': unicode(video.duration),
}
)
for encoded_video in video.encoded_videos.all():
SubElement(
video_el,
'encoded_video',
{
name: unicode(getattr(encoded_video, name))
for name in ['profile', 'url', 'file_size', 'bitrate']
}
)
# Note: we are *not* exporting Subtitle data since it is not currently updated by VEDA or used
# by LMS/Studio.
return video_el
def import_from_xml(xml, edx_video_id, course_id=None):
"""
Imports data from a video_asset element about the given edx_video_id.
If the edx_video_id already exists, then no changes are made. If an unknown
profile is referenced by an encoded video, that encoding will be ignored.
Args:
xml: An lxml video_asset element containing import data
edx_video_id (str): The ID for the video content
course_id (str): The ID of a course to associate the video with
(optional)
Raises:
ValCannotCreateError: if there is an error importing the video
"""
if xml.tag != 'video_asset':
raise ValCannotCreateError('Invalid XML')
if Video.objects.filter(edx_video_id=edx_video_id).exists():
logger.info(
"edx_video_id '%s' present in course '%s' not imported because it exists in VAL.",
edx_video_id,
course_id,
)
return
data = {
'edx_video_id': edx_video_id,
'client_video_id': xml.get('client_video_id'),
'duration': xml.get('duration'),
'status': 'imported',
'encoded_videos': [],
'courses': [],
}
for encoded_video_el in xml.iterfind('encoded_video'):
profile_name = encoded_video_el.get('profile')
try:
Profile.objects.get(profile_name=profile_name)
except Profile.DoesNotExist:
logger.info(
"Imported edx_video_id '%s' contains unknown profile '%s'.",
edx_video_id,
profile_name
)
continue
data['encoded_videos'].append({
'profile': profile_name,
'url': encoded_video_el.get('url'),
'file_size': encoded_video_el.get('file_size'),
'bitrate': encoded_video_el.get('bitrate'),
})
if course_id:
data['courses'].append(course_id)
create_video(data)
...@@ -79,8 +79,9 @@ class CourseSerializer(serializers.RelatedField): ...@@ -79,8 +79,9 @@ class CourseSerializer(serializers.RelatedField):
def from_native(self, data): def from_native(self, data):
if data: if data:
return CourseVideo(course_id=data) course_video = CourseVideo(course_id=data)
course_video.full_clean(exclude=["video"])
return course_video
class VideoSerializer(serializers.ModelSerializer): class VideoSerializer(serializers.ModelSerializer):
""" """
......
...@@ -79,6 +79,22 @@ class SerializerTests(TestCase): ...@@ -79,6 +79,22 @@ class SerializerTests(TestCase):
u"edx_video_id has invalid characters" u"edx_video_id has invalid characters"
) )
def test_invalid_course_id(self):
errors = VideoSerializer(
data={
"edx_video_id": "dummy",
"client_video_id": "dummy",
"duration": 0,
"status": "dummy",
"encoded_videos": [],
"courses": ["x" * 300],
}
).errors
self.assertEqual(
errors,
{"courses": ["Ensure this value has at most 255 characters (it has 300)."]}
)
def test_encoded_video_set_output(self): def test_encoded_video_set_output(self):
""" """
Tests for basic structure of EncodedVideoSetSerializer Tests for basic structure of EncodedVideoSetSerializer
......
django>=1.4,<1.5 django>=1.4,<1.5
djangorestframework<2.4 djangorestframework<2.4
enum34==1.0.4 enum34==1.0.4
lxml==3.3.6
South==1.0.1 South==1.0.1
-e git+https://github.com/edx/django-oauth2-provider.git@0.2.7-fork-edx-1#egg=django-oauth2-provider -e git+https://github.com/edx/django-oauth2-provider.git@0.2.7-fork-edx-1#egg=django-oauth2-provider
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