Commit c860f790 by Greg Price

Merge pull request #7508 from edx/gprice/export-import-val

Include VAL data in video module export/import
parents e394c181 cfcb3048
......@@ -40,20 +40,7 @@ class VideoUploadTestMixin(object):
"course_video_upload_token": self.test_token,
}
self.save_course()
self.profiles = [
{
"profile_name": "profile1",
"extension": "mp4",
"width": 640,
"height": 480,
},
{
"profile_name": "profile2",
"extension": "mp4",
"width": 1920,
"height": 1080,
},
]
self.profiles = ["profile1", "profile2"]
self.previous_uploads = [
{
"edx_video_id": "test1",
......
......@@ -15,20 +15,21 @@ the course, section, subsection, unit, etc.
import unittest
import datetime
from uuid import uuid4
from mock import Mock, patch
from . import LogicTest
from lxml import etree
from mock import ANY, Mock, patch
from django.conf import settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.video_module import VideoDescriptor, create_youtube_string, get_video_from_cdn
from .test_import import DummySystem
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.tests import get_test_descriptor_system
from xmodule.video_module import VideoDescriptor, create_youtube_string, get_video_from_cdn
from xmodule.video_module.transcripts_utils import download_youtube_subs, save_to_store
from django.conf import settings
from . import LogicTest
from .test_import import DummySystem
SRT_FILEDATA = '''
0
......@@ -89,6 +90,19 @@ def instantiate_descriptor(**field_data):
)
# Because of the way xmodule.video_module.video_module imports edxval.api, we
# must mock the entire module, which requires making mock exception classes.
class _MockValVideoNotFoundError(Exception):
"""Mock ValVideoNotFoundError exception"""
pass
class _MockValCannotCreateError(Exception):
"""Mock ValCannotCreateError exception"""
pass
class VideoModuleTest(LogicTest):
"""Logic tests for Video Xmodule."""
descriptor_class = VideoDescriptor
......@@ -176,6 +190,21 @@ class VideoDescriptorTestBase(unittest.TestCase):
super(VideoDescriptorTestBase, self).setUp()
self.descriptor = instantiate_descriptor()
def assertXmlEqual(self, expected, xml):
"""
Assert that the given XML fragments have the same attributes, text, and
(recursively) children
"""
def get_child_tags(elem):
"""Extract the list of tag names for children of elem"""
return [child.tag for child in elem]
for attr in ['tag', 'attrib', 'text', 'tail']:
self.assertEqual(getattr(expected, attr), getattr(xml, attr))
self.assertEqual(get_child_tags(expected), get_child_tags(xml))
for left, right in zip(expected, xml):
self.assertXmlEqual(left, right)
class TestCreateYoutubeString(VideoDescriptorTestBase):
"""
......@@ -522,21 +551,61 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'data': ''
})
@patch('xmodule.video_module.video_module.edxval_api')
def test_import_val_data(self, mock_val_api):
def mock_val_import(xml, edx_video_id):
"""Mock edxval.api.import_from_xml"""
self.assertEqual(xml.tag, 'video_asset')
self.assertEqual(dict(xml.items()), {'mock_attr': ''})
self.assertEqual(edx_video_id, 'test_edx_video_id')
mock_val_api.import_from_xml = Mock(wraps=mock_val_import)
module_system = DummySystem(load_error_modules=True)
# import new edx_video_id
xml_data = """
<video edx_video_id="test_edx_video_id">
<video_asset mock_attr=""/>
</video>
"""
video = VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock())
self.assert_attributes_equal(video, {'edx_video_id': 'test_edx_video_id'})
mock_val_api.import_from_xml.assert_called_once_with(ANY, 'test_edx_video_id')
@patch('xmodule.video_module.video_module.edxval_api')
def test_import_val_data_invalid(self, mock_val_api):
mock_val_api.ValCannotCreateError = _MockValCannotCreateError
mock_val_api.import_from_xml = Mock(side_effect=mock_val_api.ValCannotCreateError)
module_system = DummySystem(load_error_modules=True)
# Negative duration is invalid
xml_data = """
<video edx_video_id="test_edx_video_id">
<video_asset client_video_id="test_client_video_id" duration="-1"/>
</video>
"""
with self.assertRaises(mock_val_api.ValCannotCreateError):
VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock())
class VideoExportTestCase(VideoDescriptorTestBase):
"""
Make sure that VideoDescriptor can export itself to XML correctly.
"""
def assertXmlEqual(self, expected, xml):
for attr in ['tag', 'attrib', 'text', 'tail']:
self.assertEqual(getattr(expected, attr), getattr(xml, attr))
for left, right in zip(expected, xml):
self.assertXmlEqual(left, right)
def test_export_to_xml(self):
@patch('xmodule.video_module.video_module.edxval_api')
def test_export_to_xml(self, mock_val_api):
"""
Test that we write the correct XML on export.
"""
def mock_val_export(edx_video_id):
"""Mock edxval.api.export_to_xml"""
return etree.Element( # pylint:disable=no-member
'video_asset',
attrib={'export_edx_video_id': edx_video_id}
)
mock_val_api.export_to_xml = mock_val_export
self.descriptor.youtube_id_0_75 = 'izygArpw-Qo'
self.descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
self.descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
......@@ -550,6 +619,7 @@ class VideoExportTestCase(VideoDescriptorTestBase):
self.descriptor.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
self.descriptor.download_video = True
self.descriptor.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
self.descriptor.edx_video_id = 'test_edx_video_id'
xml = self.descriptor.definition_to_xml(None) # We don't use the `resource_fs` parameter
parser = etree.XMLParser(remove_blank_text=True)
......@@ -561,11 +631,25 @@ class VideoExportTestCase(VideoDescriptorTestBase):
<handout src="http://www.example.com/handout"/>
<transcript language="ge" src="german_translation.srt" />
<transcript language="ua" src="ukrainian_translation.srt" />
<video_asset export_edx_video_id="test_edx_video_id"/>
</video>
'''
expected = etree.XML(xml_string, parser=parser)
self.assertXmlEqual(expected, xml)
@patch('xmodule.video_module.video_module.edxval_api')
def test_export_to_xml_val_error(self, mock_val_api):
# Export should succeed without VAL data if video does not exist
mock_val_api.ValVideoNotFoundError = _MockValVideoNotFoundError
mock_val_api.export_to_xml = Mock(side_effect=mock_val_api.ValVideoNotFoundError)
self.descriptor.edx_video_id = 'test_edx_video_id'
xml = self.descriptor.definition_to_xml(None)
parser = etree.XMLParser(remove_blank_text=True)
xml_string = '<video url_name="SampleProblem" download_video="false"/>'
expected = etree.XML(xml_string, parser=parser)
self.assertXmlEqual(expected, xml)
def test_export_to_xml_empty_end_time(self):
"""
Test that we write the correct XML on export.
......
......@@ -404,8 +404,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
xml_data: A string of xml that will be translated into data and children for
this module
system: A DescriptorSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
id_generator is used to generate course-specific urls and identifiers
"""
xml_object = etree.fromstring(xml_data)
url_name = xml_object.get('url_name', xml_object.get('slug'))
......@@ -478,6 +477,12 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
ele.set('src', self.transcripts[transcript_language])
xml.append(ele)
if self.edx_video_id and edxval_api:
try:
xml.append(edxval_api.export_to_xml(self.edx_video_id))
except edxval_api.ValVideoNotFoundError:
pass
return xml
def get_context(self):
......@@ -621,6 +626,15 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
if 'download_track' not in field_data and track is not None:
field_data['download_track'] = True
video_asset_elem = xml.find('video_asset')
if (
edxval_api and
video_asset_elem is not None and
'edx_video_id' in field_data
):
# Allow ValCannotCreateError to escape
edxval_api.import_from_xml(video_asset_elem, field_data['edx_video_id'])
return field_data
def index_dictionary(self):
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -396,7 +396,7 @@ CREATE TABLE `auth_permission` (
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
KEY `auth_permission_e4470c6e` (`content_type_id`),
CONSTRAINT `content_type_id_refs_id_728de91f` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=499 DEFAULT CHARSET=utf8;
) ENGINE=InnoDB AUTO_INCREMENT=511 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `auth_registration`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
......@@ -623,6 +623,20 @@ CREATE TABLE `certificates_certificategenerationcoursesetting` (
KEY `certificates_certificategenerationcoursesetting_b4b47e7a` (`course_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `certificates_certificatehtmlviewconfiguration`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `certificates_certificatehtmlviewconfiguration` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`change_date` datetime NOT NULL,
`changed_by_id` int(11) DEFAULT NULL,
`enabled` tinyint(1) NOT NULL,
`configuration` longtext NOT NULL,
PRIMARY KEY (`id`),
KEY `certificates_certificatehtmlviewconfiguration_16905482` (`changed_by_id`),
CONSTRAINT `changed_by_id_refs_id_8584db17` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `certificates_certificatewhitelist`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......@@ -772,6 +786,31 @@ CREATE TABLE `course_creators_coursecreator` (
CONSTRAINT `user_id_refs_id_6a0e6044` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `course_groups_coursecohort`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `course_groups_coursecohort` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course_user_group_id` int(11) NOT NULL,
`assignment_type` varchar(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `course_user_group_id` (`course_user_group_id`),
CONSTRAINT `course_user_group_id_refs_id_8febc00f` FOREIGN KEY (`course_user_group_id`) REFERENCES `course_groups_courseusergroup` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `course_groups_coursecohortssettings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `course_groups_coursecohortssettings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`is_cohorted` tinyint(1) NOT NULL,
`course_id` varchar(255) NOT NULL,
`cohorted_discussions` longtext,
`always_cohort_inline_discussions` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `course_id` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `course_groups_courseusergroup`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......@@ -1031,8 +1070,8 @@ CREATE TABLE `django_admin_log` (
PRIMARY KEY (`id`),
KEY `django_admin_log_fbfc09f1` (`user_id`),
KEY `django_admin_log_e4470c6e` (`content_type_id`),
CONSTRAINT `user_id_refs_id_c8665aa` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
CONSTRAINT `content_type_id_refs_id_288599e6` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
CONSTRAINT `content_type_id_refs_id_288599e6` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`),
CONSTRAINT `user_id_refs_id_c8665aa` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `django_comment_client_permission`;
......@@ -1094,7 +1133,7 @@ CREATE TABLE `django_content_type` (
`model` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `app_label` (`app_label`,`model`)
) ENGINE=InnoDB AUTO_INCREMENT=166 DEFAULT CHARSET=utf8;
) ENGINE=InnoDB AUTO_INCREMENT=170 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `django_openid_auth_association`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
......@@ -1294,9 +1333,9 @@ DROP TABLE IF EXISTS `edxval_profile`;
CREATE TABLE `edxval_profile` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`profile_name` varchar(50) NOT NULL,
`extension` varchar(10) NOT NULL,
`width` int(10) unsigned NOT NULL,
`height` int(10) unsigned NOT NULL,
`extension` varchar(10) DEFAULT 'mp4',
`width` int(10) unsigned DEFAULT '1',
`height` int(10) unsigned DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `profile_name` (`profile_name`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
......@@ -1650,6 +1689,20 @@ CREATE TABLE `milestones_usermilestone` (
CONSTRAINT `milestone_id_refs_id_af7fa460` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `mobile_api_mobileapiconfig`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mobile_api_mobileapiconfig` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`change_date` datetime NOT NULL,
`changed_by_id` int(11) DEFAULT NULL,
`enabled` tinyint(1) NOT NULL,
`video_profiles` longtext NOT NULL,
PRIMARY KEY (`id`),
KEY `mobile_api_mobileapiconfig_16905482` (`changed_by_id`),
CONSTRAINT `changed_by_id_refs_id_97c2f4c8` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `notes_note`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......@@ -2245,7 +2298,7 @@ CREATE TABLE `south_migrationhistory` (
`migration` varchar(255) NOT NULL,
`applied` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=222 DEFAULT CHARSET=utf8;
) ENGINE=InnoDB AUTO_INCREMENT=228 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `splash_splashconfig`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
......
......@@ -2,15 +2,21 @@
"""Video xmodule tests in mongo."""
import json
from collections import OrderedDict
from mock import patch, MagicMock
from lxml import etree
from mock import patch, MagicMock, Mock
from django.conf import settings
from django.test import TestCase
from xmodule.video_module import create_youtube_string
from xmodule.video_module import create_youtube_string, VideoDescriptor
from xmodule.x_module import STUDENT_VIEW
from xmodule.tests.test_video import VideoDescriptorTestBase
from xmodule.tests.test_import import DummySystem
from edxval.api import create_profile, create_video
from edxval.api import (
create_profile, create_video, get_video_info, ValCannotCreateError, ValVideoNotFoundError
)
from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML
......@@ -520,15 +526,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
# create test profiles and their encodings
encoded_videos = []
for profile, extension in [("desktop_webm", "webm"), ("desktop_mp4", "mp4")]:
result = create_profile(
dict(
profile_name=profile,
extension=extension,
width=200,
height=2001
)
)
self.assertEqual(result, profile)
create_profile(profile)
encoded_videos.append(
dict(
url=u"http://fake-video.edx.org/thundercats.{}".format(extension),
......@@ -831,7 +829,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
self.assertFalse(self.item_descriptor.download_video)
class VideoDescriptorTest(VideoDescriptorTestBase):
class VideoDescriptorTest(TestCase, VideoDescriptorTestBase):
"""
Tests for video descriptor that requires access to django settings.
"""
......@@ -859,3 +857,79 @@ class VideoDescriptorTest(VideoDescriptorTestBase):
]
rendered_context = self.descriptor.get_context()
self.assertListEqual(rendered_context['tabs'], correct_tabs)
def test_export_val_data(self):
self.descriptor.edx_video_id = 'test_edx_video_id'
create_profile('mobile')
create_video({
'edx_video_id': self.descriptor.edx_video_id,
'client_video_id': 'test_client_video_id',
'duration': 111,
'status': 'dummy',
'encoded_videos': [{
'profile': 'mobile',
'url': 'http://example.com/video',
'file_size': 222,
'bitrate': 333,
}],
})
actual = self.descriptor.definition_to_xml(resource_fs=None)
expected_str = """
<video download_video="false" url_name="SampleProblem">
<video_asset client_video_id="test_client_video_id" duration="111.0">
<encoded_video profile="mobile" url="http://example.com/video" file_size="222" bitrate="333"/>
</video_asset>
</video>
"""
parser = etree.XMLParser(remove_blank_text=True)
expected = etree.XML(expected_str, parser=parser)
self.assertXmlEqual(expected, actual)
def test_export_val_data_not_found(self):
self.descriptor.edx_video_id = 'nonexistent'
actual = self.descriptor.definition_to_xml(resource_fs=None)
expected_str = """<video download_video="false" url_name="SampleProblem"/>"""
parser = etree.XMLParser(remove_blank_text=True)
expected = etree.XML(expected_str, parser=parser)
self.assertXmlEqual(expected, actual)
def test_import_val_data(self):
create_profile('mobile')
module_system = DummySystem(load_error_modules=True)
xml_data = """
<video edx_video_id="test_edx_video_id">
<video_asset client_video_id="test_client_video_id" duration="111.0">
<encoded_video profile="mobile" url="http://example.com/video" file_size="222" bitrate="333"/>
</video_asset>
</video>
"""
video = VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock())
self.assertEqual(video.edx_video_id, 'test_edx_video_id')
video_data = get_video_info(video.edx_video_id)
self.assertEqual(video_data['client_video_id'], 'test_client_video_id')
self.assertEqual(video_data['duration'], 111)
self.assertEqual(video_data['status'], 'imported')
self.assertEqual(video_data['courses'], [])
self.assertEqual(video_data['encoded_videos'][0]['profile'], 'mobile')
self.assertEqual(video_data['encoded_videos'][0]['url'], 'http://example.com/video')
self.assertEqual(video_data['encoded_videos'][0]['file_size'], 222)
self.assertEqual(video_data['encoded_videos'][0]['bitrate'], 333)
def test_import_val_data_invalid(self):
create_profile('mobile')
module_system = DummySystem(load_error_modules=True)
# Negative file_size is invalid
xml_data = """
<video edx_video_id="test_edx_video_id">
<video_asset client_video_id="test_client_video_id" duration="111.0">
<encoded_video profile="mobile" url="http://example.com/video" file_size="-222" bitrate="333"/>
</video_asset>
</video>
"""
with self.assertRaises(ValCannotCreateError):
VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock())
with self.assertRaises(ValVideoNotFoundError):
get_video_info("test_edx_video_id")
......@@ -63,24 +63,9 @@ class TestVideoAPITestCase(MobileAPITestCase):
self.youtube_url = 'http://val.edx.org/val/youtube.mp4'
self.html5_video_url = 'http://video.edx.org/html5/video.mp4'
api.create_profile({
'profile_name': 'youtube',
'extension': 'mp4',
'width': 1280,
'height': 720
})
api.create_profile({
'profile_name': 'mobile_high',
'extension': 'mp4',
'width': 750,
'height': 590
})
api.create_profile({
'profile_name': 'mobile_low',
'extension': 'mp4',
'width': 640,
'height': 480
})
api.create_profile('youtube')
api.create_profile('mobile_high')
api.create_profile('mobile_low')
# create the video in VAL
api.create_video({
......
......@@ -35,7 +35,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease
-e git+https://github.com/edx/i18n-tools.git@193cebd9aa784f8899ef496f2aa050b08eff402b#egg=i18n-tools
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.2#egg=oauth2-provider
-e git+https://github.com/edx/edx-val.git@64aa7637e3459fb3000a85a9e156880a40307dd1#egg=edx-val
-e git+https://github.com/edx/edx-val.git@cb9cf1a37124ad8e589734b36d4e8199e0082a02#egg=edx-val
-e git+https://github.com/pmitros/RecommenderXBlock.git@9b07e807c89ba5761827d0387177f71aa57ef056#egg=recommender-xblock
-e git+https://github.com/edx/edx-milestones.git@547f2250ee49e73ce8d7ff4e78ecf1b049892510#egg=edx-milestones
-e git+https://github.com/edx/edx-search.git@21ac6b06b3bfe789dcaeaf4e2ab5b00a688324d4#egg=edx-search
......
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