diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c0b652204..a2c892bc0e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ 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: Fix download subs for non youtube videos and non-en language. BLD-897. + Blades: Fix issues related to videos that have separate YouTube IDs for the different video speeds. BLD-915, BLD-901. diff --git a/cms/djangoapps/contentstore/features/transcripts.feature b/cms/djangoapps/contentstore/features/transcripts.feature index bd0a6efc1b..8fb2a0d99e 100644 --- a/cms/djangoapps/contentstore/features/transcripts.feature +++ b/cms/djangoapps/contentstore/features/transcripts.feature @@ -369,31 +369,32 @@ Feature: CMS Transcripts And I click transcript button "choose" number 2 And I see value "test_transcripts|t_not_exist" in the field "Transcript (primary)" + # Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927 #21 - Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing - Given I have created a Video component with subtitles "t_not_exist" - And I edit the component - - And I enter a "t_not_exist.mp4" source to field number 1 - Then I see status message "found" - And I see button "download_to_edit" - And I see button "upload_new_timed_transcripts" - And I see value "t_not_exist" in the field "Transcript (primary)" - - And I save changes - And I edit the component - - And I enter a "video_name_2.mp4" source to field number 1 - Then I see status message "use existing" - And I see button "use_existing" - And I click transcript button "use_existing" - And I see value "video_name_2" in the field "Transcript (primary)" - - And I enter a "video_name_3.mp4" source to field number 1 - Then I see status message "use existing" - And I see button "use_existing" - And I click transcript button "use_existing" - And I see value "video_name_3" in the field "Transcript (primary)" + #Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o #transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing + # Given I have created a Video component with subtitles "t_not_exist" + # And I edit the component + # + # And I enter a "t_not_exist.mp4" source to field number 1 + # Then I see status message "found" + # And I see button "download_to_edit" + # And I see button "upload_new_timed_transcripts" + # And I see value "t_not_exist" in the field "Transcript (primary)" + # + # And I save changes + # And I edit the component + # + # And I enter a "video_name_2.mp4" source to field number 1 + # Then I see status message "use existing" + # And I see button "use_existing" + # And I click transcript button "use_existing" + # And I see value "video_name_2" in the field "Transcript (primary)" + # + # And I enter a "video_name_3.mp4" source to field number 1 + # Then I see status message "use existing" + # And I see button "use_existing" + # And I click transcript button "use_existing" + # And I see value "video_name_3" in the field "Transcript (primary)" #22 Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - click on use existing -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> change it to another one HTML5 source w/o transcripts - click on use existing diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py index fdf7b1313a..6423dc88bd 100644 --- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py +++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py @@ -487,3 +487,64 @@ class TestYoutubeTranscripts(unittest.TestCase): transcripts = transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation) self.assertEqual(transcripts, expected_transcripts) mock_get.assert_called_with('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_youtube_id'}) + +class TestTranscript(unittest.TestCase): + """ + Tests for Transcript class e.g. different transcript conversions. + """ + def setUp(self): + + self.srt_transcript = textwrap.dedent("""\ + 0 + 00:00:10,500 --> 00:00:13,000 + Elephant's Dream + + 1 + 00:00:15,000 --> 00:00:18,000 + At the left we can see... + + """) + + + self.sjson_transcript = textwrap.dedent("""\ + { + "start": [ + 10500, + 15000 + ], + "end": [ + 13000, + 18000 + ], + "text": [ + "Elephant's Dream", + "At the left we can see..." + ] + } + """) + + self.txt_transcript = u"Elephant's Dream\nAt the left we can see..." + + def test_convert_srt_to_txt(self): + expected = self.txt_transcript + actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'txt') + self.assertEqual(actual, expected) + + def test_convert_srt_to_srt(self): + expected = self.srt_transcript + actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'srt') + self.assertEqual(actual, expected) + + def test_convert_sjson_to_txt(self): + expected = self.txt_transcript + actual = transcripts_utils.Transcript.convert(self.sjson_transcript, 'sjson', 'txt') + self.assertEqual(actual, expected) + + def test_convert_sjson_to_srt(self): + expected = self.srt_transcript + actual = transcripts_utils.Transcript.convert(self.sjson_transcript, 'sjson', 'srt') + self.assertEqual(actual, expected) + + def test_convert_srt_to_sjson(self): + with self.assertRaises(NotImplementedError): + transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson') diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index be17e900fc..8cc8dc859a 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -233,8 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): end_time="00:01:00"> - - + + ''' output = VideoDescriptor.from_xml(xml_data, module_system, Mock()) @@ -251,7 +251,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): 'download_video': False, 'html5_sources': ['http://www.example.com/source.mp4'], 'data': '', - 'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}, + 'transcripts': {'uk': 'ukrainian_translation.srt', 'de': 'german_translation.srt'}, }) def test_from_xml_missing_attributes(self): diff --git a/common/lib/xmodule/xmodule/video_module/transcripts_utils.py b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py index 217fec5b9d..d971514ead 100644 --- a/common/lib/xmodule/xmodule/video_module/transcripts_utils.py +++ b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py @@ -9,6 +9,7 @@ import requests import logging from pysrt import SubRipTime, SubRipItem, SubRipFile from lxml import etree +from HTMLParser import HTMLParser from xmodule.exceptions import NotFoundError from xmodule.contentstore.content import StaticContent @@ -77,7 +78,7 @@ def save_subs_to_store(subs, subs_id, item, language='en'): filedata = json.dumps(subs, indent=2) mime_type = 'application/json' filename = subs_filename(subs_id, language) - content_location = asset_location(item.location, filename) + content_location = Transcript.asset_location(item.location, filename) content = StaticContent(content_location, filename, mime_type, filedata) contentstore().save(content) return content_location @@ -193,7 +194,7 @@ def remove_subs_from_store(subs_id, item, lang='en'): Remove from store, if transcripts content exists. """ try: - content = asset(item.location, subs_id, lang) + content = Transcript.asset(item.location, subs_id, lang) contentstore().delete(content.get_id()) log.info("Removed subs %s from store", subs_id) except NotFoundError: @@ -412,30 +413,6 @@ def subs_filename(subs_id, lang='en'): return '{0}_subs_{1}.srt.sjson'.format(lang, subs_id) -def asset_location(location, filename): - """ - Return asset location. - - `location` is module location. - """ - return StaticContent.compute_location( - location.org, location.course, filename - ) - - -def asset(location, subs_id, lang='en', filename=None): - """ - Get asset from contentstore, asset location is built from subs_id and lang. - - `location` is module location. - """ - return contentstore().find( - asset_location( - location, - subs_filename(subs_id, lang) if not filename else filename - ) - ) - def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang): """ @@ -444,7 +421,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang): `item` is module object. """ try: - srt_transcripts = contentstore().find(asset_location(item.location, user_filename)) + srt_transcript = contentstore().find(Transcript.asset_location(item.location, user_filename)) except NotFoundError as ex: raise TranscriptException("{}: Can't find uploaded transcripts: {}".format(ex.message, user_filename)) @@ -454,7 +431,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang): generate_subs_from_source( result_subs_dict, os.path.splitext(user_filename)[1][1:], - srt_transcripts.data.decode('utf8'), + srt_transcript.data.decode('utf8'), item, lang ) @@ -477,8 +454,72 @@ def get_or_create_sjson(item): user_subs_id = os.path.splitext(user_filename)[0] source_subs_id, result_subs_dict = user_subs_id, {1.0: user_subs_id} try: - sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data + sjson_transcript = Transcript.asset(item.location, source_subs_id, item.transcript_language).data except (NotFoundError): # generating sjson from srt generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, item.transcript_language) - sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data + sjson_transcript = Transcript.asset(item.location, source_subs_id, item.transcript_language).data return sjson_transcript + +class Transcript(object): + """ + Container for transcript methods. + """ + + @staticmethod + def convert(content, input_format, output_format): + """ + Convert transcript `content` from `input_format` to `output_format`. + + Accepted input formats: sjson, srt. + Accepted output format: srt, txt. + """ + assert input_format in ('srt', 'sjson') + assert output_format in ('txt', 'srt', 'sjson') + + if input_format == output_format: + return content + + if input_format == 'srt': + + if output_format == 'txt': + text = SubRipFile.from_string(content.decode('utf8')).text + return HTMLParser().unescape(text) + + elif output_format == 'sjson': + raise NotImplementedError + + if input_format == 'sjson': + + if output_format == 'txt': + text = json.loads(content)['text'] + return HTMLParser().unescape("\n".join(text)) + + elif output_format == 'srt': + return generate_srt_from_sjson(json.loads(content), speed=1.0) + + @staticmethod + def asset(location, subs_id, lang='en', filename=None): + """ + Get asset from contentstore, asset location is built from subs_id and lang. + + `location` is module location. + """ + return contentstore().find( + Transcript.asset_location( + location, + subs_filename(subs_id, lang) if not filename else filename + ) + ) + + + @staticmethod + def asset_location(location, filename): + """ + Return asset location. + + `location` is module location. + """ + return StaticContent.compute_location( + location.org, location.course, filename + ) + diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index c60dc42dcc..3b5bcfac2e 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -10,10 +10,10 @@ in-browser HTML5 video method (when in HTML5 mode). in XML. """ +import os import json import logging from operator import itemgetter -from HTMLParser import HTMLParser from lxml import etree from pkg_resources import resource_string @@ -33,12 +33,11 @@ from xblock.core import XBlock from xblock.fields import Scope, String, Float, Boolean, List, Dict, ScopeIds from xmodule.fields import RelativeTime from .transcripts_utils import ( - generate_srt_from_sjson, - asset, get_or_create_sjson, TranscriptException, generate_sjson_for_all_speeds, - youtube_speed_dict + youtube_speed_dict, + Transcript, ) from .video_utils import create_youtube_string @@ -271,13 +270,13 @@ class VideoModule(VideoFields, XModule): track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download' if not self.transcripts: - transcript_language = 'en' + transcript_language = u'en' languages = {'en': 'English'} else: if self.transcript_language in self.transcripts: transcript_language = self.transcript_language elif self.sub: - transcript_language = 'en' + transcript_language = u'en' else: transcript_language = sorted(self.transcripts.keys())[0] @@ -323,30 +322,47 @@ class VideoModule(VideoFields, XModule): 'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations', }) - def get_transcript(self, format='srt'): + def get_transcript(self, transcript_format='srt'): """ - Returns transcript in *.srt format. + Returns transcript, filename and MIME type. Raises: - NotFoundError if cannot find transcript file in storage. - ValueError if transcript file is empty or incorrect JSON. - KeyError if transcript file has incorrect format. + + If language is 'en', self.sub should be correct subtitles name. + If language is 'en', but if self.sub is not defined, this means that we + should search for video name in order to get proper transcript (old style courses). + If language is not 'en', give back transcript in proper language and format. """ lang = self.transcript_language - subs_id = self.sub if lang == 'en' else self.youtube_id_1_0 - data = asset(self.location, subs_id, lang).data - if format == 'txt': - text = json.loads(data)['text'] - str_subs = HTMLParser().unescape("\n".join(text)) - mime_type = 'text/plain' + + if lang == 'en': + if self.sub: # HTML5 case and (Youtube case for new style videos) + transcript_name = self.sub + elif self.youtube_id_1_0: # old courses + transcript_name = self.youtube_id_1_0 + else: + log.debug("No subtitles for 'en' language") + raise ValueError + + data = Transcript.asset(self.location, transcript_name, lang).data + filename = '{}.{}'.format(transcript_name, transcript_format) + content = Transcript.convert(data, 'sjson', transcript_format) else: - str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0) - mime_type = 'application/x-subrip' - if not str_subs: - log.debug('generate_srt_from_sjson produces no subtitles') + data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data + filename = '{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format) + content = Transcript.convert(data, 'srt', transcript_format) + + if not content: + log.debug('no subtitles produced in get_transcript') raise ValueError - return str_subs, format, mime_type + mime_type = 'text/plain' if transcript_format == 'txt' else 'application/x-subrip' + + return content, filename, mime_type + @XBlock.handler def transcript(self, request, dispatch): @@ -384,34 +400,31 @@ class VideoModule(VideoFields, XModule): elif dispatch == 'download': try: - subs, format, mime_type = self.get_transcript(format=self.transcript_download_format) + transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(self.transcript_download_format) except (NotFoundError, ValueError, KeyError): log.debug("Video@download exception") response = Response(status=404) else: response = Response( - subs, + transcript_content, headerlist=[ - ('Content-Disposition', 'attachment; filename="{filename}.{format}"'.format( - filename=self.transcript_language, - format=format, - )), + ('Content-Disposition', 'attachment; filename="{}"'.format(transcript_filename)), ] ) - response.content_type = mime_type + response.content_type = transcript_mime_type elif dispatch == 'available_translations': available_translations = [] if self.sub: # check if sjson exists for 'en'. try: - asset(self.location, self.sub, 'en') + Transcript.asset(self.location, self.sub, 'en') except NotFoundError: pass else: available_translations = ['en'] for lang in self.transcripts: try: - asset(self.location, None, None, self.transcripts[lang]) + Transcript.asset(self.location, None, None, self.transcripts[lang]) except NotFoundError: continue available_translations.append(lang) @@ -462,13 +475,13 @@ class VideoModule(VideoFields, XModule): if youtube_id: # Youtube case: if self.transcript_language == 'en': - return asset(self.location, youtube_id).data + return Transcript.asset(self.location, youtube_id).data youtube_ids = youtube_speed_dict(self) assert youtube_id in youtube_ids try: - sjson_transcript = asset(self.location, youtube_id, self.transcript_language).data + sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data except (NotFoundError): log.info("Can't find content in storage for %s transcript: generating.", youtube_id) generate_sjson_for_all_speeds( @@ -477,19 +490,17 @@ class VideoModule(VideoFields, XModule): {speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()}, self.transcript_language ) - sjson_transcript = asset(self.location, youtube_id, self.transcript_language).data + sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data return sjson_transcript else: # HTML5 case if self.transcript_language == 'en': - return asset(self.location, self.sub).data + return Transcript.asset(self.location, self.sub).data else: return get_or_create_sjson(self) - - class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor): """Descriptor for `VideoModule`.""" module_class = VideoModule diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index c5df8c23e4..002da92636 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -82,9 +82,9 @@ Feature: LMS Video component # 10 Scenario: Language menu works correctly in Video component Given I am registered for the course "test_course" - And I have a "chinese_transcripts.srt" transcript file in assets - And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets - And it has a video in "Youtube" mode: + And I have a "chinese_transcripts.srt" transcript file in assets + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Youtube" mode: | transcripts | sub | | {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | And I make sure captions are closed @@ -97,8 +97,8 @@ Feature: LMS Video component # 11 Scenario: CC button works correctly w/o english transcript in HTML5 mode of Video component Given I am registered for the course "test_course" - And I have a "chinese_transcripts.srt" transcript file in assets - And it has a video in "HTML5" mode: + And I have a "chinese_transcripts.srt" transcript file in assets + And it has a video in "HTML5" mode: | transcripts | | {"zh": "chinese_transcripts.srt"} | And I make sure captions are opened @@ -181,11 +181,11 @@ Feature: LMS Video component | track | download_track | | http://example.org/ | true | And I open the section with videos - And I can download transcript in "srt" format + Then I can download transcript in "srt" format and has text "00:00:00,270" And I select the transcript format "txt" - And I can download transcript in "txt" format + Then I can download transcript in "txt" format and has text "Hi, welcome to Edx." When I open video "B" - Then I can download transcript in "txt" format + Then I can download transcript in "txt" format and has text "Hi, welcome to Edx." When I open video "C" Then menu "download_transcript" doesn't exist @@ -203,3 +203,47 @@ Feature: LMS Video component And I reload the page Then I see "Hi, welcome to Edx." text in the captions And I see duration "1:00" + + # 21 + Scenario: Download button works correctly for non-english transcript in Youtube mode of Video component + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Youtube" mode: + | transcripts | sub | download_track | + | {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true | + And I select language with code "zh" + And I see "好 各位同学" text in the captions + Then I can download transcript in "srt" format and has text "好 各位同学" + + # 22 + Scenario: Download button works correctly for non-english transcript in HTML5 mode of Video component + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "HTML5" mode: + | transcripts | sub | download_track | + | {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true | + And I select language with code "zh" + And I see "好 各位同学" text in the captions + Then I can download transcript in "srt" format and has text "好 各位同学" + + # 23 + Scenario: Download button works correctly w/o english transcript in HTML5 mode of Video component + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And it has a video in "HTML5" mode: + | transcripts | download_track | + | {"zh": "chinese_transcripts.srt"} | true | + And I see "好 各位同学" text in the captions + Then I can download transcript in "srt" format and has text "好 各位同学" + + # 24 + Scenario: Download button works correctly w/o english transcript in Youtube mode of Video component + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And it has a video in "Youtube" mode: + | transcripts | download_track | + | {"zh": "chinese_transcripts.srt"} | true | + And I see "好 各位同学" text in the captions + Then I can download transcript in "srt" format and has text "好 各位同学" \ No newline at end of file diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 530333c711..91be1e015f 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -2,16 +2,17 @@ #pylint: disable=C0111 from lettuce import world, step -import json import os -import requests +import json import time +import requests from common import i_am_registered_for_the_course, section_location, visit_scenario_item from django.utils.translation import ugettext as _ from django.conf import settings from cache_toolbox.core import del_cached_content from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore + TEST_ROOT = settings.COMMON_TEST_DATA_ROOT LANGUAGES = settings.ALL_LANGUAGES @@ -54,7 +55,7 @@ class ReuqestHandlerWithSessionId(object): """ kwargs = dict() - session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name']==u'sessionid'] + session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name'] == u'sessionid'] if session_id: kwargs.update({ 'cookies': session_id[0] @@ -118,7 +119,7 @@ def add_video_to_course(course, player_mode, hashes, display_name='Video'): }) if player_mode == 'youtube_html5': kwargs['metadata'].update({ - 'html5_sources': HTML5_SOURCES + 'html5_sources': HTML5_SOURCES, }) if player_mode == 'youtube_html5_unsupported_video': kwargs['metadata'].update({ @@ -166,7 +167,6 @@ def _change_video_speed(speed): speed_css = 'li[data-speed="{0}"] a'.format(speed) world.css_click(speed_css) - def _open_menu(menu): world.browser.execute_script("$('{selector}').parent().addClass('open')".format( selector=VIDEO_MENUS[menu] @@ -333,11 +333,11 @@ def set_captions_visibility_state(_step, captions_state): def i_see_menu(_step, menu): _open_menu(menu) menu_items = world.css_find(VIDEO_MENUS[menu] + ' li') - Video = world.scenario_dict['VIDEO'] - transcripts = dict(Video.transcripts) - if Video.sub: + video = world.scenario_dict['VIDEO'] + transcripts = dict(video.transcripts) + if video.sub: transcripts.update({ - 'en': Video.sub + 'en': video.sub }) languages = {i[0]: i[1] for i in LANGUAGES} @@ -359,9 +359,8 @@ def select_language(_step, code): selector = VIDEO_MENUS["language"] + ' li[data-lang-code={code}]'.format( code=code ) - item = world.css_find(selector) - item.click() + world.css_click(selector) assert world.css_has_class(selector, 'active') assert len(world.css_find(VIDEO_MENUS["language"] + ' li.active')) == 1 @@ -406,6 +405,7 @@ def upload_to_assets(_step, filename): def is_hidden_button(_step, button): assert not world.css_visible(VIDEO_BUTTONS[button]) + @step('menu "([^"]*)" doesn\'t exist$') def is_hidden_menu(_step, menu): assert world.is_css_not_present(VIDEO_MENUS[menu]) @@ -435,27 +435,21 @@ def video_alignment(_step, transcript_visibility): assert all([width, height]) -@step('I can download transcript in "([^"]*)" format$') -def i_can_download_transcript(_step, format): +@step('I can download transcript in "([^"]*)" format and has text "([^"]*)"$') +def i_can_download_transcript(_step, format, text): button = world.css_find('.video-tracks .a11y-menu-button').first assert button.text.strip() == '.' + format formats = { - 'srt': { - 'content': '0\n00:00:00,270', - 'mime_type': 'application/x-subrip' - }, - 'txt': { - 'content': 'Hi, welcome to Edx.', - 'mime_type': 'text/plain' - }, + 'srt': 'application/x-subrip', + 'txt': 'text/plain', } url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href'] request = ReuqestHandlerWithSessionId() assert request.get(url).is_success() - assert request.check_header('content-type', formats[format]['mime_type']) - assert request.content.startswith(formats[format]['content']) + assert request.check_header('content-type', formats[format]) + assert (text.encode('utf-8') in request.content) @step('I select the transcript format "([^"]*)"$') diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index 7447fb5b82..7d9998272c 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -135,10 +135,69 @@ class TestVideo(BaseTestXmodule): def tearDown(self): _clear_assets(self.item_descriptor.location) - -class TestVideoTranscriptTranslation(TestVideo): +class TestTranscriptAvailableTranslationsDispatch(TestVideo): """ - Test video handlers that provide translation transcripts. + Test video handler that provide available translations info. + + Tests for `available_translations` dispatch. + """ + non_en_file = _create_srt_file() + DATA = """ + + """.format(os.path.split(non_en_file.name)[1]) + + MODEL_DATA = { + 'data': DATA + } + + def setUp(self): + super(TestTranscriptAvailableTranslationsDispatch, self).setUp() + self.item_descriptor.render('student_view') + self.item = self.item_descriptor.xmodule_runtime.xmodule_instance + self.subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} + + def test_available_translation_en(self): + good_sjson = _create_file(json.dumps(self.subs)) + _upload_sjson_file(good_sjson, self.item_descriptor.location) + self.item.sub = _get_subs_id(good_sjson.name) + + request = Request.blank('/translation') + response = self.item.transcript(request=request, dispatch='available_translations') + self.assertEqual(json.loads(response.body), ['en']) + + def test_available_translation_non_en(self): + _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + + request = Request.blank('/translation') + response = self.item.transcript(request=request, dispatch='available_translations') + self.assertEqual(json.loads(response.body), ['uk']) + + def test_multiple_available_translations(self): + good_sjson = _create_file(json.dumps(self.subs)) + + # Upload english transcript. + _upload_sjson_file(good_sjson, self.item_descriptor.location) + + # Upload non-english transcript. + _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + + self.item.sub = _get_subs_id(good_sjson.name) + + request = Request.blank('/translation') + response = self.item.transcript(request=request, dispatch='available_translations') + self.assertEqual(json.loads(response.body), ['en', 'uk']) + +class TestTranscriptDownloadDispatch(TestVideo): + """ + Test video handler that provide translation transcripts. + + Tests for `download` dispatch. """ non_en_file = _create_srt_file() @@ -157,11 +216,10 @@ class TestVideoTranscriptTranslation(TestVideo): } def setUp(self): - super(TestVideoTranscriptTranslation, self).setUp() + super(TestTranscriptDownloadDispatch, self).setUp() self.item_descriptor.render('student_view') self.item = self.item_descriptor.xmodule_runtime.xmodule_instance - # Tests for `download` dispatch: def test_language_is_not_supported(self): request = Request.blank('/download?language=ru') @@ -173,7 +231,7 @@ class TestVideoTranscriptTranslation(TestVideo): response = self.item.transcript(request=request, dispatch='download') self.assertEqual(response.status, '404 Not Found') - @patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'srt', 'application/x-subrip')) + @patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'test_filename.srt', 'application/x-subrip')) def test_download_srt_exist(self, __): request = Request.blank('/download?language=en') response = self.item.transcript(request=request, dispatch='download') @@ -195,7 +253,32 @@ class TestVideoTranscriptTranslation(TestVideo): with self.assertRaises(NotFoundError): self.item.get_transcript() - # Tests for `translation` dispatch: +class TestTranscriptTranslationDispatch(TestVideo): + """ + Test video handler that provide translation transcripts. + + Tests for `translation` dispatch. + """ + + non_en_file = _create_srt_file() + DATA = """ + + """.format(os.path.split(non_en_file.name)[1]) + + MODEL_DATA = { + 'data': DATA + } + + def setUp(self): + super(TestTranscriptTranslationDispatch, self).setUp() + self.item_descriptor.render('student_view') + self.item = self.item_descriptor.xmodule_runtime.xmodule_instance def test_translation_fails(self): # No language @@ -295,30 +378,35 @@ class TestVideoTranscriptTranslation(TestVideo): self.assertDictEqual(json.loads(response.body), subs) -class TestVideoTranscriptsDownload(TestVideo): +class TestGetTranscript(TestVideo): """ Make sure that `get_transcript` method works correctly """ - + non_en_file = _create_srt_file() DATA = """ - """ + """.format(os.path.split(non_en_file.name)[1]) + MODEL_DATA = { 'data': DATA } METADATA = {} def setUp(self): - super(TestVideoTranscriptsDownload, self).setUp() + super(TestGetTranscript, self).setUp() self.item_descriptor.render('student_view') self.item = self.item_descriptor.xmodule_runtime.xmodule_instance - def test_good_srt_transcript(self): + def test_good_transcript(self): + """ + Test for download 'en' sub with html5 video and self.sub has correct non-empty value. + """ good_sjson = _create_file(content=textwrap.dedent("""\ { "start": [ @@ -338,7 +426,9 @@ class TestVideoTranscriptsDownload(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.sub = _get_subs_id(good_sjson.name) - text, format, download = self.item.get_transcript() + + text, filename, mime_type = self.item.get_transcript() + expected_text = textwrap.dedent("""\ 0 00:00:00,270 --> 00:00:02,720 @@ -351,6 +441,8 @@ class TestVideoTranscriptsDownload(TestVideo): """) self.assertEqual(text, expected_text) + self.assertEqual(filename[:-4], self.item.sub) + self.assertEqual(mime_type, 'application/x-subrip') def test_good_txt_transcript(self): good_sjson = _create_file(content=textwrap.dedent("""\ @@ -372,17 +464,77 @@ class TestVideoTranscriptsDownload(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.sub = _get_subs_id(good_sjson.name) - text, format, mime_type = self.item.get_transcript(format="txt") + text, filename, mime_type = self.item.get_transcript("txt") expected_text = textwrap.dedent("""\ Hi, welcome to Edx. Let's start with what is on your screen right now.""") self.assertEqual(text, expected_text) + self.assertEqual(filename, self.item.sub + '.txt') + self.assertEqual(mime_type, 'text/plain') - def test_not_found_error(self): + def test_en_with_empty_sub(self): + + # no self.sub, self.youttube_1_0 exist, but no file in assets with self.assertRaises(NotFoundError): self.item.get_transcript() + # no self.sub and no self.youtube_1_0 + self.item.youtube_id_1_0 = None + with self.assertRaises(ValueError): + self.item.get_transcript() + + # no self.sub but youtube_1_0 exists with file in assets + good_sjson = _create_file(content=textwrap.dedent("""\ + { + "start": [ + 270, + 2720 + ], + "end": [ + 2720, + 5430 + ], + "text": [ + "Hi, welcome to Edx.", + "Let's start with what is on your screen right now." + ] + } + """)) + _upload_sjson_file(good_sjson, self.item.location) + self.item.youtube_id_1_0 = _get_subs_id(good_sjson.name) + + text, filename, mime_type = self.item.get_transcript() + expected_text = textwrap.dedent("""\ + 0 + 00:00:00,270 --> 00:00:02,720 + Hi, welcome to Edx. + + 1 + 00:00:02,720 --> 00:00:05,430 + Let's start with what is on your screen right now. + + """) + + self.assertEqual(text, expected_text) + self.assertEqual(filename, self.item.youtube_id_1_0 + '.srt') + self.assertEqual(mime_type, 'application/x-subrip') + + def test_non_en(self): + self.item.transcript_language = 'uk' + self.non_en_file.seek(0) + _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + + text, filename, mime_type = self.item.get_transcript() + expected_text = textwrap.dedent(""" + 0 + 00:00:00,12 --> 00:00:00,100 + Привіт, edX вітає вас. + """) + self.assertEqual(text, expected_text) + self.assertEqual(filename, os.path.split(self.non_en_file.name)[1]) + self.assertEqual(mime_type, 'application/x-subrip') + def test_value_error(self): good_sjson = _create_file(content='bad content') diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 38e69afa35..ecd8ff84c6 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -42,7 +42,7 @@ class TestVideoYouTube(TestVideo): 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': 'en', + 'transcript_language': u'en', 'transcript_languages': '{"en": "English", "uk": "Ukrainian"}', 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript' @@ -51,6 +51,7 @@ class TestVideoYouTube(TestVideo): self.item_descriptor, 'transcript' ).rstrip('/?') + '/available_translations', } + self.assertEqual( context, self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context), @@ -106,7 +107,7 @@ class TestVideoNonYouTube(TestVideo): 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': 'en', + 'transcript_language': u'en', 'transcript_languages': '{"en": "English"}', 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript' @@ -143,6 +144,7 @@ class TestGetHtmlMethod(BaseTestXmodule): {track} + {transcripts} """ @@ -152,24 +154,35 @@ class TestGetHtmlMethod(BaseTestXmodule): 'track': u'', 'sub': u'a_sub_file.srt.sjson', 'expected_track_url': u'http://www.example.com/track', + 'transcripts': '', }, { 'download_track': u'true', 'track': u'', 'sub': u'a_sub_file.srt.sjson', 'expected_track_url': u'a_sub_file.srt.sjson', + 'transcripts': '', }, { 'download_track': u'true', 'track': u'', 'sub': u'', - 'expected_track_url': None + 'expected_track_url': None, + 'transcripts': '', }, { 'download_track': u'false', 'track': u'', 'sub': u'a_sub_file.srt.sjson', 'expected_track_url': None, + 'transcripts': '', + }, + { + 'download_track': u'true', + 'track': u'', + 'sub': u'', + 'expected_track_url': u'a_sub_file.srt.sjson', + 'transcripts': '', }, ] @@ -201,7 +214,8 @@ class TestGetHtmlMethod(BaseTestXmodule): DATA = SOURCE_XML.format( download_track=data['download_track'], track=data['track'], - sub=data['sub'] + sub=data['sub'], + transcripts=data['transcripts'], ) self.initialize_module(data=DATA) @@ -213,8 +227,8 @@ class TestGetHtmlMethod(BaseTestXmodule): expected_context.update({ 'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt', - 'transcript_languages': '{"en": "English"}', - 'transcript_language': 'en', + 'transcript_languages': '{"en": "English"}' if not data['transcripts'] else '{"uk": "Ukrainian"}', + 'transcript_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk', 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript' ).rstrip('/?') + '/translation', @@ -312,7 +326,7 @@ class TestGetHtmlMethod(BaseTestXmodule): 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': 'en', + 'transcript_language': u'en', 'transcript_languages': '{"en": "English"}', }