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"}',
}