diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index dba8bbd0b4..1c25b272a3 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,6 +1,6 @@ --- metadata: - display_name: Video Alpha 1 + display_name: Video Alpha version: 1 data: | diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py index ff17f88dfc..6fb331b3cf 100644 --- a/common/lib/xmodule/xmodule/tests/test_logic.py +++ b/common/lib/xmodule/xmodule/tests/test_logic.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- +# pylint: disable=W0232 """Test for Xmodule functional logic.""" import json import unittest -from lxml import etree - from xmodule.poll_module import PollDescriptor from xmodule.conditional_module import ConditionalDescriptor from xmodule.word_cloud_module import WordCloudDescriptor -from xmodule.videoalpha_module import VideoAlphaDescriptor +from xmodule.tests import test_system class PostData: """Class which emulate postdata.""" @@ -17,6 +16,7 @@ class PostData: self.dict_data = dict_data def getlist(self, key): + """Get data by key from `self.dict_data`.""" return self.dict_data.get(key) @@ -27,9 +27,10 @@ class LogicTest(unittest.TestCase): def setUp(self): class EmptyClass: + """Empty object.""" pass - self.system = None + self.system = test_system() self.descriptor = EmptyClass() self.xmodule_class = self.descriptor_class.module_class @@ -40,10 +41,12 @@ class LogicTest(unittest.TestCase): ) def ajax_request(self, dispatch, get): + """Call Xmodule.handle_ajax.""" return json.loads(self.xmodule.handle_ajax(dispatch, get)) class PollModuleTest(LogicTest): + """Logic tests for Poll Xmodule.""" descriptor_class = PollDescriptor raw_model_data = { 'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0}, @@ -69,6 +72,7 @@ class PollModuleTest(LogicTest): class ConditionalModuleTest(LogicTest): + """Logic tests for Conditional Xmodule.""" descriptor_class = ConditionalDescriptor def test_ajax_request(self): @@ -83,6 +87,7 @@ class ConditionalModuleTest(LogicTest): class WordCloudModuleTest(LogicTest): + """Logic tests for Word Cloud Xmodule.""" descriptor_class = WordCloudDescriptor raw_model_data = { 'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2}, @@ -91,8 +96,6 @@ class WordCloudModuleTest(LogicTest): } def test_bad_ajax_request(self): - - # TODO: move top global test. Formalize all our Xmodule errors. response = self.ajax_request('bad_dispatch', {}) self.assertDictEqual(response, { 'status': 'fail', @@ -118,34 +121,6 @@ class WordCloudModuleTest(LogicTest): {'text': 'cat', 'size': 12, 'percent': 54.0}] ) - self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) ) - - -class VideoAlphaModuleTest(LogicTest): - descriptor_class = VideoAlphaDescriptor - - raw_model_data = { - 'data': '' - } - - def test_get_timeframe_no_parameters(self): - xmltree = etree.fromstring('test') - output = self.xmodule._get_timeframe(xmltree) - self.assertEqual(output, ('', '')) - - def test_get_timeframe_with_one_parameter(self): - xmltree = etree.fromstring( - 'test' - ) - output = self.xmodule._get_timeframe(xmltree) - self.assertEqual(output, (247, '')) - - def test_get_timeframe_with_two_parameters(self): - xmltree = etree.fromstring( - '''test''' - ) - output = self.xmodule._get_timeframe(xmltree) - self.assertEqual(output, (247, 47079)) + self.assertEqual( + 100.0, + sum(i['percent'] for i in response['top_words'])) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 43de021799..a64e094a58 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -1,3 +1,15 @@ +# pylint: disable=W0223 +"""VideoAlpha is ungraded Xmodule for support video content. +It's new improved video module, which support additional feature: + +- Can play non-YouTube video sources via in-browser HTML5 video player. +- YouTube defaults to HTML5 mode from the start. +- Speed changes in both YouTube and non-YouTube videos happen via +in-browser HTML5 video method (when in HTML5 mode). +- Navigational subtitles can be disabled altogether via an attribute +in XML. +""" + import json import logging @@ -21,6 +33,7 @@ log = logging.getLogger(__name__) class VideoAlphaFields(object): + """Fields for `VideoAlphaModule` and `VideoAlphaDescriptor`.""" data = String(help="XML data for the problem", scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) display_name = String(help="Display name for this module", scope=Scope.settings) @@ -68,7 +81,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): 'ogv': self._get_source(xmltree, ['ogv']), } self.track = self._get_track(xmltree) - self.start_time, self.end_time = self._get_timeframe(xmltree) + self.start_time, self.end_time = self.get_timeframe(xmltree) def _get_source(self, xmltree, exts=None): """Find the first valid source, which ends with one of `exts`.""" @@ -77,7 +90,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): - # find the first valid track + """Find the first valid track.""" return self._get_first_external(xmltree, 'track') def _get_first_external(self, xmltree, tag, condition=bool): @@ -93,39 +106,33 @@ class VideoAlphaModule(VideoAlphaFields, XModule): break return result - def _get_timeframe(self, xmltree): + def get_timeframe(self, xmltree): """ Converts 'start_time' and 'end_time' parameters in video tag to seconds. If there are no parameters, returns empty string. """ - def parse_time(s): + def parse_time(str_time): """Converts s in '12:34:45' format to seconds. If s is None, returns empty string""" - if s is None: + if str_time is None: return '' else: - x = time.strptime(s, '%H:%M:%S') + obj_time = time.strptime(str_time, '%H:%M:%S') return datetime.timedelta( - hours=x.tm_hour, - minutes=x.tm_min, - seconds=x.tm_sec + hours=obj_time.tm_hour, + minutes=obj_time.tm_min, + seconds=obj_time.tm_sec ).total_seconds() return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time')) def handle_ajax(self, dispatch, get): - """Handle ajax calls to this video. - TODO (vshnayder): This is not being called right now, so the - position is not being saved. - """ + """This is not being called right now and we raise 404 error.""" log.debug(u"GET {0}".format(get)) log.debug(u"DISPATCH {0}".format(dispatch)) - if dispatch == 'goto_position': - self.position = int(float(get['position'])) - log.info(u"NEW POSITION {0}".format(self.position)) - return json.dumps({'success': True}) raise Http404() def get_instance_state(self): + """Return information about state (position).""" return json.dumps({'position': self.position}) def get_html(self): @@ -143,7 +150,8 @@ class VideoAlphaModule(VideoAlphaFields, XModule): 'sources': self.sources, 'track': self.track, 'display_name': self.display_name_with_default, - # TODO (cpennington): This won't work when we move to data that isn't on the filesystem + # This won't work when we move to data that + # isn't on the filesystem 'data_dir': getattr(self, 'data_dir', None), 'caption_asset_path': caption_asset_path, 'show_captions': self.show_captions, @@ -154,5 +162,6 @@ class VideoAlphaModule(VideoAlphaFields, XModule): class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor): + """Descriptor for `VideoAlphaModule`.""" module_class = VideoAlphaModule template_dir_name = "videoalpha" diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index cc53bf735a..1cb403018c 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -25,8 +25,8 @@ class BaseTestXmodule(ModuleStoreTestCase): """Base class for testing Xmodules with mongo store. This class prepares course and users for tests: - 1. create test course - 2. create, enrol and login users for this course + 1. create test course; + 2. create, enrol and login users for this course; Any xmodule should overwrite only next parameters for test: 1. TEMPLATE_NAME diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py new file mode 100644 index 0000000000..a6bff60acf --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""Video xmodule tests in mongo.""" + +from . import BaseTestXmodule +from .test_videoalpha_xml import SOURCE_XML +from django.conf import settings + + +class TestVideo(BaseTestXmodule): + """Integration tests: web client + mongo.""" + + TEMPLATE_NAME = "i4x://edx/templates/videoalpha/Video_Alpha" + DATA = SOURCE_XML + MODEL_DATA = { + 'data': DATA + } + + def test_handle_ajax_dispatch(self): + responses = { + user.username: self.clients[user.username].post( + self.get_url('whatever'), + {}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + for user in self.users + } + + self.assertEqual( + set([ + response.status_code + for _, response in responses.items() + ]).pop(), + 404) + + def test_videoalpha_constructor(self): + """Make sure that all parameters extracted correclty from xml""" + + # `get_html` return only context, cause we + # overwrite `system.render_template` + context = self.item_module.get_html() + expected_context = { + 'data_dir': getattr(self, 'data_dir', None), + 'caption_asset_path': '/c4x/MITx/999/asset/subs_', + 'show_captions': self.item_module.show_captions, + 'display_name': self.item_module.display_name_with_default, + 'end': self.item_module.end_time, + 'id': self.item_module.location.html_id(), + 'sources': self.item_module.sources, + 'start': self.item_module.start_time, + 'sub': self.item_module.sub, + 'track': self.item_module.track, + 'youtube_streams': self.item_module.youtube_streams, + 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) + } + self.assertDictEqual(context, expected_context) diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py new file mode 100644 index 0000000000..44e0a7811a --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +"""Test for VideoAlpha Xmodule functional logic. +These tests data readed from xml, not from mongo. + +We have a ModuleStoreTestCase class defined in +common/lib/xmodule/xmodule/modulestore/tests/django_utils.py. +You can search for usages of this in the cms and lms tests for examples. +You use this so that it will do things like point the modulestore +setting to mongo, flush the contentstore before and after, load the +templates, etc. +You can then use the CourseFactory and XModuleItemFactory as defined in +common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the +course, section, subsection, unit, etc. +""" + +import json +import unittest +from mock import Mock +from lxml import etree + +from django.conf import settings + +from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule +from xmodule.modulestore import Location +from xmodule.tests import test_system +from xmodule.tests.test_logic import LogicTest + + +SOURCE_XML = """ + + + + + +""" + + +class VideoAlphaFactory(object): + """A helper class to create videoalpha modules with various parameters + for testing. + """ + + # tag that uses youtube videos + sample_problem_xml_youtube = SOURCE_XML + + @staticmethod + def create(): + """Method return VideoAlpha Xmodule instance.""" + location = Location(["i4x", "edX", "videoalpha", "default", + "SampleProblem1"]) + model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube} + + descriptor = Mock(weight="1") + + system = test_system() + system.render_template = lambda template, context: context + VideoAlphaModule.location = location + module = VideoAlphaModule(system, descriptor, model_data) + + return module + + +class VideoAlphaModuleTest(LogicTest): + """Tests for logic of VideoAlpha Xmodule.""" + + descriptor_class = VideoAlphaDescriptor + + raw_model_data = { + 'data': '' + } + + def test_get_timeframe_no_parameters(self): + xmltree = etree.fromstring('test') + output = self.xmodule.get_timeframe(xmltree) + self.assertEqual(output, ('', '')) + + def test_get_timeframe_with_one_parameter(self): + xmltree = etree.fromstring( + 'test' + ) + output = self.xmodule.get_timeframe(xmltree) + self.assertEqual(output, (247, '')) + + def test_get_timeframe_with_two_parameters(self): + xmltree = etree.fromstring( + '''test''' + ) + output = self.xmodule.get_timeframe(xmltree) + self.assertEqual(output, (247, 47079)) + + +class VideoAlphaModuleUnitTest(unittest.TestCase): + """Unit tests for VideoAlpha Xmodule.""" + + def test_videoalpha_constructor(self): + """Make sure that all parameters extracted correclty from xml""" + module = VideoAlphaFactory.create() + + # `get_html` return only context, cause we + # overwrite `system.render_template` + context = module.get_html() + expected_context = { + 'caption_asset_path': '/static/subs/', + 'sub': module.sub, + 'data_dir': getattr(self, 'data_dir', None), + 'display_name': module.display_name_with_default, + 'end': module.end_time, + 'start': module.start_time, + 'id': module.location.html_id(), + 'show_captions': module.show_captions, + 'sources': module.sources, + 'youtube_streams': module.youtube_streams, + 'track': module.track, + 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) + } + self.assertDictEqual(context, expected_context) + + self.assertDictEqual( + json.loads(module.get_instance_state()), + {'position': 0})