From 4a36fa892bcc5432a569930a5b0116eb65fc3832 Mon Sep 17 00:00:00 2001 From: polesye <s2pak.anton@gmail.com> Date: Thu, 26 Dec 2013 12:41:12 +0200 Subject: [PATCH] BLD-368: Turn "download transcript" into a dropdown. --- CHANGELOG.rst | 3 +++ cms/djangoapps/contentstore/features/component_settings_editor_helpers.py | 2 +- cms/djangoapps/contentstore/features/video-editor.py | 2 +- cms/static/js/views/metadata.js | 13 +++++++++++++ cms/static/sass/views/_unit.scss | 6 ++++++ common/lib/xmodule/xmodule/tests/test_video.py | 22 +++++++++++++++++----- common/lib/xmodule/xmodule/video_module.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- lms/djangoapps/courseware/tests/__init__.py | 52 ++++++++++++++++++++++++++++++---------------------- lms/djangoapps/courseware/tests/test_video_mongo.py | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- lms/djangoapps/courseware/tests/test_video_xml.py | 5 +---- 10 files changed, 509 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b0af2d..a7a77c1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Blades: Change the track field to a dropdown that will allow students +to download the transcript of the video without timecodes. BLD-368. + Blades: Video player start-end time range is now shown even before Play is clicked. Video player VCR time shows correct non-zero total time for YouTube videos even before Play is clicked. BLD-529. diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 932c21b..d3c0beb 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -140,7 +140,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set): for the problem, rather than derived from the defaults. This is verified by the existence of a "Clear" button next to the field value. """ - assert_equal(display_name, setting.find_by_css('.setting-label')[0].html) + assert_equal(display_name, setting.find_by_css('.setting-label')[0].html.strip()) # Check if the web object is a list type # If so, we use a slightly different mechanism for determining its value diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py index 6f78014..5b4dffc 100644 --- a/cms/djangoapps/contentstore/features/video-editor.py +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -40,12 +40,12 @@ def correct_video_settings(_step): # advanced ['Display Name', 'Video', False], - ['Download Transcript', '', False], ['Download Video', '', False], ['End Time', '00:00:00', False], ['HTML5 Transcript', '', False], ['Show Transcript', 'True', False], ['Start Time', '00:00:00', False], + ['Transcript Download Allowed', 'False', False], ['Video Sources', '', False], ['Youtube ID', 'OEoXaMPEzfM', False], ['Youtube ID for .75x speed', '', False], diff --git a/cms/static/js/views/metadata.js b/cms/static/js/views/metadata.js index 2f07ab1..9864611 100644 --- a/cms/static/js/views/metadata.js +++ b/cms/static/js/views/metadata.js @@ -94,6 +94,19 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) { templateName: "metadata-string-entry", + render: function () { + AbstractEditor.prototype.render.apply(this); + + // If the model has property `non editable` equals `true`, + // the field is disabled, but user is able to clear it. + if (this.model.get('non_editable')) { + this.$el.find('#' + this.uniqueId) + .prop('readonly', true) + .addClass('is-disabled'); + } + }, + + getValueFromEditor : function () { return this.$el.find('#' + this.uniqueId).val(); }, diff --git a/cms/static/sass/views/_unit.scss b/cms/static/sass/views/_unit.scss index 663fa31..d240fd7 100644 --- a/cms/static/sass/views/_unit.scss +++ b/cms/static/sass/views/_unit.scss @@ -708,6 +708,12 @@ body.course.unit,.view-unit { text-overflow: ellipsis; } + //Allows users to copy full value of disabled inputs. + input.is-disabled{ + text-overflow: clip; + opacity: .5; + } + input[type="number"] { width: 38.5%; diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index 78f5205..116aad7 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -25,7 +25,6 @@ from .test_import import DummySystem from xblock.field_data import DictFieldData from xblock.fields import ScopeIds -from textwrap import dedent from xmodule.tests import get_test_descriptor_system @@ -187,6 +186,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): <video display_name="Test Video" youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" show_captions="false" + download_track="true" start_time="00:00:01" end_time="00:01:00"> <source src="http://www.example.com/source.mp4"/> @@ -211,6 +211,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=1), 'end_time': datetime.timedelta(seconds=60), 'track': 'http://www.example.com/track', + 'download_track': True, 'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'], 'data': '' }) @@ -221,6 +222,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): <video display_name="Test Video" youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" show_captions="false" + download_track="false" start_time="00:00:01" end_time="00:01:00"> <source src="http://www.example.com/source.mp4"/> @@ -237,6 +239,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=1), 'end_time': datetime.timedelta(seconds=60), 'track': 'http://www.example.com/track', + 'download_track': False, 'source': 'http://www.example.com/source.mp4', 'html5_sources': ['http://www.example.com/source.mp4'], 'data': '' @@ -253,7 +256,6 @@ class VideoDescriptorImportTestCase(unittest.TestCase): youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA" show_captions="true"> <source src="http://www.example.com/source.mp4"/> - <track src="http://www.example.com/track"/> </video> ''' output = VideoDescriptor.from_xml(xml_data, module_system, Mock()) @@ -265,7 +267,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'show_captions': True, 'start_time': datetime.timedelta(seconds=0.0), 'end_time': datetime.timedelta(seconds=0.0), - 'track': 'http://www.example.com/track', + 'track': '', + 'download_track': False, 'source': 'http://www.example.com/source.mp4', 'html5_sources': ['http://www.example.com/source.mp4'], 'data': '' @@ -287,6 +290,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=0.0), 'end_time': datetime.timedelta(seconds=0.0), 'track': '', + 'download_track': False, 'source': '', 'html5_sources': [], 'data': '' @@ -305,6 +309,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): source=""http://download_video"" sub=""html5_subtitles"" track=""http://download_track"" + download_track="true" youtube_id_0_75=""OEoXaMPEzf65"" youtube_id_1_25=""OEoXaMPEzf125"" youtube_id_1_5=""OEoXaMPEzf15"" @@ -321,6 +326,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=0.0), 'end_time': datetime.timedelta(seconds=0.0), 'track': 'http://download_track', + 'download_track': True, 'source': 'http://download_video', 'html5_sources': ["source_1", "source_2"], 'data': '' @@ -343,6 +349,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=0.0), 'end_time': datetime.timedelta(seconds=0.0), 'track': '', + 'download_track': False, 'source': '', 'html5_sources': [], 'data': '' @@ -373,6 +380,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=1), 'end_time': datetime.timedelta(seconds=60), 'track': 'http://www.example.com/track', + 'download_track': True, 'html5_sources': ['http://www.example.com/source.mp4'], 'data': '' }) @@ -402,6 +410,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=1), 'end_time': datetime.timedelta(seconds=60), 'track': 'http://www.example.com/track', + 'download_track': True, 'html5_sources': ['http://www.example.com/source.mp4'], 'data': '' }) @@ -431,6 +440,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'start_time': datetime.timedelta(seconds=1), 'end_time': datetime.timedelta(seconds=60), 'track': 'http://www.example.com/track', + 'download_track': True, 'html5_sources': ['http://www.example.com/source.mp4'], 'data': '' }) @@ -461,11 +471,12 @@ class VideoExportTestCase(unittest.TestCase): desc.start_time = datetime.timedelta(seconds=1.0) desc.end_time = datetime.timedelta(seconds=60) desc.track = 'http://www.example.com/track' + desc.download_track = True desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'] xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter expected = etree.fromstring('''\ - <video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00"> + <video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00" download_track="true"> <source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.ogg"/> <track src="http://www.example.com/track"/> @@ -488,11 +499,12 @@ class VideoExportTestCase(unittest.TestCase): desc.start_time = datetime.timedelta(seconds=5.0) desc.end_time = datetime.timedelta(seconds=0.0) desc.track = 'http://www.example.com/track' + desc.download_track = True desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'] xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter expected = etree.fromstring('''\ - <video url_name="SampleProblem1" start_time="0:00:05" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false"> + <video url_name="SampleProblem1" start_time="0:00:05" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" download_track="true"> <source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.ogg"/> <track src="http://www.example.com/track"/> diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 2a27c03..9dd7a0c 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -13,18 +13,24 @@ in XML. import json import logging +from HTMLParser import HTMLParser from lxml import etree from pkg_resources import resource_string import datetime import copy +from webob import Response from django.http import Http404 from django.conf import settings -from xmodule.x_module import XModule +from xmodule.x_module import XModule, module_attr from xmodule.editing_module import TabsEditingDescriptor from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field +from xmodule.contentstore.django import contentstore +from xmodule.contentstore.content import StaticContent +from xmodule.exceptions import NotFoundError +from xblock.core import XBlock from xblock.fields import Scope, String, Boolean, List, Integer, ScopeIds from xmodule.fields import RelativeTime @@ -103,11 +109,19 @@ class VideoFields(object): display_name="Video Sources", scope=Scope.settings, ) + # `track` is deprecated field and should not be used in future. + # `download_track` is used instead. track = String( - help="The external URL to download the timed transcript track. This appears as a link beneath the video.", + help="The external URL to download the timed transcript track.", display_name="Download Transcript", scope=Scope.settings, - default="" + default='' + ) + download_track = Boolean( + help="Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.", + display_name="Transcript Download Allowed", + scope=Scope.settings, + default=False ) sub = String( help="The name of the timed transcript track (for non-Youtube videos).", @@ -162,18 +176,25 @@ class VideoModule(VideoFields, XModule): raise Http404() def get_html(self): + track_url = None caption_asset_path = "/static/subs/" get_ext = lambda filename: filename.rpartition('.')[-1] sources = {get_ext(src): src for src in self.html5_sources} sources['main'] = self.source + if self.download_track: + if self.track: + track_url = self.track + elif self.sub: + track_url = self.runtime.handler_url(self, 'download_transcript') + return self.system.render_template('video.html', { 'youtube_streams': _create_youtube_string(self), 'id': self.location.html_id(), 'sub': self.sub, 'sources': sources, - 'track': self.track, + 'track': track_url, 'display_name': self.display_name_with_default, # This won't work when we move to data that # isn't on the filesystem @@ -189,10 +210,58 @@ class VideoModule(VideoFields, XModule): 'yt_test_url': settings.YOUTUBE_TEST_URL }) + def get_transcript(self, subs_id): + ''' + Returns transcript without timecodes. + + Args: + `subs_id`: str, subtitles id + + Raises: + - NotFoundError if cannot find transcript file in storage. + - ValueError if transcript file is incorrect JSON. + - KeyError if transcript file has incorrect format. + ''' + + filename = 'subs_{0}.srt.sjson'.format(subs_id) + content_location = StaticContent.compute_location( + self.location.org, self.location.course, filename + ) + + data = contentstore().find(content_location).data + text = json.loads(data)['text'] + + return HTMLParser().unescape("\n".join(text)) + + + @XBlock.handler + def download_transcript(self, __, ___): + """ + This is called to get transcript file without timecodes to student. + """ + try: + subs = self.get_transcript(self.sub) + except (NotFoundError): + log.debug("Can't find content in storage for %s transcript", self.sub) + return Response(status=404) + except (ValueError, KeyError): + log.debug("Invalid transcript JSON.") + return Response(status=400) + + response = Response( + subs, + headerlist=[ + ('Content-Disposition', 'attachment; filename="{0}.txt"'.format(self.sub)), + ]) + response.content_type="text/plain; charset=utf-8" + + return response + class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor): """Descriptor for `VideoModule`.""" module_class = VideoModule + download_transcript = module_attr('download_transcript') tabs = [ { @@ -207,6 +276,12 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ] def __init__(self, *args, **kwargs): + ''' + `track` is deprecated field. + If `track` field exists show `track` field on front-end as not-editable + but clearable. Dropdown `download_track` is a new field and it has value + True. + ''' super(VideoDescriptor, self).__init__(*args, **kwargs) # For backwards compatibility -- if we've got XML data, parse # it out and set the metadata fields @@ -215,6 +290,24 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor self._field_data.set_many(self, field_data) del self.data + self.track_visible = False + if self.track: + self.track_visible = True + download_track = self.editable_metadata_fields['download_track'] + if not download_track['explicitly_set']: + self.download_track = True + + @property + def editable_metadata_fields(self): + editable_fields = super(VideoDescriptor, self).editable_metadata_fields + + if self.track_visible: + editable_fields['track']['non_editable'] = True + else: + editable_fields.pop('track') + + return editable_fields + @classmethod def from_xml(cls, xml_data, system, id_generator): """ @@ -265,6 +358,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor 'start_time': self.start_time, 'end_time': self.end_time, 'sub': self.sub, + 'download_track': json.dumps(self.download_track), } for key, value in attrs.items(): # Mild workaround to ensure that tests pass -- if a field @@ -282,6 +376,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ele = etree.Element('track') ele.set('src', self.track) xml.append(ele) + return xml def get_context(self): diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index fcca925..fcd9251 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -15,7 +15,6 @@ from edxmako.shortcuts import render_to_string from student.tests.factories import UserFactory, CourseEnrollmentFactory from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xblock.field_data import DictFieldData -from xblock.fields import Scope from xmodule.tests import get_test_system, get_test_descriptor_system from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -35,7 +34,7 @@ class BaseTestXmodule(ModuleStoreTestCase): Any xmodule should overwrite only next parameters for test: 1. CATEGORY - 2. DATA + 2. DATA or METADATA 3. MODEL_DATA 4. COURSE_DATA and USER_COUNT if needed @@ -48,6 +47,10 @@ class BaseTestXmodule(ModuleStoreTestCase): # Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml CATEGORY = "vertical" DATA = '' + # METADATA must be overwritten for every instance that uses it. Otherwise, + # if we'll change it in the tests, it will be changed for all other instances + # of parent class. + METADATA = {} MODEL_DATA = {'data': '<some_module></some_module>'} def new_module_runtime(self): @@ -71,8 +74,27 @@ class BaseTestXmodule(ModuleStoreTestCase): runtime.get_block = modulestore().get_item return runtime - def setUp(self): + def initialize_module(self, **kwargs): + kwargs.update({ + 'parent_location': self.section.location, + 'category': self.CATEGORY + }) + + self.item_descriptor = ItemFactory.create(**kwargs) + + self.runtime = self.new_descriptor_runtime() + field_data = {} + field_data.update(self.MODEL_DATA) + student_data = DictFieldData(field_data) + self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data) + + self.item_descriptor.xmodule_runtime = self.new_module_runtime() + self.item_module = self.item_descriptor + + self.item_url = Location(self.item_module.location).url() + + def setup_course(self): self.course = CourseFactory.create(data=self.COURSE_DATA) # Turn off cache. @@ -83,7 +105,7 @@ class BaseTestXmodule(ModuleStoreTestCase): parent_location=self.course.location, category="sequential", ) - section = ItemFactory.create( + self.section = ItemFactory.create( parent_location=chapter.location, category="sequential" ) @@ -97,24 +119,6 @@ class BaseTestXmodule(ModuleStoreTestCase): for user in self.users: CourseEnrollmentFactory.create(user=user, course_id=self.course.id) - self.item_descriptor = ItemFactory.create( - parent_location=section.location, - category=self.CATEGORY, - data=self.DATA - ) - - self.runtime = self.new_descriptor_runtime() - - field_data = {} - field_data.update(self.MODEL_DATA) - student_data = DictFieldData(field_data) - self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data) - - self.item_descriptor.xmodule_runtime = self.new_module_runtime() - self.item_module = self.item_descriptor - - self.item_url = Location(self.item_module.location).url() - # login all users for acces to Xmodule self.clients = {user.username: Client() for user in self.users} self.login_statuses = [ @@ -125,6 +129,10 @@ class BaseTestXmodule(ModuleStoreTestCase): self.assertTrue(all(self.login_statuses)) + def setUp(self): + self.setup_course(); + self.initialize_module(metadata=self.METADATA, data=self.DATA) + def get_url(self, dispatch): """Return item url with dispatch.""" return reverse( diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index e3c4ea5..386cdc8 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- """Video xmodule tests in mongo.""" +from mock import patch, PropertyMock +import os +import tempfile +import textwrap +from functools import partial + +from xmodule.contentstore.content import StaticContent +from xmodule.modulestore import Location +from xmodule.contentstore.django import contentstore from . import BaseTestXmodule from .test_video_xml import SOURCE_XML from django.conf import settings from xmodule.video_module import _create_youtube_string - +from cache_toolbox.core import del_cached_content +from xmodule.exceptions import NotFoundError class TestVideo(BaseTestXmodule): """Integration tests: web client + mongo.""" - CATEGORY = "video" DATA = SOURCE_XML + METADATA = {} def test_handle_ajax_dispatch(self): responses = { @@ -29,16 +39,21 @@ class TestVideo(BaseTestXmodule): ]).pop(), 404) + def tearDown(self): + _clear_assets(self.item_module.location) + + +class TestVideoYouTube(TestVideo): + METADATA = {} + def test_video_constructor(self): """Make sure that all parameters extracted correclty from xml""" - context = self.item_module.render('student_view').content sources = { 'main': u'example.mp4', u'mp4': u'example.mp4', u'webm': u'example.webm', - u'ogv': u'example.ogv' } expected_context = { @@ -51,7 +66,7 @@ class TestVideo(BaseTestXmodule): 'sources': sources, 'start': 3603.0, 'sub': u'a_sub_file.srt.sjson', - 'track': '', + 'track': None, 'youtube_streams': _create_youtube_string(self.item_module), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'yt_test_timeout': 1500, @@ -75,12 +90,12 @@ class TestVideoNonYouTube(TestVideo): > <source src="example.mp4"/> <source src="example.webm"/> - <source src="example.ogv"/> </video> """ MODEL_DATA = { 'data': DATA } + METADATA = {} def test_video_constructor(self): """Make sure that if the 'youtube' attribute is omitted in XML, then @@ -90,7 +105,6 @@ class TestVideoNonYouTube(TestVideo): 'main': u'example.mp4', u'mp4': u'example.mp4', u'webm': u'example.webm', - u'ogv': u'example.ogv' } context = self.item_module.render('student_view').content @@ -105,7 +119,7 @@ class TestVideoNonYouTube(TestVideo): 'sources': sources, 'start': 3603.0, 'sub': u'a_sub_file.srt.sjson', - 'track': '', + 'track': None, 'youtube_streams': '1.00:OEoXaMPEzfM', 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), 'yt_test_timeout': 1500, @@ -116,3 +130,319 @@ class TestVideoNonYouTube(TestVideo): context, self.item_module.xmodule_runtime.render_template('video.html', expected_context) ) + + +class TestVideoGetTranscriptsMethod(TestVideo): + """ + Make sure that `get_transcript` method works correctly + """ + + DATA = """ + <video show_captions="true" + display_name="A Name" + > + <source src="example.mp4"/> + <source src="example.webm"/> + </video> + """ + MODEL_DATA = { + 'data': DATA + } + METADATA = {} + + def test_good_transcript(self): + self.item_module.render('student_view') + item = self.item_descriptor.xmodule_runtime.xmodule_instance + + good_sjson = _create_file(content=""" + { + "start": [ + 270, + 2720 + ], + "end": [ + 2720, + 5430 + ], + "text": [ + "Hi, welcome to Edx.", + "Let's start with what is on your screen right now." + ] + } + """) + + _upload_file(good_sjson, self.item_module.location) + subs_id = _get_subs_id(good_sjson.name) + + text = item.get_transcript(subs_id) + expected_text = "Hi, welcome to Edx.\nLet's start with what is on your screen right now." + + self.assertEqual( + text, expected_text + ) + + def test_not_found_error(self): + self.item_module.render('student_view') + item = self.item_descriptor.xmodule_runtime.xmodule_instance + + with self.assertRaises(NotFoundError): + item.get_transcript('wrong') + + def test_value_error(self): + self.item_module.render('student_view') + item = self.item_descriptor.xmodule_runtime.xmodule_instance + + good_sjson = _create_file(content=""" + bad content + """) + + _upload_file(good_sjson, self.item_module.location) + subs_id = _get_subs_id(good_sjson.name) + + with self.assertRaises(ValueError): + item.get_transcript(subs_id) + + def test_key_error(self): + self.item_module.render('student_view') + item = self.item_descriptor.xmodule_runtime.xmodule_instance + + good_sjson = _create_file(content=""" + { + "start": [ + 270, + 2720 + ], + "end": [ + 2720, + 5430 + ] + } + """) + + _upload_file(good_sjson, self.item_module.location) + subs_id = _get_subs_id(good_sjson.name) + + with self.assertRaises(KeyError): + item.get_transcript(subs_id) + + +class TestGetHtmlMethod(BaseTestXmodule): + """ + Make sure that `get_html` works correctly. + """ + CATEGORY = "video" + DATA = SOURCE_XML + METADATA = {} + + def setUp(self): + self.setup_course(); + + def test_get_html_track(self): + SOURCE_XML = """ + <video show_captions="true" + display_name="A Name" + sub="{sub}" download_track="{download_track}" + start_time="01:00:03" end_time="01:00:10" + > + <source src="example.mp4"/> + <source src="example.webm"/> + {track} + </video> + """ + + cases = [ + { + 'download_track': u'true', + 'track': u'<track src="http://www.example.com/track"/>', + 'sub': u'a_sub_file.srt.sjson', + 'expected_track_url': u'http://www.example.com/track', + }, + { + 'download_track': u'true', + 'track': u'', + 'sub': u'a_sub_file.srt.sjson', + 'expected_track_url': u'a_sub_file.srt.sjson', + }, + { + 'download_track': u'true', + 'track': u'', + 'sub': u'', + 'expected_track_url': None + }, + { + 'download_track': u'false', + 'track': u'<track src="http://www.example.com/track"/>', + 'sub': u'a_sub_file.srt.sjson', + 'expected_track_url': None, + } + ] + + expected_context = { + 'data_dir': getattr(self, 'data_dir', None), + 'caption_asset_path': '/static/subs/', + 'show_captions': 'true', + 'display_name': u'A Name', + 'end': 3610.0, + 'id': None, + 'sources': { + 'main': u'example.mp4', + u'mp4': u'example.mp4', + u'webm': u'example.webm' + }, + 'start': 3603.0, + 'sub': u'a_sub_file.srt.sjson', + 'track': '', + 'youtube_streams': '1.00:OEoXaMPEzfM', + 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), + 'yt_test_timeout': 1500, + 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/' + } + + for data in cases: + DATA = SOURCE_XML.format( + download_track=data['download_track'], + track=data['track'], + sub=data['sub'], + ) + + self.initialize_module(data=DATA) + track_url = self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'download_transcript') + + expected_context.update({ + 'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'], + 'sub': data['sub'], + 'id': self.item_module.location.html_id(), + }) + + context = self.item_module.render('student_view').content + self.assertEqual( + context, + self.item_module.xmodule_runtime.render_template('video.html', expected_context) + ) + + +class TestVideoDescriptorInitialization(BaseTestXmodule): + """ + Make sure that module initialization works correctly. + """ + CATEGORY = "video" + DATA = SOURCE_XML + METADATA = {} + + def setUp(self): + self.setup_course(); + + def test_track_is_not_empty(self): + metatdata = { + 'track': 'http://example.org/track', + } + + self.initialize_module(metadata=metatdata) + fields = self.item_descriptor.editable_metadata_fields + + self.assertIn('track', fields) + self.assertEqual(self.item_module.track, 'http://example.org/track') + self.assertTrue(self.item_module.download_track) + self.assertTrue(self.item_module.track_visible) + + @patch('xmodule.x_module.XModuleDescriptor.editable_metadata_fields', new_callable=PropertyMock) + def test_download_track_is_explicitly_set(self, mock_editable_fields): + mock_editable_fields.return_value = { + 'download_track': { + 'default_value': False, + 'explicitly_set': True, + 'display_name': 'Transcript Download Allowed', + 'help': 'Show a link beneath the video to allow students to download the transcript.', + 'type': 'Boolean', + 'value': False, + 'field_name': 'download_track', + 'options': [ + {'display_name': "True", "value": True}, + {'display_name': "False", "value": False} + ] + }, + 'track': { + 'default_value': '', + 'explicitly_set': False, + 'display_name': 'Download Transcript', + 'help': 'The external URL to download the timed transcript track.', + 'type': 'Generic', + 'value': u'http://example.org/track', + 'field_name': 'track', + 'options': [] + }, + } + metadata = { + 'track': 'http://example.org/track', + } + + self.initialize_module(metadata=metadata) + fields = self.item_descriptor.editable_metadata_fields + + self.assertIn('track', fields) + self.assertEqual(self.item_module.track, 'http://example.org/track') + self.assertFalse(self.item_module.download_track) + self.assertTrue(self.item_module.track_visible) + + + def test_track_is_empty(self): + metatdata = { + 'track': '', + } + + self.initialize_module(metadata=metatdata) + fields = self.item_descriptor.editable_metadata_fields + + self.assertNotIn('track', fields) + self.assertEqual(self.item_module.track, '') + self.assertFalse(self.item_module.download_track) + self.assertFalse(self.item_module.track_visible) + + +def _clear_assets(location): + store = contentstore() + + content_location = StaticContent.compute_location( + location.org, location.course, location.name + ) + + assets, __ = store.get_all_content_for_course(content_location) + for asset in assets: + asset_location = Location(asset["_id"]) + id = StaticContent.get_id_from_location(asset_location) + store.delete(id) + +def _get_subs_id(filename): + basename = os.path.splitext(os.path.basename(filename))[0] + return basename.replace('subs_', '').replace('.srt', '') + +def _create_file(content=''): + sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson") + sjson_file.content_type = 'application/json' + sjson_file.write(textwrap.dedent(content)) + sjson_file.seek(0) + + return sjson_file + +def _upload_file(file, location): + filename = 'subs_{}.srt.sjson'.format(_get_subs_id(file.name)) + mime_type = file.content_type + + content_location = StaticContent.compute_location( + location.org, location.course, filename + ) + + sc_partial = partial(StaticContent, content_location, filename, mime_type) + content = sc_partial(file.read()) + + (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail( + content, + tempfile_path=None + ) + del_cached_content(thumbnail_location) + + if thumbnail_content is not None: + content.thumbnail_location = thumbnail_location + + contentstore().save(content) + del_cached_content(content.location) diff --git a/lms/djangoapps/courseware/tests/test_video_xml.py b/lms/djangoapps/courseware/tests/test_video_xml.py index 87e430c..2cfa3e1 100644 --- a/lms/djangoapps/courseware/tests/test_video_xml.py +++ b/lms/djangoapps/courseware/tests/test_video_xml.py @@ -35,7 +35,6 @@ SOURCE_XML = """ > <source src="example.mp4"/> <source src="example.webm"/> - <source src="example.ogv"/> </video> """ @@ -68,12 +67,10 @@ class VideoModuleUnitTest(unittest.TestCase): def test_video_get_html(self): """Make sure that all parameters extracted correclty from xml""" module = VideoFactory.create() - sources = { 'main': 'example.mp4', 'mp4': 'example.mp4', 'webm': 'example.webm', - 'ogv': 'example.ogv' } expected_context = { @@ -87,7 +84,7 @@ class VideoModuleUnitTest(unittest.TestCase): 'show_captions': 'true', 'sources': sources, 'youtube_streams': _create_youtube_string(module), - 'track': '', + 'track': None, 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'yt_test_timeout': 1500, 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/' -- libgit2 0.26.0