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,
in roughly chronological order, most recent first. Add your entries at or near
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
specifying the giturl in the course settings.
......
......@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
# Check if the web object is a list type
# 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'))
assert_equal(value, list_value)
elif setting.has_class('metadata-videolist-enum'):
......
......@@ -103,7 +103,7 @@ def i_do_not_see_error_message(_step):
@step('I see error message "([^"]*)"$')
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$')
......@@ -114,7 +114,7 @@ def i_do_not_see_status_message(_step):
@step('I see status message "([^"]*)"$')
def i_see_status_message(_step, status):
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]
if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) \
......
@shard_3
Feature: CMS.Video Component Editor
As a course author, I want to be able to create video components.
Feature: CMS Video Component Editor
As a course author, I want to be able to create video components
Scenario: User can view Video metadata
Given I have created a Video component
......@@ -17,14 +17,14 @@ Feature: CMS.Video Component Editor
# Sauce Labs cannot delete cookies
@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
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
# Sauce Labs cannot delete cookies
@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
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
......@@ -5,7 +5,7 @@ from lettuce import world, step
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):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
......@@ -13,7 +13,7 @@ def set_show_captions(step, setting):
world.css_click('a.edit-button')
world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.click_link_by_text('Advanced')
world.browser.select('Show Transcript', setting)
world.browser.select('Transcript Display', setting)
world.css_click('a.save-button')
......@@ -42,10 +42,11 @@ def correct_video_settings(_step):
['Display Name', 'Video', False],
['Download Transcript', '', False],
['End Time', '00:00:00', False],
['HTML5 Transcript', '', False],
['Show Transcript', 'True', False],
['Start Time', '00:00:00', False],
['Transcript (primary)', '', False],
['Transcript Display', 'True', False],
['Transcript Download Allowed', 'False', False],
['Transcript Translations', '', False],
['Video Download Allowed', 'False', False],
['Video Sources', '', False],
['Youtube ID', 'OEoXaMPEzfM', False],
......
@shard_3
Feature: CMS.Video Component
As a course author, I want to be able to view my created videos in Studio.
Feature: CMS Video Component
As a course author, I want to be able to view my created videos in Studio
# 1
# Video Alpha Features will work in Firefox only when Firefox is the active window
......@@ -43,38 +43,6 @@ Feature: CMS.Video Component
Then the correct Youtube video is shown
# 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.
# Enabled back on 11/29.
Scenario: When enter key is pressed on a caption shows an outline around it
......@@ -84,7 +52,7 @@ Feature: CMS.Video Component
Then I press "enter" button on caption line with data-index "0"
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
Given I have created a Video component with subtitles
And Make sure captions are closed
......
......@@ -56,6 +56,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
world.visit(video_url)
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.wait_for_present('.is-initialized')
......
......@@ -492,15 +492,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, 'i4x://edX/toy/video/video_with_end_time')
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):
""" Preview test case. """
direct_store = modulestore('direct')
......
......@@ -3,11 +3,13 @@ import unittest
from uuid import uuid4
import copy
import textwrap
from mock import patch, Mock
from pymongo import MongoClient
from django.test.utils import override_settings
from django.conf import settings
from django.utils import translation
from nose.plugins.skip import SkipTest
......@@ -16,7 +18,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.exceptions import NotFoundError
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
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
......@@ -188,20 +190,29 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
def test_success_downloading_subs(self):
# Disabled 11/14/13
# 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
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>
""")
good_youtube_subs = {
0.5: 'JMD_ifUUfsU',
1.0: 'hI10vDNYz4M',
2.0: 'AKqURZnYqpk'
0.5: 'good_id_1',
1.0: 'good_id_2',
2.0: 'good_id_3'
}
self.clear_subs_content(good_youtube_subs)
with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
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)
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.
for subs_id in good_youtube_subs.values():
......@@ -226,12 +237,10 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self.assertEqual(html5_ids[2], 'baz.1.4')
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
# 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
mock_get.return_value = Mock(status_code=404, text='Error 404')
bad_youtube_subs = {
0.5: 'BAD_YOUTUBE_ID1',
......@@ -239,9 +248,8 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
2.0: 'BAD_YOUTUBE_ID3'
}
self.clear_subs_content(bad_youtube_subs)
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.
for subs_id in bad_youtube_subs.values():
......@@ -267,7 +275,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self.clear_subs_content(good_youtube_subs)
# 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.
for subs_id in good_youtube_subs.values():
......@@ -438,3 +446,43 @@ class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs):
}
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1)
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
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore import Location
from xmodule.video_module import manage_video_subtitles_save
from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool
from ..transcripts_utils import manage_video_subtitles_save
from ..utils import get_modulestore
from .access import has_course_access
......@@ -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.")
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
old_metadata = own_metadata(existing_item)
if publish:
if publish == 'make_private':
_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
field.write_to(existing_item, value)
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
store.update_item(existing_item, request.user.id)
......
......@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_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 cache_toolbox.core import del_cached_content
from xmodule.modulestore.django import modulestore
......
......@@ -26,12 +26,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
from util.json_request import JsonResponse
from xmodule.modulestore.locator import BlockUsageLocator
from ..transcripts_utils import (
from xmodule.video_module.transcripts_utils import (
generate_subs_from_source,
generate_srt_from_sjson, remove_subs_from_store,
download_youtube_subs, get_transcripts_from_youtube,
copy_or_rename_transcript,
save_module,
manage_video_subtitles_save,
TranscriptsGenerationException,
GetTranscriptsFromYouTubeException,
......@@ -136,7 +135,7 @@ def upload_transcripts(request):
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.
save_module(item, request.user)
item.save_with_metadata(request.user)
response['subs'] = item.sub
response['status'] = 'Success'
else:
......@@ -272,7 +271,11 @@ def check_transcripts(request):
#check youtube local and server transcripts for equality
if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
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
transcripts_presence['youtube_diff'] = False
except GetTranscriptsFromYouTubeException:
......@@ -389,7 +392,7 @@ def choose_transcripts(request):
if item.sub != html5_id: # update sub value
item.sub = html5_id
save_module(item, request.user)
item.save_with_metadata(request.user)
response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response)
......@@ -415,12 +418,12 @@ def replace_transcripts(request):
return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id))
try:
download_youtube_subs({1.0: youtube_id}, item)
download_youtube_subs({1.0: youtube_id}, item, settings)
except GetTranscriptsFromYouTubeException as e:
return error_response(response, e.message)
item.sub = youtube_id
save_module(item, request.user)
item.save_with_metadata(request.user)
response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response)
......@@ -519,10 +522,10 @@ def save_transcripts(request):
for metadata_key, value in metadata.items():
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:
manage_video_subtitles_save(None, item, request.user)
manage_video_subtitles_save(item, request.user)
else:
# 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.
......
......@@ -26,7 +26,9 @@ Longer TODO:
import sys
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 lms.lib.xblock.mixin import LmsBlockMixin
......
......@@ -11,7 +11,10 @@
data-start=""
data-end=""
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-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
......@@ -51,9 +54,11 @@
</div>
<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>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div>
</section>
</article>
......
......@@ -10,7 +10,10 @@
data-start=""
data-end=""
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-mp4-source="xmodule/include/fixtures/test.mp4"
data-webm-source="xmodule/include/fixtures/test.webm"
......@@ -54,9 +57,11 @@
</div>
<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>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div>
</section>
</article>
......
......@@ -10,7 +10,10 @@
data-start=""
data-end=""
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-mp4-source="xmodule/include/fixtures/test.mp4"
data-webm-source="xmodule/include/fixtures/test.webm"
......
......@@ -11,7 +11,10 @@
data-start=""
data-end=""
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-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
......
......@@ -11,7 +11,10 @@
data-start=""
data-end=""
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-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
......@@ -51,9 +54,11 @@
</div>
<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>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div>
</section>
</article>
......@@ -73,9 +78,13 @@
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-speed="1.0"
data-start=""
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-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
......@@ -112,7 +121,9 @@
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</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>
</section>
......@@ -132,9 +143,13 @@
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-speed="1.0"
data-start=""
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-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
......@@ -171,7 +186,9 @@
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</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>
</section>
......
(function ($, undefined) {
var oldAjaxWithPrefix = $.ajaxWithPrefix;
// Stub YouTube API.
window.YT = {
Player: function () {
......@@ -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.
window.WAIT_TIMEOUT = 5000;
......@@ -145,13 +107,16 @@
jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'];
jasmine.stubRequests = function () {
return spyOn($, 'ajax').andCallFake(function (settings) {
var match, status, callCallback;
var spy = $.ajax;
if (
match = settings.url
.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)
) {
if (!($.ajax.isSpy)) {
spy = spyOn($, 'ajax');
}
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('_');
if (status && status[0] === 'status') {
callCallback = function (callback) {
......@@ -177,11 +142,10 @@
}
};
}
} else if (
match = settings.url
.match(/static(\/.*)?\/subs\/(.+)\.srt\.sjson/)
) {
} else if (settings.url == '/transcript/translation') {
return settings.success(jasmine.stubbedCaption);
} else if (settings.url == '/transcript/available_translations') {
return settings.success(['uk', 'de']);
} else if (settings.url.match(/.+\/problem_get$/)) {
return settings.success({
html: readFixtures('problem_content.html')
......@@ -265,6 +229,7 @@
.data(params);
}
jasmine.stubRequests();
state = new Video('#example');
state.resizer = (function () {
......
......@@ -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 () {
var oldUserAgent;
......@@ -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 () {
beforeEach(function () {
loadFixtures('video_html5.html');
......
......@@ -6,8 +6,14 @@ require(
['video/01_initialize.js'],
function (Initialize) {
describe('Initialize', function () {
var state = {};
afterEach(function () {
state = {};
});
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
// that Math.round() is called.
......@@ -40,10 +46,6 @@ function (Initialize) {
spyOn(Time, 'formatFull').andCallThrough();
});
afterEach(function () {
state = undefined;
});
it('data is not an object, async is true', function () {
itSpec({
asyncVal: true,
......@@ -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));
......@@ -57,9 +57,11 @@ function (VideoPlayer, VideoStorage) {
});
});
},
methodsDict = {
bindTo: bindTo,
fetchMetadata: fetchMetadata,
getCurrentLanguage: getCurrentLanguage,
getDuration: getDuration,
getVideoMetadata: getVideoMetadata,
initialize: initialize,
......@@ -305,6 +307,11 @@ function (VideoPlayer, VideoStorage) {
value ||
'1.0';
},
'transcriptLanguage': function (value) {
return storage.getItem('language') ||
value ||
'en';
},
'ytTestTimeout': function (value) {
value = parseInt(value, 10);
......@@ -432,6 +439,7 @@ function (VideoPlayer, VideoStorage) {
this.config.endTime = null;
}
this.lang = this.config.transcriptLanguage;
this.speed = Number(
this.config.speed || this.config.generalSpeed
).toFixed(2).replace(/\.00$/, '.0');
......@@ -631,17 +639,16 @@ function (VideoPlayer, VideoStorage) {
function setSpeed(newSpeed, updateStorage) {
// Possible speeds for each player type.
// flash = [0.75, 1, 1.25, 1.5]
// html5 = [0.75, 1, 1.25, 1.5]
// youtube html5 = [0.25, 0.5, 1, 1.5, 2]
// 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]
var map = {
'0.25': '0.75',
'0.50': '0.75',
'0.75': '0.50',
'1.25': '1.50',
'2.0': '1.50'
},
useSession = true;
'0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
'0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
'0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
'1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
'2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
};
if (_.contains(this.speeds, newSpeed)) {
this.speed = newSpeed;
......@@ -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
* chain with a method (function) at the end. It will call this function.
......
......@@ -5,30 +5,16 @@ define(
'video/03_video_player.js',
['video/02_html5_video.js', 'video/00_resizer.js'],
function (HTML5Video, Resizer) {
var dfd = $.Deferred();
// VideoPlayer() function - what this module "exports".
return function (state) {
var dfd = $.Deferred(),
VideoPlayer = function (state) {
state.videoPlayer = {};
_makeFunctionsPublic(state);
_initialize(state);
// No callbacks to DOM events (click, mousemove, etc.).
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 = {
},
methodsDict = {
duration: duration,
handlePlaybackQualityChange: handlePlaybackQualityChange,
isPlaying: isPlaying,
......@@ -46,10 +32,25 @@ function (HTML5Video, Resizer) {
onVolumeChange: onVolumeChange,
pause: pause,
play: play,
setPlaybackRate: setPlaybackRate,
update: update,
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);
}
......@@ -70,7 +71,7 @@ function (HTML5Video, Resizer) {
$(window).on('unload', state.saveState);
if (state.currentPlayerMode !== 'flash') {
state.videoPlayer.onSpeedChange(state.speed);
state.videoPlayer.setPlaybackRate(state.speed);
}
state.videoPlayer.player.setVolume(state.currentVolume);
});
......@@ -325,33 +326,10 @@ function (HTML5Video, Resizer) {
}
}
function onSpeedChange(newSpeed) {
function setPlaybackRate(newSpeed) {
var time = this.videoPlayer.currentTime,
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 (
this.currentPlayerMode === 'html5' &&
!(
......@@ -377,7 +355,33 @@ function (HTML5Video, Resizer) {
this.videoPlayer.player[methodName](youtubeId, 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.saveState(true, { speed: newSpeed });
......
......@@ -113,7 +113,7 @@ class ModelsTest(unittest.TestCase):
def test_load_class(self):
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)
......
......@@ -20,7 +20,7 @@ from mock import Mock
from . import LogicTest
from lxml import etree
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 xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
......@@ -150,7 +150,7 @@ class VideoDescriptorTest(unittest.TestCase):
descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
descriptor.youtube_id_1_5 = '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):
"""
......@@ -165,7 +165,7 @@ class VideoDescriptorTest(unittest.TestCase):
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
descriptor.youtube_id_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):
......@@ -193,6 +193,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>
<transcript language="ua" src="ukrainian_translation.srt" />
<transcript language="ge" src="german_translation.srt" />
</video>
'''
location = Location(["i4x", "edX", "video", "default",
......@@ -215,7 +217,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'track': 'http://www.example.com/track',
'download_track': True,
'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):
......@@ -230,6 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
end_time="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
<transcript language="ua" src="ukrainian_translation.srt" />
<transcript language="ge" src="german_translation.srt" />
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system, Mock())
......@@ -245,7 +250,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'download_track': False,
'download_video': False,
'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):
......@@ -304,7 +310,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'download_track': True,
'download_video': True,
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
'data': '',
'transcripts': {},
})
def test_from_xml_no_attributes(self):
......@@ -326,7 +333,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'download_track': False,
'download_video': False,
'html5_sources': [],
'data': ''
'data': '',
'transcripts': {},
})
def test_from_xml_double_quotes(self):
......@@ -508,6 +516,7 @@ class VideoExportTestCase(unittest.TestCase):
desc.download_track = True
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
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
expected = etree.fromstring('''\
......@@ -515,9 +524,10 @@ class VideoExportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>
<transcript language="ge" src="german_translation.srt" />
<transcript language="ua" src="ukrainian_translation.srt" />
</video>
''')
self.assertXmlEqual(expected, xml)
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
Feature: LMS.Video component
As a student, I want to view course videos in LMS.
Feature: LMS Video component
As a student, I want to view course videos in LMS
# 0
Scenario: Video component stores position correctly when page is reloaded
......@@ -58,7 +58,7 @@ Feature: LMS.Video component
And error message has correct text
# 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"
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
......@@ -78,3 +78,15 @@ Feature: LMS.Video component
Then video "B" should start playing at speed "0.50"
When I open video "C"
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
from lettuce import world, step
import json
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
import os
from functools import partial
from xmodule.contentstore.django import contentstore
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
LANGUAGES = settings.ALL_LANGUAGES
############### ACTIONS ####################
......@@ -14,16 +25,23 @@ HTML5_SOURCES = [
HTML5_SOURCES_INCORRECT = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99',
]
VIDEO_BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
}
VIDEO_MENUS = {
'language': '.lang .menu',
'speed': '.speed .menu',
}
# We should wait 300 ms for event handler invocation + 200ms for safety.
DELAY = 0.5
VIDEO_BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
}
coursenum = 'test_course'
sequence = {}
......@@ -33,20 +51,20 @@ def does_not_autoplay(_step, video_type):
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):
i_am_registered_for_the_course(_step, coursenum)
# 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')
@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):
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$')
......@@ -70,49 +88,55 @@ def check_video_speed(_step, player_id, speed):
speed_css = '.speeds p.active'
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'
kwargs = {
'parent_location': section_location(course),
'category': category,
'display_name': display_name
'display_name': display_name,
'metadata': {},
}
if player_mode == 'html5':
kwargs.update({
'metadata': {
kwargs['metadata'].update({
'youtube_id_1_0': '',
'youtube_id_0_75': '',
'youtube_id_1_25': '',
'youtube_id_1_5': '',
'html5_sources': HTML5_SOURCES
}
})
if player_mode == 'youtube_html5':
kwargs.update({
'metadata': {
kwargs['metadata'].update({
'html5_sources': HTML5_SOURCES
}
})
if player_mode == 'youtube_html5_unsupported_video':
kwargs.update({
'metadata': {
kwargs['metadata'].update({
'html5_sources': HTML5_SOURCES_INCORRECT
}
})
if player_mode == 'html5_unsupported_video':
kwargs.update({
'metadata': {
kwargs['metadata'].update({
'youtube_id_1_0': '',
'youtube_id_0_75': '',
'youtube_id_1_25': '',
'youtube_id_1_5': '',
'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$')
......@@ -152,6 +176,92 @@ def error_message_has_correct_text(_step):
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):
sequence_css = 'a[data-element="{0}"]'.format(number)
world.css_click(sequence_css)
......@@ -165,7 +275,6 @@ def _change_video_speed(speed):
@step('I click video button "([^"]*)"$')
def click_button_video(_step, button_type):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
world.css_click(VIDEO_BUTTONS[button])
......@@ -184,3 +293,9 @@ def seek_video_to_n_seconds(_step, seconds):
time = float(seconds.strip())
jsCode = "$('.video').data('video-player-state').videoPlayer.onSlideSeek({{time: {0:f}}})".format(time)
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):
self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data)
self.item_descriptor.xmodule_runtime = self.new_module_runtime()
self.item_module = self.item_descriptor
self.item_url = Location(self.item_module.location).url()
#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):
self.course = CourseFactory.create(data=self.COURSE_DATA)
......@@ -130,7 +132,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
self.assertTrue(all(self.login_statuses))
def setUp(self):
self.setup_course();
self.setup_course()
self.initialize_module(metadata=self.METADATA, data=self.DATA)
def get_url(self, dispatch):
......
......@@ -27,8 +27,8 @@ class TestLTI(BaseTestXmodule):
mocked_signature_after_sign = u'my_signature%3D'
mocked_decoded_signature = u'my_signature='
lti_id = self.item_module.lti_id
module_id = unicode(urllib.quote(self.item_module.id))
lti_id = self.item_descriptor.lti_id
module_id = unicode(urllib.quote(self.item_descriptor.id))
user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id)
sourcedId = "{id}:{resource_link}:{user_id}".format(
......@@ -39,7 +39,7 @@ class TestLTI(BaseTestXmodule):
lis_outcome_service_url = 'https://{host}{path}'.format(
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 = {
u'user_id': user_id,
......@@ -63,13 +63,13 @@ class TestLTI(BaseTestXmodule):
saved_sign = oauthlib.oauth1.Client.sign
self.expected_context = {
'display_name': self.item_module.display_name,
'display_name': self.item_descriptor.display_name,
'input_fields': self.correct_headers,
'element_class': self.item_module.category,
'element_id': self.item_module.location.html_id(),
'element_class': self.item_descriptor.category,
'element_id': self.item_descriptor.location.html_id(),
'launch_url': 'http://www.example.com', # default value
'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):
......@@ -92,11 +92,11 @@ class TestLTI(BaseTestXmodule):
self.addCleanup(patcher.stop)
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)
self.assertEqual(generated_content, expected_content)
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)
self.assertEqual(generated_content, expected_content)
......@@ -32,6 +32,7 @@ SOURCE_XML = """
>
<source src="example.mp4"/>
<source src="example.webm"/>
<transcript language="uk" src="ukrainian_translation.srt" />
</video>
"""
......
......@@ -242,12 +242,12 @@ class TestWordCloud(BaseTestXmodule):
def test_word_cloud_constructor(self):
"""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 = {
'ajax_url': self.item_module.xmodule_runtime.ajax_url,
'element_class': self.item_module.location.category,
'element_id': self.item_module.location.html_id(),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
'element_class': self.item_descriptor.location.category,
'element_id': self.item_descriptor.location.html_id(),
'num_inputs': 5, # default value
'submitted': False # default value
}
......
......@@ -1287,3 +1287,193 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
##### LMS DEADLINE DISPLAY TIME_ZONE #######
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 @@
data-saved-video-position="${saved_video_position}"
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-transcript-language="${transcript_language}"
data-transcript-languages='${transcript_languages}'
data-autoplay="${autoplay}"
data-yt-test-timeout="${yt_test_timeout}"
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
## either enables or disables autohiding of controls and captions on mouse
......@@ -67,12 +70,12 @@
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<div class="speeds menu-container">
<a href="#" title="${_('Speeds')}" role="button" aria-disabled="false">
<h3>${_('Speed')}</h3>
<p class="active"></p>
</a>
<ol class="video_speeds" role="menu"></ol>
<ol class="video_speeds menu" role="menu"></ol>
</div>
<div class="volume">
<a href="#" title="${_('Volume')}" role="button" aria-disabled="false"></a>
......@@ -83,7 +86,10 @@
<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="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>
</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