Commit 25407ef3 by Alexander Kryklia

BLD-642: Allow multiple transcripts with video.

parent 0b262e7c
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Blades: Allow multiple transcripts with video. BLD-642.
CMS: Add feature to allow exporting a course to a git repository by CMS: Add feature to allow exporting a course to a git repository by
specifying the giturl in the course settings. specifying the giturl in the course settings.
......
...@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set): ...@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
# Check if the web object is a list type # Check if the web object is a list type
# If so, we use a slightly different mechanism for determining its value # If so, we use a slightly different mechanism for determining its value
if setting.has_class('metadata-list-enum'): if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict'):
list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item')) list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
assert_equal(value, list_value) assert_equal(value, list_value)
elif setting.has_class('metadata-videolist-enum'): elif setting.has_class('metadata-videolist-enum'):
......
...@@ -103,7 +103,7 @@ def i_do_not_see_error_message(_step): ...@@ -103,7 +103,7 @@ def i_do_not_see_error_message(_step):
@step('I see error message "([^"]*)"$') @step('I see error message "([^"]*)"$')
def i_see_error_message(_step, error): def i_see_error_message(_step, error):
assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error.strip()]) assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error])
@step('I do not see status message$') @step('I do not see status message$')
...@@ -114,7 +114,7 @@ def i_do_not_see_status_message(_step): ...@@ -114,7 +114,7 @@ def i_do_not_see_status_message(_step):
@step('I see status message "([^"]*)"$') @step('I see status message "([^"]*)"$')
def i_see_status_message(_step, status): def i_see_status_message(_step, status):
assert not world.css_visible(SELECTORS['error_bar']) assert not world.css_visible(SELECTORS['error_bar'])
assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status.strip()]) assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status])
DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0] DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0]
if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) \ if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) \
......
@shard_3 @shard_3
Feature: CMS.Video Component Editor Feature: CMS Video Component Editor
As a course author, I want to be able to create video components. As a course author, I want to be able to create video components
Scenario: User can view Video metadata Scenario: User can view Video metadata
Given I have created a Video component Given I have created a Video component
...@@ -17,14 +17,14 @@ Feature: CMS.Video Component Editor ...@@ -17,14 +17,14 @@ Feature: CMS.Video Component Editor
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are hidden when "show captions" is false Scenario: Captions are hidden when "transcript display" is false
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And I have set "show transcript" to False And I have set "transcript display" to False
Then when I view the video it does not show the captions Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are shown when "show captions" is true Scenario: Captions are shown when "transcript display" is true
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And I have set "show transcript" to True And I have set "transcript display" to True
Then when I view the video it does show the captions Then when I view the video it does show the captions
...@@ -5,7 +5,7 @@ from lettuce import world, step ...@@ -5,7 +5,7 @@ from lettuce import world, step
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
@step('I have set "show transcript" to (.*)$') @step('I have set "transcript display" to (.*)$')
def set_show_captions(step, setting): def set_show_captions(step, setting):
# Prevent cookies from overriding course settings # Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions') world.browser.cookies.delete('hide_captions')
...@@ -13,7 +13,7 @@ def set_show_captions(step, setting): ...@@ -13,7 +13,7 @@ def set_show_captions(step, setting):
world.css_click('a.edit-button') world.css_click('a.edit-button')
world.wait_for(lambda _driver: world.css_visible('a.save-button')) world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.click_link_by_text('Advanced') world.click_link_by_text('Advanced')
world.browser.select('Show Transcript', setting) world.browser.select('Transcript Display', setting)
world.css_click('a.save-button') world.css_click('a.save-button')
...@@ -42,10 +42,11 @@ def correct_video_settings(_step): ...@@ -42,10 +42,11 @@ def correct_video_settings(_step):
['Display Name', 'Video', False], ['Display Name', 'Video', False],
['Download Transcript', '', False], ['Download Transcript', '', False],
['End Time', '00:00:00', False], ['End Time', '00:00:00', False],
['HTML5 Transcript', '', False],
['Show Transcript', 'True', False],
['Start Time', '00:00:00', False], ['Start Time', '00:00:00', False],
['Transcript (primary)', '', False],
['Transcript Display', 'True', False],
['Transcript Download Allowed', 'False', False], ['Transcript Download Allowed', 'False', False],
['Transcript Translations', '', False],
['Video Download Allowed', 'False', False], ['Video Download Allowed', 'False', False],
['Video Sources', '', False], ['Video Sources', '', False],
['Youtube ID', 'OEoXaMPEzfM', False], ['Youtube ID', 'OEoXaMPEzfM', False],
......
@shard_3 @shard_3
Feature: CMS.Video Component Feature: CMS Video Component
As a course author, I want to be able to view my created videos in Studio. As a course author, I want to be able to view my created videos in Studio
# 1 # 1
# Video Alpha Features will work in Firefox only when Firefox is the active window # Video Alpha Features will work in Firefox only when Firefox is the active window
...@@ -43,38 +43,6 @@ Feature: CMS.Video Component ...@@ -43,38 +43,6 @@ Feature: CMS.Video Component
Then the correct Youtube video is shown Then the correct Youtube video is shown
# 7 # 7
Scenario: Closed captions become visible when the mouse hovers over CC button
Given I have created a Video component with subtitles
And Make sure captions are closed
Then Captions become "invisible"
And I hover over button "CC"
Then Captions become "visible"
And I hover over button "volume"
Then Captions become "invisible"
# 8
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: Open captions never become invisible
Given I have created a Video component with subtitles
And Make sure captions are open
Then Captions are "visible"
And I hover over button "CC"
Then Captions are "visible"
And I hover over button "volume"
Then Captions are "visible"
# 9
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: Closed captions are invisible when mouse doesn't hover on CC button
Given I have created a Video component with subtitles
And Make sure captions are closed
Then Captions become "invisible"
And I hover over button "volume"
Then Captions are "invisible"
# 10
# Disabled 11/26 due to flakiness in master. # Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29. # Enabled back on 11/29.
Scenario: When enter key is pressed on a caption shows an outline around it Scenario: When enter key is pressed on a caption shows an outline around it
...@@ -84,7 +52,7 @@ Feature: CMS.Video Component ...@@ -84,7 +52,7 @@ Feature: CMS.Video Component
Then I press "enter" button on caption line with data-index "0" Then I press "enter" button on caption line with data-index "0"
And I see caption line with data-index "0" has class "focused" And I see caption line with data-index "0" has class "focused"
# 11 # 8
Scenario: When start end end times are specified, a range on slider is shown Scenario: When start end end times are specified, a range on slider is shown
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And Make sure captions are closed And Make sure captions are closed
......
...@@ -56,6 +56,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id): ...@@ -56,6 +56,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
world.visit(video_url) world.visit(video_url)
world.wait_for_xmodule() world.wait_for_xmodule()
# update .sub filed with proper subs name (which mimics real Studio/XML behavior)
# this is needed only for that videos which are created in acceptance tests.
_step.given('I edit the component')
world.wait_for_ajax_complete()
_step.given('I save changes')
world.disable_jquery_animations() world.disable_jquery_animations()
world.wait_for_present('.is-initialized') world.wait_for_present('.is-initialized')
......
...@@ -492,15 +492,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -492,15 +492,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, 'i4x://edX/toy/video/video_with_end_time') self.assertContains(resp, 'i4x://edX/toy/video/video_with_end_time')
self.assertContains(resp, 'i4x://edX/toy/poll_question/T1_changemind_poll_foo_2') self.assertContains(resp, 'i4x://edX/toy/poll_question/T1_changemind_poll_foo_2')
def test_video_module_caption_asset_path(self):
"""
This verifies that a video caption url is as we expect it to be
"""
resp = self._test_preview(Location('i4x', 'edX', 'toy', 'video', 'sample_video', None))
self.assertEquals(resp.status_code, 200)
content = json.loads(resp.content)
self.assertIn('data-caption-asset-path="/c4x/edX/toy/asset/subs_"', content['html'])
def _test_preview(self, location): def _test_preview(self, location):
""" Preview test case. """ """ Preview test case. """
direct_store = modulestore('direct') direct_store = modulestore('direct')
......
...@@ -3,11 +3,13 @@ import unittest ...@@ -3,11 +3,13 @@ import unittest
from uuid import uuid4 from uuid import uuid4
import copy import copy
import textwrap import textwrap
from mock import patch, Mock
from pymongo import MongoClient from pymongo import MongoClient
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from django.utils import translation
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
...@@ -16,7 +18,7 @@ from xmodule.contentstore.content import StaticContent ...@@ -16,7 +18,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.contentstore.django import contentstore, _CONTENTSTORE from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from contentstore import transcripts_utils from xmodule.video_module import transcripts_utils
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
...@@ -188,20 +190,29 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -188,20 +190,29 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
def test_success_downloading_subs(self): def test_success_downloading_subs(self):
# Disabled 11/14/13 response = textwrap.dedent("""<?xml version="1.0" encoding="utf-8" ?>
# This test is flakey because it performs an HTTP request on an external service <transcript>
# Re-enable when `requests.get` is patched using `mock.patch` <text start="0" dur="0.27"></text>
raise SkipTest <text start="0.27" dur="2.45">Test text 1.</text>
<text start="2.72">Test text 2.</text>
<text start="5.43" dur="1.73">Test text 3.</text>
</transcript>
""")
good_youtube_subs = { good_youtube_subs = {
0.5: 'JMD_ifUUfsU', 0.5: 'good_id_1',
1.0: 'hI10vDNYz4M', 1.0: 'good_id_2',
2.0: 'AKqURZnYqpk' 2.0: 'good_id_3'
} }
self.clear_subs_content(good_youtube_subs) self.clear_subs_content(good_youtube_subs)
# Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
transcripts_utils.download_youtube_subs(good_youtube_subs, self.course) mock_get.return_value = Mock(status_code=200, text=response, content=response)
# Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
transcripts_utils.download_youtube_subs(good_youtube_subs, self.course, settings)
mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_1'})
mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_2'})
mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_3'})
# Check assets status after importing subtitles. # Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values(): for subs_id in good_youtube_subs.values():
...@@ -226,12 +237,10 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -226,12 +237,10 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self.assertEqual(html5_ids[2], 'baz.1.4') self.assertEqual(html5_ids[2], 'baz.1.4')
self.assertEqual(html5_ids[3], 'foo') self.assertEqual(html5_ids[3], 'foo')
def test_fail_downloading_subs(self): @patch('xmodule.video_module.transcripts_utils.requests.get')
def test_fail_downloading_subs(self, mock_get):
# Disabled 11/14/13 mock_get.return_value = Mock(status_code=404, text='Error 404')
# This test is flakey because it performs an HTTP request on an external service
# Re-enable when `requests.get` is patched using `mock.patch`
raise SkipTest
bad_youtube_subs = { bad_youtube_subs = {
0.5: 'BAD_YOUTUBE_ID1', 0.5: 'BAD_YOUTUBE_ID1',
...@@ -239,9 +248,8 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -239,9 +248,8 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
2.0: 'BAD_YOUTUBE_ID3' 2.0: 'BAD_YOUTUBE_ID3'
} }
self.clear_subs_content(bad_youtube_subs) self.clear_subs_content(bad_youtube_subs)
with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException): with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course) transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course, settings)
# Check assets status after importing subtitles. # Check assets status after importing subtitles.
for subs_id in bad_youtube_subs.values(): for subs_id in bad_youtube_subs.values():
...@@ -267,7 +275,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -267,7 +275,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self.clear_subs_content(good_youtube_subs) self.clear_subs_content(good_youtube_subs)
# Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
transcripts_utils.download_youtube_subs(good_youtube_subs, self.course) transcripts_utils.download_youtube_subs(good_youtube_subs, self.course, settings)
# Check assets status after importing subtitles. # Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values(): for subs_id in good_youtube_subs.values():
...@@ -438,3 +446,43 @@ class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs): ...@@ -438,3 +446,43 @@ class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs):
} }
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1) srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1)
self.assertFalse(srt_subs) self.assertFalse(srt_subs)
class TestYoutubeTranscripts(unittest.TestCase):
"""
Tests for checking right datastructure returning when using youtube api.
"""
@patch('xmodule.video_module.transcripts_utils.requests.get')
def test_youtube_bad_status_code(self, mock_get):
mock_get.return_value = Mock(status_code=404, text='test')
youtube_id = 'bad_youtube_id'
with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
@patch('xmodule.video_module.transcripts_utils.requests.get')
def test_youtube_empty_text(self, mock_get):
mock_get.return_value = Mock(status_code=200, text='')
youtube_id = 'bad_youtube_id'
with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
def test_youtube_good_result(self):
response = textwrap.dedent("""<?xml version="1.0" encoding="utf-8" ?>
<transcript>
<text start="0" dur="0.27"></text>
<text start="0.27" dur="2.45">Test text 1.</text>
<text start="2.72">Test text 2.</text>
<text start="5.43" dur="1.73">Test text 3.</text>
</transcript>
""")
expected_transcripts = {
'start': [270, 2720, 5430],
'end': [2720, 2720, 7160],
'text': ['Test text 1.', 'Test text 2.', 'Test text 3.']
}
youtube_id = 'good_youtube_id'
with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
mock_get.return_value = Mock(status_code=200, text=response, content=response)
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'})
...@@ -24,12 +24,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr ...@@ -24,12 +24,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.video_module import manage_video_subtitles_save
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool from util.string_utils import str_to_bool
from ..transcripts_utils import manage_video_subtitles_save
from ..utils import get_modulestore from ..utils import get_modulestore
from .access import has_course_access from .access import has_course_access
...@@ -251,6 +250,8 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta ...@@ -251,6 +250,8 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
log.error("Can't find item by location.") log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404) return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
old_metadata = own_metadata(existing_item)
if publish: if publish:
if publish == 'make_private': if publish == 'make_private':
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location)) _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
...@@ -299,7 +300,7 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta ...@@ -299,7 +300,7 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
field.write_to(existing_item, value) field.write_to(existing_item, value)
if existing_item.category == 'video': if existing_item.category == 'video':
manage_video_subtitles_save(existing_item, existing_item, request.user) manage_video_subtitles_save(existing_item, request.user, old_metadata, generate_translation=True)
# commit to datastore # commit to datastore
store.update_item(existing_item, request.user.id) store.update_item(existing_item, request.user.id)
......
...@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse ...@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from contentstore import transcripts_utils from xmodule.video_module import transcripts_utils
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from cache_toolbox.core import del_cached_content from cache_toolbox.core import del_cached_content
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
......
...@@ -26,12 +26,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr ...@@ -26,12 +26,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
from util.json_request import JsonResponse from util.json_request import JsonResponse
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from ..transcripts_utils import ( from xmodule.video_module.transcripts_utils import (
generate_subs_from_source, generate_subs_from_source,
generate_srt_from_sjson, remove_subs_from_store, generate_srt_from_sjson, remove_subs_from_store,
download_youtube_subs, get_transcripts_from_youtube, download_youtube_subs, get_transcripts_from_youtube,
copy_or_rename_transcript, copy_or_rename_transcript,
save_module,
manage_video_subtitles_save, manage_video_subtitles_save,
TranscriptsGenerationException, TranscriptsGenerationException,
GetTranscriptsFromYouTubeException, GetTranscriptsFromYouTubeException,
...@@ -136,7 +135,7 @@ def upload_transcripts(request): ...@@ -136,7 +135,7 @@ def upload_transcripts(request):
return error_response(response, "Can't find transcripts in storage for {}".format(sub_attr)) return error_response(response, "Can't find transcripts in storage for {}".format(sub_attr))
item.sub = selected_name # write one of new subtitles names to item.sub attribute. item.sub = selected_name # write one of new subtitles names to item.sub attribute.
save_module(item, request.user) item.save_with_metadata(request.user)
response['subs'] = item.sub response['subs'] = item.sub
response['status'] = 'Success' response['status'] = 'Success'
else: else:
...@@ -272,7 +271,11 @@ def check_transcripts(request): ...@@ -272,7 +271,11 @@ def check_transcripts(request):
#check youtube local and server transcripts for equality #check youtube local and server transcripts for equality
if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']: if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
try: try:
youtube_server_subs = get_transcripts_from_youtube(youtube_id) youtube_server_subs = get_transcripts_from_youtube(
youtube_id,
settings,
item.runtime.service(item, "i18n")
)
if json.loads(local_transcripts) == youtube_server_subs: # check transcripts for equality if json.loads(local_transcripts) == youtube_server_subs: # check transcripts for equality
transcripts_presence['youtube_diff'] = False transcripts_presence['youtube_diff'] = False
except GetTranscriptsFromYouTubeException: except GetTranscriptsFromYouTubeException:
...@@ -389,7 +392,7 @@ def choose_transcripts(request): ...@@ -389,7 +392,7 @@ def choose_transcripts(request):
if item.sub != html5_id: # update sub value if item.sub != html5_id: # update sub value
item.sub = html5_id item.sub = html5_id
save_module(item, request.user) item.save_with_metadata(request.user)
response = {'status': 'Success', 'subs': item.sub} response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response) return JsonResponse(response)
...@@ -415,12 +418,12 @@ def replace_transcripts(request): ...@@ -415,12 +418,12 @@ def replace_transcripts(request):
return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id)) return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id))
try: try:
download_youtube_subs({1.0: youtube_id}, item) download_youtube_subs({1.0: youtube_id}, item, settings)
except GetTranscriptsFromYouTubeException as e: except GetTranscriptsFromYouTubeException as e:
return error_response(response, e.message) return error_response(response, e.message)
item.sub = youtube_id item.sub = youtube_id
save_module(item, request.user) item.save_with_metadata(request.user)
response = {'status': 'Success', 'subs': item.sub} response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response) return JsonResponse(response)
...@@ -519,10 +522,10 @@ def save_transcripts(request): ...@@ -519,10 +522,10 @@ def save_transcripts(request):
for metadata_key, value in metadata.items(): for metadata_key, value in metadata.items():
setattr(item, metadata_key, value) setattr(item, metadata_key, value)
save_module(item, request.user) # item becomes updated with new values item.save_with_metadata(request.user) # item becomes updated with new values
if new_sub: if new_sub:
manage_video_subtitles_save(None, item, request.user) manage_video_subtitles_save(item, request.user)
else: else:
# If `new_sub` is empty, it means that user explicitly does not want to use # If `new_sub` is empty, it means that user explicitly does not want to use
# transcripts for current video ids and we remove all transcripts from storage. # transcripts for current video ids and we remove all transcripts from storage.
......
...@@ -26,7 +26,9 @@ Longer TODO: ...@@ -26,7 +26,9 @@ Longer TODO:
import sys import sys
import lms.envs.common import lms.envs.common
from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites from lms.envs.common import (
USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites, ALL_LANGUAGES
)
from path import path from path import path
from lms.lib.xblock.mixin import LmsBlockMixin from lms.lib.xblock.mixin import LmsBlockMixin
......
...@@ -11,7 +11,10 @@ ...@@ -11,7 +11,10 @@
data-start="" data-start=""
data-end="" data-end=""
data-saved-video-position="0" data-saved-video-position="0"
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
...@@ -51,7 +54,9 @@ ...@@ -51,7 +54,9 @@
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a> <a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div> </div>
</div> </div>
</section> </section>
......
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
data-start="" data-start=""
data-end="" data-end=""
data-saved-video-position="0" data-saved-video-position="0"
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y" data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4" data-mp4-source="xmodule/include/fixtures/test.mp4"
data-webm-source="xmodule/include/fixtures/test.webm" data-webm-source="xmodule/include/fixtures/test.webm"
...@@ -54,7 +57,9 @@ ...@@ -54,7 +57,9 @@
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a> <a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div> </div>
</div> </div>
</section> </section>
......
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
data-start="" data-start=""
data-end="" data-end=""
data-saved-video-position="0" data-saved-video-position="0"
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y" data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4" data-mp4-source="xmodule/include/fixtures/test.mp4"
data-webm-source="xmodule/include/fixtures/test.webm" data-webm-source="xmodule/include/fixtures/test.webm"
......
...@@ -11,7 +11,10 @@ ...@@ -11,7 +11,10 @@
data-start="" data-start=""
data-end="" data-end=""
data-saved-video-position="0" data-saved-video-position="0"
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
......
...@@ -11,7 +11,10 @@ ...@@ -11,7 +11,10 @@
data-start="" data-start=""
data-end="" data-end=""
data-saved-video-position="0" data-saved-video-position="0"
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
...@@ -51,7 +54,9 @@ ...@@ -51,7 +54,9 @@
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a> <a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div> </div>
</div> </div>
</section> </section>
...@@ -73,9 +78,13 @@ ...@@ -73,9 +78,13 @@
class="video" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true" data-show-captions="true"
data-speed="1.0"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
...@@ -112,7 +121,9 @@ ...@@ -112,7 +121,9 @@
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a> <a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div> </div>
</div> </div>
</section> </section>
...@@ -132,9 +143,13 @@ ...@@ -132,9 +143,13 @@
class="video" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true" data-show-captions="true"
data-speed="1.0"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
...@@ -171,7 +186,9 @@ ...@@ -171,7 +186,9 @@
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a> <a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div> </div>
</div> </div>
</section> </section>
......
(function ($, undefined) { (function ($, undefined) {
var oldAjaxWithPrefix = $.ajaxWithPrefix;
// Stub YouTube API. // Stub YouTube API.
window.YT = { window.YT = {
Player: function () { Player: function () {
...@@ -63,42 +61,6 @@ ...@@ -63,42 +61,6 @@
] ]
}; };
// For our purposes, we need to make sure that the function
// $.ajaxWithPrefix does not fail when during tests a captions file is
// requested. It is originally defined in file:
//
// common/static/coffee/src/ajax_prefix.js
//
// We will replace it with a function that does:
//
// 1.) Return a hard coded captions object if the file name contains
// 'Z5KLxerq05Y'.
// 2.) Behaves the same a as the original function in all other cases.
$.ajaxWithPrefix = function (url, settings) {
var data, success;
if (!settings) {
settings = url;
url = settings.url;
success = settings.success;
data = settings.data;
}
if (
url.match(/Z5KLxerq05Y/g) ||
url.match(/7tqY6eQzVhE/g) ||
url.match(/cogebirgzzM/g)
) {
if ($.isFunction(success)) {
return success(jasmine.stubbedCaption);
} else if ($.isFunction(data)) {
return data(jasmine.stubbedCaption);
}
} else {
return oldAjaxWithPrefix.apply(this, arguments);
}
};
// Time waitsFor() should wait for before failing a test. // Time waitsFor() should wait for before failing a test.
window.WAIT_TIMEOUT = 5000; window.WAIT_TIMEOUT = 5000;
...@@ -145,13 +107,16 @@ ...@@ -145,13 +107,16 @@
jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']; jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'];
jasmine.stubRequests = function () { jasmine.stubRequests = function () {
return spyOn($, 'ajax').andCallFake(function (settings) { var spy = $.ajax;
var match, status, callCallback;
if ( if (!($.ajax.isSpy)) {
match = settings.url spy = spyOn($, 'ajax');
.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/) }
) { return spy.andCallFake(function (settings) {
var match = settings.url
.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/),
status, callCallback;
if (match) {
status = match[1].split('_'); status = match[1].split('_');
if (status && status[0] === 'status') { if (status && status[0] === 'status') {
callCallback = function (callback) { callCallback = function (callback) {
...@@ -177,11 +142,10 @@ ...@@ -177,11 +142,10 @@
} }
}; };
} }
} else if ( } else if (settings.url == '/transcript/translation') {
match = settings.url
.match(/static(\/.*)?\/subs\/(.+)\.srt\.sjson/)
) {
return settings.success(jasmine.stubbedCaption); return settings.success(jasmine.stubbedCaption);
} else if (settings.url == '/transcript/available_translations') {
return settings.success(['uk', 'de']);
} else if (settings.url.match(/.+\/problem_get$/)) { } else if (settings.url.match(/.+\/problem_get$/)) {
return settings.success({ return settings.success({
html: readFixtures('problem_content.html') html: readFixtures('problem_content.html')
...@@ -265,6 +229,7 @@ ...@@ -265,6 +229,7 @@
.data(params); .data(params);
} }
jasmine.stubRequests();
state = new Video('#example'); state = new Video('#example');
state.resizer = (function () { state.resizer = (function () {
......
...@@ -181,31 +181,6 @@ ...@@ -181,31 +181,6 @@
}); });
}); });
describe('youtubeId', function () {
beforeEach(function () {
loadFixtures('video.html');
$.cookie.andReturn('1.0');
state = new Video('#example');
});
describe('with speed', function () {
it('return the video id for given speed', function () {
expect(state.youtubeId('0.50'))
.toEqual('7tqY6eQzVhE');
expect(state.youtubeId('1.0'))
.toEqual('cogebirgzzM');
expect(state.youtubeId('1.50'))
.toEqual('abcdefghijkl');
});
});
describe('without speed', function () {
it('return the video id for current speed', function () {
expect(state.youtubeId()).toEqual('abcdefghijkl');
});
});
});
describe('YouTube video in FireFox will cue first', function () { describe('YouTube video in FireFox will cue first', function () {
var oldUserAgent; var oldUserAgent;
...@@ -368,84 +343,6 @@ ...@@ -368,84 +343,6 @@
}); });
}); });
describe('setSpeed', function () {
describe('YT', function () {
beforeEach(function () {
loadFixtures('video.html');
state = new Video('#example');
});
it('check mapping', function () {
var map = {
'0.75': '0.50',
'1.25': '1.50'
};
$.each(map, function(key, expected) {
state.setSpeed(key, true);
expect(state.speed).toBe(expected);
});
});
});
describe('HTML5', function () {
beforeEach(function () {
loadFixtures('video_html5.html');
state = new Video('#example');
});
describe('when new speed is available', function () {
beforeEach(function () {
state.setSpeed('0.75', true);
});
it('set new speed', function () {
expect(state.speed).toEqual('0.75');
});
it('save setting for new speed', function () {
expect(state.storage.getItem('general_speed')).toBe('0.75');
expect(state.storage.getItem('speed', true)).toBe('0.75');
});
});
describe('when new speed is not available', function () {
beforeEach(function () {
state.setSpeed('1.75');
});
it('set speed to 1.0x', function () {
expect(state.speed).toEqual('1.0');
});
});
it('check mapping', function () {
var map = {
'0.25': '0.75',
'0.50': '0.75',
'2.0': '1.50'
};
$.each(map, function(key, expected) {
state.setSpeed(key, true);
expect(state.speed).toBe(expected);
});
});
});
});
describe('getDuration', function () {
beforeEach(function () {
loadFixtures('video.html');
state = new Video('#example');
});
it('return duration for current video', function () {
expect(state.getDuration()).toEqual(400);
});
});
describe('log', function () { describe('log', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('video_html5.html'); loadFixtures('video_html5.html');
......
...@@ -6,8 +6,14 @@ require( ...@@ -6,8 +6,14 @@ require(
['video/01_initialize.js'], ['video/01_initialize.js'],
function (Initialize) { function (Initialize) {
describe('Initialize', function () { describe('Initialize', function () {
var state = {};
afterEach(function () {
state = {};
});
describe('saveState function', function () { describe('saveState function', function () {
var state, videoPlayerCurrentTime, newCurrentTime, speed; var videoPlayerCurrentTime, newCurrentTime, speed;
// We make sure that `currentTime` is a float. We need to test // We make sure that `currentTime` is a float. We need to test
// that Math.round() is called. // that Math.round() is called.
...@@ -40,10 +46,6 @@ function (Initialize) { ...@@ -40,10 +46,6 @@ function (Initialize) {
spyOn(Time, 'formatFull').andCallThrough(); spyOn(Time, 'formatFull').andCallThrough();
}); });
afterEach(function () {
state = undefined;
});
it('data is not an object, async is true', function () { it('data is not an object, async is true', function () {
itSpec({ itSpec({
asyncVal: true, asyncVal: true,
...@@ -161,8 +163,211 @@ function (Initialize) { ...@@ -161,8 +163,211 @@ function (Initialize) {
}); });
} }
}); });
});
describe('getCurrentLanguage', function () {
var msg;
beforeEach(function () {
state.config = {};
state.config.transcriptLanguages = {
'de': 'German',
'en': 'English',
'uk': 'Ukrainian',
};
});
it ('returns current language', function () {
var expected;
state.lang = 'de';
expected = Initialize.prototype.getCurrentLanguage.call(state);
expect(expected).toBe('de');
});
msg = 'returns `en`, if language isn\'t available for the video';
it (msg, function () {
var expected;
state.lang = 'zh';
expected = Initialize.prototype.getCurrentLanguage.call(state);
expect(expected).toBe('en');
});
msg = 'returns any available language, if current and `en` ' +
'languages aren\'t available for the video';
it (msg, function () {
var expected;
state.lang = 'zh';
state.config.transcriptLanguages = {
'de': 'German',
'uk': 'Ukrainian',
};
expected = Initialize.prototype.getCurrentLanguage.call(state);
expect(expected).toBe('uk');
});
it ('returns `null`, if transcript unavailable', function () {
var expected;
state.lang = 'zh';
state.config.transcriptLanguages = {};
expected = Initialize.prototype.getCurrentLanguage.call(state);
expect(expected).toBeNull();
});
});
describe('getDuration', function () {
beforeEach(function () {
state = {
speed: '1.50',
metadata: {
'testId': {
duration: 400
},
'videoId': {
duration: 100
}
},
videos: {
'1.0': 'testId',
'1.50': 'videoId'
},
youtubeId: Initialize.prototype.youtubeId
};
});
it('returns duration for current video', function () {
var expected = Initialize.prototype.getDuration.call(state);
expect(expected).toEqual(100);
});
var msg = 'returns duration for the 1.0 speed as a fallback';
it(msg, function () {
var expected;
state.speed = '2.0';
expected = Initialize.prototype.getDuration.call(state);
expect(expected).toEqual(400);
});
});
describe('youtubeId', function () {
beforeEach(function () {
state = {
speed: '1.50',
videos: {
'0.50': '7tqY6eQzVhE',
'1.0': 'cogebirgzzM',
'1.50': 'abcdefghijkl'
}
};
});
describe('with speed', function () {
it('return the video id for given speed', function () {
$.each(state.videos, function(speed, videoId) {
var expected = Initialize.prototype.youtubeId.call(
state, speed
);
expect(videoId).toBe(expected);
});
});
});
describe('without speed', function () {
it('return the video id for current speed', function () {
var expected = Initialize.prototype.youtubeId.call(state);
expect(expected).toEqual('abcdefghijkl');
});
});
describe('speed is absent in the list of video speeds', function () {
it('return the video id for 1.0x speed', function () {
var expected = Initialize.prototype.youtubeId.call(state, '0.0');
expect(expected).toEqual('cogebirgzzM');
});
});
});
describe('setSpeed', function () {
describe('YT', function () {
beforeEach(function () {
state = {
speeds: ['0.25', '0.50', '1.0', '1.50', '2.0'],
storage: jasmine.createSpyObj('storage', ['setItem'])
};
});
it('check mapping', function () {
var map = {
'0.75': '0.50',
'1.25': '1.50'
};
$.each(map, function(key, expected) {
Initialize.prototype.setSpeed.call(state, key);
expect(state.speed).toBe(expected);
});
});
});
describe('HTML5', function () {
beforeEach(function () {
state = {
speeds: ['0.75', '1.0', '1.25', '1.50'],
storage: jasmine.createSpyObj('storage', ['setItem'])
};
});
describe('when new speed is available', function () {
beforeEach(function () {
Initialize.prototype.setSpeed.call(state, '0.75', true);
});
it('set new speed', function () {
expect(state.speed).toEqual('0.75');
});
it('save setting for new speed', function () {
expect(state.storage.setItem.calls[0].args)
.toEqual(['speed', '0.75', true]);
expect(state.storage.setItem.calls[1].args)
.toEqual(['general_speed', '0.75']);
});
});
describe('when new speed is not available', function () {
beforeEach(function () {
Initialize.prototype.setSpeed.call(state, '1.75');
});
it('set speed to 1.0x', function () {
expect(state.speed).toEqual('1.0');
});
});
it('check mapping', function () {
var map = {
'0.25': '0.75',
'0.50': '0.75',
'2.0': '1.50'
};
$.each(map, function(key, expected) {
Initialize.prototype.setSpeed.call(state, key, true);
expect(state.speed).toBe(expected);
});
});
});
});
});
}); });
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); }(RequireJS.requirejs, RequireJS.require, RequireJS.define));
...@@ -57,9 +57,11 @@ function (VideoPlayer, VideoStorage) { ...@@ -57,9 +57,11 @@ function (VideoPlayer, VideoStorage) {
}); });
}); });
}, },
methodsDict = { methodsDict = {
bindTo: bindTo, bindTo: bindTo,
fetchMetadata: fetchMetadata, fetchMetadata: fetchMetadata,
getCurrentLanguage: getCurrentLanguage,
getDuration: getDuration, getDuration: getDuration,
getVideoMetadata: getVideoMetadata, getVideoMetadata: getVideoMetadata,
initialize: initialize, initialize: initialize,
...@@ -305,6 +307,11 @@ function (VideoPlayer, VideoStorage) { ...@@ -305,6 +307,11 @@ function (VideoPlayer, VideoStorage) {
value || value ||
'1.0'; '1.0';
}, },
'transcriptLanguage': function (value) {
return storage.getItem('language') ||
value ||
'en';
},
'ytTestTimeout': function (value) { 'ytTestTimeout': function (value) {
value = parseInt(value, 10); value = parseInt(value, 10);
...@@ -432,6 +439,7 @@ function (VideoPlayer, VideoStorage) { ...@@ -432,6 +439,7 @@ function (VideoPlayer, VideoStorage) {
this.config.endTime = null; this.config.endTime = null;
} }
this.lang = this.config.transcriptLanguage;
this.speed = Number( this.speed = Number(
this.config.speed || this.config.generalSpeed this.config.speed || this.config.generalSpeed
).toFixed(2).replace(/\.00$/, '.0'); ).toFixed(2).replace(/\.00$/, '.0');
...@@ -631,17 +639,16 @@ function (VideoPlayer, VideoStorage) { ...@@ -631,17 +639,16 @@ function (VideoPlayer, VideoStorage) {
function setSpeed(newSpeed, updateStorage) { function setSpeed(newSpeed, updateStorage) {
// Possible speeds for each player type. // Possible speeds for each player type.
// flash = [0.75, 1, 1.25, 1.5] // HTML5 = [0.75, 1, 1.25, 1.5]
// html5 = [0.75, 1, 1.25, 1.5] // Youtube Flash = [0.75, 1, 1.25, 1.5]
// youtube html5 = [0.25, 0.5, 1, 1.5, 2] // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2]
var map = { var map = {
'0.25': '0.75', '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
'0.50': '0.75', '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
'0.75': '0.50', '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
'1.25': '1.50', '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
'2.0': '1.50' '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
}, };
useSession = true;
if (_.contains(this.speeds, newSpeed)) { if (_.contains(this.speeds, newSpeed)) {
this.speed = newSpeed; this.speed = newSpeed;
...@@ -712,6 +719,24 @@ function (VideoPlayer, VideoStorage) { ...@@ -712,6 +719,24 @@ function (VideoPlayer, VideoStorage) {
} }
} }
function getCurrentLanguage() {
var keys = _.keys(this.config.transcriptLanguages);
if (keys.length) {
if (!_.contains(keys, this.lang)) {
if (_.contains(keys, 'en')) {
this.lang = 'en';
} else {
this.lang = keys.pop();
}
}
} else {
return null;
}
return this.lang;
}
/* /*
* The trigger() function will assume that the @objChain is a complete * The trigger() function will assume that the @objChain is a complete
* chain with a method (function) at the end. It will call this function. * chain with a method (function) at the end. It will call this function.
......
...@@ -5,30 +5,16 @@ define( ...@@ -5,30 +5,16 @@ define(
'video/03_video_player.js', 'video/03_video_player.js',
['video/02_html5_video.js', 'video/00_resizer.js'], ['video/02_html5_video.js', 'video/00_resizer.js'],
function (HTML5Video, Resizer) { function (HTML5Video, Resizer) {
var dfd = $.Deferred(); var dfd = $.Deferred(),
VideoPlayer = function (state) {
// VideoPlayer() function - what this module "exports". state.videoPlayer = {};
return function (state) { _makeFunctionsPublic(state);
_initialize(state);
state.videoPlayer = {}; // No callbacks to DOM events (click, mousemove, etc.).
_makeFunctionsPublic(state); return dfd.promise();
_initialize(state); },
// No callbacks to DOM events (click, mousemove, etc.). methodsDict = {
return dfd.promise();
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called,
// these functions will get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
duration: duration, duration: duration,
handlePlaybackQualityChange: handlePlaybackQualityChange, handlePlaybackQualityChange: handlePlaybackQualityChange,
isPlaying: isPlaying, isPlaying: isPlaying,
...@@ -46,10 +32,25 @@ function (HTML5Video, Resizer) { ...@@ -46,10 +32,25 @@ function (HTML5Video, Resizer) {
onVolumeChange: onVolumeChange, onVolumeChange: onVolumeChange,
pause: pause, pause: pause,
play: play, play: play,
setPlaybackRate: setPlaybackRate,
update: update, update: update,
updatePlayTime: updatePlayTime updatePlayTime: updatePlayTime
}; };
VideoPlayer.prototype = methodsDict;
// VideoPlayer() function - what this module "exports".
return VideoPlayer;
// ***************************************************************
// Private functions start here.
// ***************************************************************
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called,
// these functions will get the 'state' object as a context.
function _makeFunctionsPublic(state) {
state.bindTo(methodsDict, state.videoPlayer, state); state.bindTo(methodsDict, state.videoPlayer, state);
} }
...@@ -70,7 +71,7 @@ function (HTML5Video, Resizer) { ...@@ -70,7 +71,7 @@ function (HTML5Video, Resizer) {
$(window).on('unload', state.saveState); $(window).on('unload', state.saveState);
if (state.currentPlayerMode !== 'flash') { if (state.currentPlayerMode !== 'flash') {
state.videoPlayer.onSpeedChange(state.speed); state.videoPlayer.setPlaybackRate(state.speed);
} }
state.videoPlayer.player.setVolume(state.currentVolume); state.videoPlayer.player.setVolume(state.currentVolume);
}); });
...@@ -325,33 +326,10 @@ function (HTML5Video, Resizer) { ...@@ -325,33 +326,10 @@ function (HTML5Video, Resizer) {
} }
} }
function onSpeedChange(newSpeed) { function setPlaybackRate(newSpeed) {
var time = this.videoPlayer.currentTime, var time = this.videoPlayer.currentTime,
methodName, youtubeId; methodName, youtubeId;
if (this.currentPlayerMode === 'flash') {
this.videoPlayer.currentTime = Time.convert(
time,
parseFloat(this.speed),
newSpeed
);
}
newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
if (this.speed != newSpeed) {
this.videoPlayer.log(
'speed_change_video',
{
current_time: time,
old_speed: this.speed,
new_speed: newSpeed
}
);
}
this.setSpeed(newSpeed, true);
if ( if (
this.currentPlayerMode === 'html5' && this.currentPlayerMode === 'html5' &&
!( !(
...@@ -377,7 +355,33 @@ function (HTML5Video, Resizer) { ...@@ -377,7 +355,33 @@ function (HTML5Video, Resizer) {
this.videoPlayer.player[methodName](youtubeId, time); this.videoPlayer.player[methodName](youtubeId, time);
this.videoPlayer.updatePlayTime(time); this.videoPlayer.updatePlayTime(time);
} }
}
function onSpeedChange(newSpeed) {
var time = this.videoPlayer.currentTime,
isFlash = this.currentPlayerMode === 'flash';
if (isFlash) {
this.videoPlayer.currentTime = Time.convert(
time,
parseFloat(this.speed),
newSpeed
);
}
newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
this.videoPlayer.log(
'speed_change_video',
{
current_time: time,
old_speed: this.speed,
new_speed: newSpeed
}
);
this.setSpeed(newSpeed, true);
this.videoPlayer.setPlaybackRate(newSpeed);
this.el.trigger('speedchange', arguments); this.el.trigger('speedchange', arguments);
this.saveState(true, { speed: newSpeed }); this.saveState(true, { speed: newSpeed });
......
...@@ -113,7 +113,7 @@ class ModelsTest(unittest.TestCase): ...@@ -113,7 +113,7 @@ class ModelsTest(unittest.TestCase):
def test_load_class(self): def test_load_class(self):
vc = XModuleDescriptor.load_class('video') vc = XModuleDescriptor.load_class('video')
vc_str = "<class 'xmodule.video_module.VideoDescriptor'>" vc_str = "<class 'xmodule.video_module.video_module.VideoDescriptor'>"
self.assertEqual(str(vc), vc_str) self.assertEqual(str(vc), vc_str)
......
...@@ -20,7 +20,7 @@ from mock import Mock ...@@ -20,7 +20,7 @@ from mock import Mock
from . import LogicTest from . import LogicTest
from lxml import etree from lxml import etree
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.video_module import VideoDescriptor, _create_youtube_string from xmodule.video_module import VideoDescriptor, create_youtube_string
from .test_import import DummySystem from .test_import import DummySystem
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
...@@ -150,7 +150,7 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -150,7 +150,7 @@ class VideoDescriptorTest(unittest.TestCase):
descriptor.youtube_id_1_25 = '1EeWXzPdhSA' descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
descriptor.youtube_id_1_5 = 'rABDYkeK0x8' descriptor.youtube_id_1_5 = 'rABDYkeK0x8'
expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"
self.assertEqual(_create_youtube_string(descriptor), expected) self.assertEqual(create_youtube_string(descriptor), expected)
def test_create_youtube_string_missing(self): def test_create_youtube_string_missing(self):
""" """
...@@ -165,7 +165,7 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -165,7 +165,7 @@ class VideoDescriptorTest(unittest.TestCase):
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8' descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
descriptor.youtube_id_1_25 = '1EeWXzPdhSA' descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA" expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
self.assertEqual(_create_youtube_string(descriptor), expected) self.assertEqual(create_youtube_string(descriptor), expected)
class VideoDescriptorImportTestCase(unittest.TestCase): class VideoDescriptorImportTestCase(unittest.TestCase):
...@@ -193,6 +193,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -193,6 +193,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/> <source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
<transcript language="ua" src="ukrainian_translation.srt" />
<transcript language="ge" src="german_translation.srt" />
</video> </video>
''' '''
location = Location(["i4x", "edX", "video", "default", location = Location(["i4x", "edX", "video", "default",
...@@ -215,7 +217,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -215,7 +217,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'track': 'http://www.example.com/track', 'track': 'http://www.example.com/track',
'download_track': True, 'download_track': True,
'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'], 'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'],
'data': '' 'data': '',
'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
}) })
def test_from_xml(self): def test_from_xml(self):
...@@ -230,6 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -230,6 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
end_time="00:01:00"> end_time="00:01:00">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
<transcript language="ua" src="ukrainian_translation.srt" />
<transcript language="ge" src="german_translation.srt" />
</video> </video>
''' '''
output = VideoDescriptor.from_xml(xml_data, module_system, Mock()) output = VideoDescriptor.from_xml(xml_data, module_system, Mock())
...@@ -245,7 +250,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -245,7 +250,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'download_track': False, 'download_track': False,
'download_video': False, 'download_video': False,
'html5_sources': ['http://www.example.com/source.mp4'], 'html5_sources': ['http://www.example.com/source.mp4'],
'data': '' 'data': '',
'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'},
}) })
def test_from_xml_missing_attributes(self): def test_from_xml_missing_attributes(self):
...@@ -304,7 +310,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -304,7 +310,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'download_track': True, 'download_track': True,
'download_video': True, 'download_video': True,
'html5_sources': ['http://www.example.com/source.mp4'], 'html5_sources': ['http://www.example.com/source.mp4'],
'data': '' 'data': '',
'transcripts': {},
}) })
def test_from_xml_no_attributes(self): def test_from_xml_no_attributes(self):
...@@ -326,7 +333,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -326,7 +333,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'download_track': False, 'download_track': False,
'download_video': False, 'download_video': False,
'html5_sources': [], 'html5_sources': [],
'data': '' 'data': '',
'transcripts': {},
}) })
def test_from_xml_double_quotes(self): def test_from_xml_double_quotes(self):
...@@ -508,6 +516,7 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -508,6 +516,7 @@ class VideoExportTestCase(unittest.TestCase):
desc.download_track = True desc.download_track = True
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'] desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
desc.download_video = True desc.download_video = True
desc.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
expected = etree.fromstring('''\ expected = etree.fromstring('''\
...@@ -515,9 +524,10 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -515,9 +524,10 @@ class VideoExportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/> <source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
<transcript language="ge" src="german_translation.srt" />
<transcript language="ua" src="ukrainian_translation.srt" />
</video> </video>
''') ''')
self.assertXmlEqual(expected, xml) self.assertXmlEqual(expected, xml)
def test_export_to_xml_empty_end_time(self): def test_export_to_xml_empty_end_time(self):
......
"""
Container for video module and it's utils.
"""
# Disable wildcard-import warnings.
# pylint: disable=W0401
from .transcripts_utils import *
from .video_utils import *
from .video_module import *
"""
Module containts utils specific for video_module but not for transcripts.
"""
def create_youtube_string(module):
"""
Create a string of Youtube IDs from `module`'s metadata
attributes. Only writes a speed if an ID is present in the
module. Necessary for backwards compatibility with XML-based
courses.
"""
youtube_ids = [
module.youtube_id_0_75,
module.youtube_id_1_0,
module.youtube_id_1_25,
module.youtube_id_1_5
]
youtube_speeds = ['0.75', '1.00', '1.25', '1.50']
return ','.join([
':'.join(pair)
for pair
in zip(youtube_speeds, youtube_ids)
if pair[1]
])
{
"start": [
270,
2720,
5430
],
"end": [
2720,
5430,
7160
],
"text": [
"好 各位同学",
"我们今天要讲的题目是",
"从算筹到ENIAC"
]
}
@shard_2 @shard_2
Feature: LMS.Video component Feature: LMS Video component
As a student, I want to view course videos in LMS. As a student, I want to view course videos in LMS
# 0 # 0
Scenario: Video component stores position correctly when page is reloaded Scenario: Video component stores position correctly when page is reloaded
...@@ -58,7 +58,7 @@ Feature: LMS.Video component ...@@ -58,7 +58,7 @@ Feature: LMS.Video component
And error message has correct text And error message has correct text
# 8 # 8
Scenario: Video component stores speed correctly when each video is in separate sequence. Scenario: Video component stores speed correctly when each video is in separate sequence
Given I am registered for the course "test_course" Given I am registered for the course "test_course"
And it has a video "A" in "Youtube" mode in position "1" of sequential And it has a video "A" in "Youtube" mode in position "1" of sequential
And a video "B" in "Youtube" mode in position "2" of sequential And a video "B" in "Youtube" mode in position "2" of sequential
...@@ -78,3 +78,15 @@ Feature: LMS.Video component ...@@ -78,3 +78,15 @@ Feature: LMS.Video component
Then video "B" should start playing at speed "0.50" Then video "B" should start playing at speed "0.50"
When I open video "C" When I open video "C"
Then video "C" should start playing at speed "1.0" Then video "C" should start playing at speed "1.0"
# 9
Scenario: Language menu in Video component works correctly
Given the course has a Video component in Youtube mode:
| transcripts | sub |
| {"zh": "OEoXaMPEzfM"} | OEoXaMPEzfM |
And I make sure captions are closed
And I see video menu "language" with correct items
And I select language with code "zh"
Then I see "好 各位同学" text in the captions
And I select language with code "en"
And I see "Hi, welcome to Edx." text in the captions
# -*- coding: utf-8 -*-
#pylint: disable=C0111 #pylint: disable=C0111
from lettuce import world, step from lettuce import world, step
import json
from common import i_am_registered_for_the_course, section_location, visit_scenario_item from common import i_am_registered_for_the_course, section_location, visit_scenario_item
from django.utils.translation import ugettext as _ 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
import os
from functools import partial
from xmodule.contentstore.django import contentstore
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
LANGUAGES = settings.ALL_LANGUAGES
############### ACTIONS #################### ############### ACTIONS ####################
...@@ -14,16 +25,23 @@ HTML5_SOURCES = [ ...@@ -14,16 +25,23 @@ HTML5_SOURCES = [
HTML5_SOURCES_INCORRECT = [ HTML5_SOURCES_INCORRECT = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99', 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99',
] ]
VIDEO_BUTTONS = { VIDEO_BUTTONS = {
'CC': '.hide-subtitles', 'CC': '.hide-subtitles',
'volume': '.volume', 'volume': '.volume',
'play': '.video_control.play', 'play': '.video_control.play',
'pause': '.video_control.pause', 'pause': '.video_control.pause',
} }
VIDEO_MENUS = {
'language': '.lang .menu',
'speed': '.speed .menu',
}
# We should wait 300 ms for event handler invocation + 200ms for safety. VIDEO_BUTTONS = {
DELAY = 0.5 'CC': '.hide-subtitles',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
}
coursenum = 'test_course' coursenum = 'test_course'
sequence = {} sequence = {}
...@@ -33,20 +51,20 @@ def does_not_autoplay(_step, video_type): ...@@ -33,20 +51,20 @@ def does_not_autoplay(_step, video_type):
assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False') assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False')
@step('the course has a Video component in (.*) mode$') @step('the course has a Video component in (.*) mode(?:\:)?$')
def view_video(_step, player_mode): def view_video(_step, player_mode):
i_am_registered_for_the_course(_step, coursenum) i_am_registered_for_the_course(_step, coursenum)
# Make sure we have a video # Make sure we have a video
add_video_to_course(coursenum, player_mode.lower()) add_video_to_course(coursenum, player_mode.lower(), _step.hashes)
visit_scenario_item('SECTION') visit_scenario_item('SECTION')
@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential$') @step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential(?:\:)?$')
def add_video(_step, player_id, player_mode, position): def add_video(_step, player_id, player_mode, position):
sequence[player_id] = position sequence[player_id] = position
add_video_to_course(coursenum, player_mode.lower(), display_name=player_id) add_video_to_course(coursenum, player_mode.lower(), _step.hashes, display_name=player_id)
@step('I open the section with videos$') @step('I open the section with videos$')
...@@ -70,49 +88,55 @@ def check_video_speed(_step, player_id, speed): ...@@ -70,49 +88,55 @@ def check_video_speed(_step, player_id, speed):
speed_css = '.speeds p.active' speed_css = '.speeds p.active'
assert world.css_has_text(speed_css, '{0}x'.format(speed)) assert world.css_has_text(speed_css, '{0}x'.format(speed))
def add_video_to_course(course, player_mode, display_name='Video'):
def add_video_to_course(course, player_mode, hashes, display_name='Video'):
category = 'video' category = 'video'
kwargs = { kwargs = {
'parent_location': section_location(course), 'parent_location': section_location(course),
'category': category, 'category': category,
'display_name': display_name 'display_name': display_name,
'metadata': {},
} }
if player_mode == 'html5': if player_mode == 'html5':
kwargs.update({ kwargs['metadata'].update({
'metadata': { 'youtube_id_1_0': '',
'youtube_id_1_0': '', 'youtube_id_0_75': '',
'youtube_id_0_75': '', 'youtube_id_1_25': '',
'youtube_id_1_25': '', 'youtube_id_1_5': '',
'youtube_id_1_5': '', 'html5_sources': HTML5_SOURCES
'html5_sources': HTML5_SOURCES
}
}) })
if player_mode == 'youtube_html5': if player_mode == 'youtube_html5':
kwargs.update({ kwargs['metadata'].update({
'metadata': { 'html5_sources': HTML5_SOURCES
'html5_sources': HTML5_SOURCES
}
}) })
if player_mode == 'youtube_html5_unsupported_video': if player_mode == 'youtube_html5_unsupported_video':
kwargs.update({ kwargs['metadata'].update({
'metadata': { 'html5_sources': HTML5_SOURCES_INCORRECT
'html5_sources': HTML5_SOURCES_INCORRECT
}
}) })
if player_mode == 'html5_unsupported_video': if player_mode == 'html5_unsupported_video':
kwargs.update({ kwargs['metadata'].update({
'metadata': { 'youtube_id_1_0': '',
'youtube_id_1_0': '', 'youtube_id_0_75': '',
'youtube_id_0_75': '', 'youtube_id_1_25': '',
'youtube_id_1_25': '', 'youtube_id_1_5': '',
'youtube_id_1_5': '', 'html5_sources': HTML5_SOURCES_INCORRECT
'html5_sources': HTML5_SOURCES_INCORRECT
}
}) })
world.ItemFactory.create(**kwargs) if hashes:
kwargs['metadata'].update(hashes[0])
if 'transcripts' in kwargs['metadata']:
kwargs['metadata']['transcripts'] = json.loads(kwargs['metadata']['transcripts'])
if 'sub' in kwargs['metadata']:
_upload_file(kwargs['metadata']['sub'], 'en', world.scenario_dict['COURSE'].location)
for lang, videoId in kwargs['metadata']['transcripts'].items():
_upload_file(videoId, lang, world.scenario_dict['COURSE'].location)
world.scenario_dict['VIDEO'] = world.ItemFactory.create(**kwargs)
@step('youtube server is up and response time is (.*) seconds$') @step('youtube server is up and response time is (.*) seconds$')
...@@ -152,6 +176,92 @@ def error_message_has_correct_text(_step): ...@@ -152,6 +176,92 @@ def error_message_has_correct_text(_step):
assert world.css_has_text(selector, text) assert world.css_has_text(selector, text)
@step('I make sure captions are (.+)$')
def set_captions_visibility_state(_step, captions_state):
SELECTOR = '.closed .subtitles'
if world.is_css_not_present(SELECTOR):
if captions_state == 'closed':
world.css_find('.hide-subtitles').click()
else:
if captions_state != 'closed':
world.css_find('.hide-subtitles').click()
@step('I see video menu "([^"]*)" with correct items$')
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:
transcripts.update({
'en': Video.sub
})
languages = {i[0]: i[1] for i in LANGUAGES}
transcripts = {k: languages[k] for k in transcripts}
for code, label in transcripts.items():
assert any([i.text == label for i in menu_items])
assert any([i['data-lang-code'] == code for i in menu_items])
@step('I see "([^"]*)" text in the captions$')
def check_text_in_the_captions(_step, text):
assert world.browser.is_text_present(text.strip())
@step('I select language with code "([^"]*)"$')
def select_language(_step, code):
_open_menu("language")
selector = VIDEO_MENUS["language"] + ' li[data-lang-code={code}]'.format(
code=code
)
item = world.css_find(selector)
item.click()
assert world.css_has_class(selector, 'active')
assert len(world.css_find(VIDEO_MENUS["language"] + ' li.active')) == 1
assert world.css_visible('.subtitles')
world.wait_for_ajax_complete()
@step('I click on video button "([^"]*)"$')
def click_button(_step, button):
world.css_find(VIDEO_BUTTONS[button]).click()
def _upload_file(videoId, lang, location):
if lang == 'en':
filename = 'subs_{0}.srt.sjson'.format(videoId)
else:
filename = '{0}_subs_{1}.srt.sjson'.format(lang, videoId)
path = os.path.join(TEST_ROOT, 'uploads/', filename)
f = open(os.path.abspath(path))
mime_type = "application/json"
content_location = StaticContent.compute_location(
location.org, location.course, filename
)
sc_partial = partial(StaticContent, content_location, filename, mime_type)
content = sc_partial(f.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)
def _navigate_to_an_item_in_a_sequence(number): def _navigate_to_an_item_in_a_sequence(number):
sequence_css = 'a[data-element="{0}"]'.format(number) sequence_css = 'a[data-element="{0}"]'.format(number)
world.css_click(sequence_css) world.css_click(sequence_css)
...@@ -165,7 +275,6 @@ def _change_video_speed(speed): ...@@ -165,7 +275,6 @@ def _change_video_speed(speed):
@step('I click video button "([^"]*)"$') @step('I click video button "([^"]*)"$')
def click_button_video(_step, button_type): def click_button_video(_step, button_type):
world.wait(DELAY)
world.wait_for_ajax_complete() world.wait_for_ajax_complete()
button = button_type.strip() button = button_type.strip()
world.css_click(VIDEO_BUTTONS[button]) world.css_click(VIDEO_BUTTONS[button])
...@@ -184,3 +293,9 @@ def seek_video_to_n_seconds(_step, seconds): ...@@ -184,3 +293,9 @@ def seek_video_to_n_seconds(_step, seconds):
time = float(seconds.strip()) time = float(seconds.strip())
jsCode = "$('.video').data('video-player-state').videoPlayer.onSlideSeek({{time: {0:f}}})".format(time) jsCode = "$('.video').data('video-player-state').videoPlayer.onSlideSeek({{time: {0:f}}})".format(time)
world.browser.execute_script(jsCode) world.browser.execute_script(jsCode)
def _open_menu(menu):
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
selector=VIDEO_MENUS[menu]
))
...@@ -90,9 +90,11 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -90,9 +90,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_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_descriptor.xmodule_runtime = self.new_module_runtime()
self.item_module = self.item_descriptor
self.item_url = Location(self.item_module.location).url() #self.item_module = self.item_descriptor.xmodule_runtime.xmodule_instance
#self.item_module is None at this time
self.item_url = Location(self.item_descriptor.location).url()
def setup_course(self): def setup_course(self):
self.course = CourseFactory.create(data=self.COURSE_DATA) self.course = CourseFactory.create(data=self.COURSE_DATA)
...@@ -130,7 +132,7 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -130,7 +132,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
self.assertTrue(all(self.login_statuses)) self.assertTrue(all(self.login_statuses))
def setUp(self): def setUp(self):
self.setup_course(); self.setup_course()
self.initialize_module(metadata=self.METADATA, data=self.DATA) self.initialize_module(metadata=self.METADATA, data=self.DATA)
def get_url(self, dispatch): def get_url(self, dispatch):
......
...@@ -27,8 +27,8 @@ class TestLTI(BaseTestXmodule): ...@@ -27,8 +27,8 @@ class TestLTI(BaseTestXmodule):
mocked_signature_after_sign = u'my_signature%3D' mocked_signature_after_sign = u'my_signature%3D'
mocked_decoded_signature = u'my_signature=' mocked_decoded_signature = u'my_signature='
lti_id = self.item_module.lti_id lti_id = self.item_descriptor.lti_id
module_id = unicode(urllib.quote(self.item_module.id)) module_id = unicode(urllib.quote(self.item_descriptor.id))
user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id) user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id)
sourcedId = "{id}:{resource_link}:{user_id}".format( sourcedId = "{id}:{resource_link}:{user_id}".format(
...@@ -38,9 +38,9 @@ class TestLTI(BaseTestXmodule): ...@@ -38,9 +38,9 @@ class TestLTI(BaseTestXmodule):
) )
lis_outcome_service_url = 'https://{host}{path}'.format( lis_outcome_service_url = 'https://{host}{path}'.format(
host=self.item_descriptor.xmodule_runtime.hostname, host=self.item_descriptor.xmodule_runtime.hostname,
path=self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'grade_handler', thirdparty=True).rstrip('/?') path=self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'grade_handler', thirdparty=True).rstrip('/?')
) )
self.correct_headers = { self.correct_headers = {
u'user_id': user_id, u'user_id': user_id,
u'oauth_callback': u'about:blank', u'oauth_callback': u'about:blank',
...@@ -63,13 +63,13 @@ class TestLTI(BaseTestXmodule): ...@@ -63,13 +63,13 @@ class TestLTI(BaseTestXmodule):
saved_sign = oauthlib.oauth1.Client.sign saved_sign = oauthlib.oauth1.Client.sign
self.expected_context = { self.expected_context = {
'display_name': self.item_module.display_name, 'display_name': self.item_descriptor.display_name,
'input_fields': self.correct_headers, 'input_fields': self.correct_headers,
'element_class': self.item_module.category, 'element_class': self.item_descriptor.category,
'element_id': self.item_module.location.html_id(), 'element_id': self.item_descriptor.location.html_id(),
'launch_url': 'http://www.example.com', # default value 'launch_url': 'http://www.example.com', # default value
'open_in_a_new_page': True, 'open_in_a_new_page': True,
'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'preview_handler').rstrip('/?'), 'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'preview_handler').rstrip('/?'),
} }
def mocked_sign(self, *args, **kwargs): def mocked_sign(self, *args, **kwargs):
...@@ -92,11 +92,11 @@ class TestLTI(BaseTestXmodule): ...@@ -92,11 +92,11 @@ class TestLTI(BaseTestXmodule):
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
def test_lti_constructor(self): def test_lti_constructor(self):
generated_content = self.item_module.render('student_view').content generated_content = self.item_descriptor.render('student_view').content
expected_content = self.runtime.render_template('lti.html', self.expected_context) expected_content = self.runtime.render_template('lti.html', self.expected_context)
self.assertEqual(generated_content, expected_content) self.assertEqual(generated_content, expected_content)
def test_lti_preview_handler(self): def test_lti_preview_handler(self):
generated_content = self.item_module.preview_handler(None, None).body generated_content = self.item_descriptor.preview_handler(None, None).body
expected_content = self.runtime.render_template('lti_form.html', self.expected_context) expected_content = self.runtime.render_template('lti_form.html', self.expected_context)
self.assertEqual(generated_content, expected_content) self.assertEqual(generated_content, expected_content)
...@@ -32,6 +32,7 @@ SOURCE_XML = """ ...@@ -32,6 +32,7 @@ SOURCE_XML = """
> >
<source src="example.mp4"/> <source src="example.mp4"/>
<source src="example.webm"/> <source src="example.webm"/>
<transcript language="uk" src="ukrainian_translation.srt" />
</video> </video>
""" """
......
...@@ -242,12 +242,12 @@ class TestWordCloud(BaseTestXmodule): ...@@ -242,12 +242,12 @@ class TestWordCloud(BaseTestXmodule):
def test_word_cloud_constructor(self): def test_word_cloud_constructor(self):
"""Make sure that all parameters extracted correclty from xml""" """Make sure that all parameters extracted correclty from xml"""
fragment = self.runtime.render(self.item_module, 'student_view') fragment = self.runtime.render(self.item_descriptor, 'student_view')
expected_context = { expected_context = {
'ajax_url': self.item_module.xmodule_runtime.ajax_url, 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
'element_class': self.item_module.location.category, 'element_class': self.item_descriptor.location.category,
'element_id': self.item_module.location.html_id(), 'element_id': self.item_descriptor.location.html_id(),
'num_inputs': 5, # default value 'num_inputs': 5, # default value
'submitted': False # default value 'submitted': False # default value
} }
......
...@@ -1287,3 +1287,193 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60 ...@@ -1287,3 +1287,193 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
##### LMS DEADLINE DISPLAY TIME_ZONE ####### ##### LMS DEADLINE DISPLAY TIME_ZONE #######
TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC' TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
# Source:
# http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1
ALL_LANGUAGES = (
[u"aa", u"Afar"],
[u"ab", u"Abkhazian"],
[u"af", u"Afrikaans"],
[u"ak", u"Akan"],
[u"sq", u"Albanian"],
[u"am", u"Amharic"],
[u"ar", u"Arabic"],
[u"an", u"Aragonese"],
[u"hy", u"Armenian"],
[u"as", u"Assamese"],
[u"av", u"Avaric"],
[u"ae", u"Avestan"],
[u"ay", u"Aymara"],
[u"az", u"Azerbaijani"],
[u"ba", u"Bashkir"],
[u"bm", u"Bambara"],
[u"eu", u"Basque"],
[u"be", u"Belarusian"],
[u"bn", u"Bengali"],
[u"bh", u"Bihari languages"],
[u"bi", u"Bislama"],
[u"bs", u"Bosnian"],
[u"br", u"Breton"],
[u"bg", u"Bulgarian"],
[u"my", u"Burmese"],
[u"ca", u"Catalan; Valencian"],
[u"ch", u"Chamorro"],
[u"ce", u"Chechen"],
[u"zh", u"Chinese"],
[u"cu", u"Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"],
[u"cv", u"Chuvash"],
[u"kw", u"Cornish"],
[u"co", u"Corsican"],
[u"cr", u"Cree"],
[u"cs", u"Czech"],
[u"da", u"Danish"],
[u"dv", u"Divehi; Dhivehi; Maldivian"],
[u"nl", u"Dutch; Flemish"],
[u"dz", u"Dzongkha"],
[u"en", u"English"],
[u"eo", u"Esperanto"],
[u"et", u"Estonian"],
[u"ee", u"Ewe"],
[u"fo", u"Faroese"],
[u"fj", u"Fijian"],
[u"fi", u"Finnish"],
[u"fr", u"French"],
[u"fy", u"Western Frisian"],
[u"ff", u"Fulah"],
[u"ka", u"Georgian"],
[u"de", u"German"],
[u"gd", u"Gaelic; Scottish Gaelic"],
[u"ga", u"Irish"],
[u"gl", u"Galician"],
[u"gv", u"Manx"],
[u"el", u"Greek, Modern (1453-)"],
[u"gn", u"Guarani"],
[u"gu", u"Gujarati"],
[u"ht", u"Haitian; Haitian Creole"],
[u"ha", u"Hausa"],
[u"he", u"Hebrew"],
[u"hz", u"Herero"],
[u"hi", u"Hindi"],
[u"ho", u"Hiri Motu"],
[u"hr", u"Croatian"],
[u"hu", u"Hungarian"],
[u"ig", u"Igbo"],
[u"is", u"Icelandic"],
[u"io", u"Ido"],
[u"ii", u"Sichuan Yi; Nuosu"],
[u"iu", u"Inuktitut"],
[u"ie", u"Interlingue; Occidental"],
[u"ia", u"Interlingua (International Auxiliary Language Association)"],
[u"id", u"Indonesian"],
[u"ik", u"Inupiaq"],
[u"it", u"Italian"],
[u"jv", u"Javanese"],
[u"ja", u"Japanese"],
[u"kl", u"Kalaallisut; Greenlandic"],
[u"kn", u"Kannada"],
[u"ks", u"Kashmiri"],
[u"kr", u"Kanuri"],
[u"kk", u"Kazakh"],
[u"km", u"Central Khmer"],
[u"ki", u"Kikuyu; Gikuyu"],
[u"rw", u"Kinyarwanda"],
[u"ky", u"Kirghiz; Kyrgyz"],
[u"kv", u"Komi"],
[u"kg", u"Kongo"],
[u"ko", u"Korean"],
[u"kj", u"Kuanyama; Kwanyama"],
[u"ku", u"Kurdish"],
[u"lo", u"Lao"],
[u"la", u"Latin"],
[u"lv", u"Latvian"],
[u"li", u"Limburgan; Limburger; Limburgish"],
[u"ln", u"Lingala"],
[u"lt", u"Lithuanian"],
[u"lb", u"Luxembourgish; Letzeburgesch"],
[u"lu", u"Luba-Katanga"],
[u"lg", u"Ganda"],
[u"mk", u"Macedonian"],
[u"mh", u"Marshallese"],
[u"ml", u"Malayalam"],
[u"mi", u"Maori"],
[u"mr", u"Marathi"],
[u"ms", u"Malay"],
[u"mg", u"Malagasy"],
[u"mt", u"Maltese"],
[u"mn", u"Mongolian"],
[u"na", u"Nauru"],
[u"nv", u"Navajo; Navaho"],
[u"nr", u"Ndebele, South; South Ndebele"],
[u"nd", u"Ndebele, North; North Ndebele"],
[u"ng", u"Ndonga"],
[u"ne", u"Nepali"],
[u"nn", u"Norwegian Nynorsk; Nynorsk, Norwegian"],
[u"nb", u"Bokmål, Norwegian; Norwegian Bokmål"],
[u"no", u"Norwegian"],
[u"ny", u"Chichewa; Chewa; Nyanja"],
[u"oc", u"Occitan (post 1500); Provençal"],
[u"oj", u"Ojibwa"],
[u"or", u"Oriya"],
[u"om", u"Oromo"],
[u"os", u"Ossetian; Ossetic"],
[u"pa", u"Panjabi; Punjabi"],
[u"fa", u"Persian"],
[u"pi", u"Pali"],
[u"pl", u"Polish"],
[u"pt", u"Portuguese"],
[u"ps", u"Pushto; Pashto"],
[u"qu", u"Quechua"],
[u"rm", u"Romansh"],
[u"ro", u"Romanian; Moldavian; Moldovan"],
[u"rn", u"Rundi"],
[u"ru", u"Russian"],
[u"sg", u"Sango"],
[u"sa", u"Sanskrit"],
[u"si", u"Sinhala; Sinhalese"],
[u"sk", u"Slovak"],
[u"sl", u"Slovenian"],
[u"se", u"Northern Sami"],
[u"sm", u"Samoan"],
[u"sn", u"Shona"],
[u"sd", u"Sindhi"],
[u"so", u"Somali"],
[u"st", u"Sotho, Southern"],
[u"es", u"Spanish; Castilian"],
[u"sc", u"Sardinian"],
[u"sr", u"Serbian"],
[u"ss", u"Swati"],
[u"su", u"Sundanese"],
[u"sw", u"Swahili"],
[u"sv", u"Swedish"],
[u"ty", u"Tahitian"],
[u"ta", u"Tamil"],
[u"tt", u"Tatar"],
[u"te", u"Telugu"],
[u"tg", u"Tajik"],
[u"tl", u"Tagalog"],
[u"th", u"Thai"],
[u"bo", u"Tibetan"],
[u"ti", u"Tigrinya"],
[u"to", u"Tonga (Tonga Islands)"],
[u"tn", u"Tswana"],
[u"ts", u"Tsonga"],
[u"tk", u"Turkmen"],
[u"tr", u"Turkish"],
[u"tw", u"Twi"],
[u"ug", u"Uighur; Uyghur"],
[u"uk", u"Ukrainian"],
[u"ur", u"Urdu"],
[u"uz", u"Uzbek"],
[u"ve", u"Venda"],
[u"vi", u"Vietnamese"],
[u"vo", u"Volapük"],
[u"cy", u"Welsh"],
[u"wa", u"Walloon"],
[u"wo", u"Wolof"],
[u"xh", u"Xhosa"],
[u"yi", u"Yiddish"],
[u"yo", u"Yoruba"],
[u"za", u"Zhuang; Chuang"],
[u"zu", u"Zulu"]
)
...@@ -25,10 +25,13 @@ ...@@ -25,10 +25,13 @@
data-saved-video-position="${saved_video_position}" data-saved-video-position="${saved_video_position}"
data-start="${start}" data-start="${start}"
data-end="${end}" data-end="${end}"
data-caption-asset-path="${caption_asset_path}" data-transcript-language="${transcript_language}"
data-transcript-languages='${transcript_languages}'
data-autoplay="${autoplay}" data-autoplay="${autoplay}"
data-yt-test-timeout="${yt_test_timeout}" data-yt-test-timeout="${yt_test_timeout}"
data-yt-test-url="${yt_test_url}" data-yt-test-url="${yt_test_url}"
data-transcript-translation-url="${transcript_translation_url}"
data-transcript-available-translations-url="${transcript_available_translations_url}"
## For now, the option "data-autohide-html5" is hard coded. This option ## For now, the option "data-autohide-html5" is hard coded. This option
## either enables or disables autohiding of controls and captions on mouse ## either enables or disables autohiding of controls and captions on mouse
...@@ -67,12 +70,12 @@ ...@@ -67,12 +70,12 @@
<li><div class="vidtime">0:00 / 0:00</div></li> <li><div class="vidtime">0:00 / 0:00</div></li>
</ul> </ul>
<div class="secondary-controls"> <div class="secondary-controls">
<div class="speeds"> <div class="speeds menu-container">
<a href="#" title="${_('Speeds')}" role="button" aria-disabled="false"> <a href="#" title="${_('Speeds')}" role="button" aria-disabled="false">
<h3>${_('Speed')}</h3> <h3>${_('Speed')}</h3>
<p class="active"></p> <p class="active"></p>
</a> </a>
<ol class="video_speeds" role="menu"></ol> <ol class="video_speeds menu" role="menu"></ol>
</div> </div>
<div class="volume"> <div class="volume">
<a href="#" title="${_('Volume')}" role="button" aria-disabled="false"></a> <a href="#" title="${_('Volume')}" role="button" aria-disabled="false"></a>
...@@ -83,7 +86,10 @@ ...@@ -83,7 +86,10 @@
<a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a> <a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a>
<a href="#" class="quality_control" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a> <a href="#" class="quality_control" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a>
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-disabled="false">${_('Turn off captions')}</a> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-
disabled="false">${_('Turn off captions')}</a>
</div>
</div> </div>
</div> </div>
</section> </section>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment