Commit 7b2d67f2 by polesye

BLD-1049: Allow the video player to work with redirected links.

parent 7228d41a
...@@ -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: Fix bug with incorrect link format and redirection. BLD-1049
Blades: Fix bug with incorrect RelativeTime value after XML serialization. BLD-1060 Blades: Fix bug with incorrect RelativeTime value after XML serialization. BLD-1060
LMS: Update bulk email implementation to lessen load on the database LMS: Update bulk email implementation to lessen load on the database
......
...@@ -34,14 +34,22 @@ Feature: CMS Transcripts ...@@ -34,14 +34,22 @@ Feature: CMS Transcripts
And I expect inputs are enabled And I expect inputs are enabled
#User input URL with incorrect format #User input URL with incorrect format
And I enter a "htt://link.c" source to field number 1 And I enter a "http://link.c" source to field number 1
Then I see error message "url_format" Then I see error message "url_format"
# Currently we are working with 1st field. It means, that if 1st field # Currently we are working with 1st field. It means, that if 1st field
# contain incorrect value, 2nd and 3rd fields should be disabled until # contain incorrect value, 2nd and 3rd fields should be disabled until
# 1st field will be filled by correct correct value # 1st field will be filled by correct correct value
And I expect 2, 3 inputs are disabled And I expect 2, 3 inputs are disabled
# We are not clearing fields here,
# Because we changing same field. #User input URL with incorrect format
And I enter a "http://goo.gl/pxxZrg" source to field number 1
And I enter a "http://goo.gl/pxxZrg" source to field number 2
Then I see error message "links_duplication"
And I expect 1, 3 inputs are disabled
And I clear fields
And I expect inputs are enabled
And I enter a "http://youtu.be/t_not_exist" source to field number 1 And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I do not see error message Then I do not see error message
And I expect inputs are enabled And I expect inputs are enabled
...@@ -699,3 +707,37 @@ Feature: CMS Transcripts ...@@ -699,3 +707,37 @@ Feature: CMS Transcripts
And I edit the component And I edit the component
Then I see status message "found" Then I see status message "found"
#37 Uploading subtitles with different file name than file
Scenario: Shortened link: File name and name of subs are different
Given I have created a Video component
And I edit the component
And I enter a "http://goo.gl/pxxZrg" source to field number 1
And I see status message "not found"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "pxxZrg" in the field "Default Timed Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
#38 Uploading subtitles with different file name than file
Scenario: Relative link: File name and name of subs are different
Given I have created a Video component
And I edit the component
And I enter a "/gizmo.webm" source to field number 1
And I see status message "not found"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "gizmo" in the field "Default Timed Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
...@@ -19,6 +19,7 @@ DELAY = 0.5 ...@@ -19,6 +19,7 @@ DELAY = 0.5
ERROR_MESSAGES = { ERROR_MESSAGES = {
'url_format': u'Incorrect url format.', 'url_format': u'Incorrect url format.',
'file_type': u'Link types should be unique.', 'file_type': u'Link types should be unique.',
'links_duplication': u'Links should be unique.',
} }
STATUSES = { STATUSES = {
...@@ -43,7 +44,7 @@ TRANSCRIPTS_BUTTONS = { ...@@ -43,7 +44,7 @@ TRANSCRIPTS_BUTTONS = {
'import': ('.setting-import', 'Import YouTube Transcript'), 'import': ('.setting-import', 'Import YouTube Transcript'),
'download_to_edit': ('.setting-download', 'Download Transcript for Editing'), 'download_to_edit': ('.setting-download', 'Download Transcript for Editing'),
'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download Transcript for Editing'), 'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download Transcript for Editing'),
'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Transcript'), 'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Transcript'),
'replace': ('.setting-replace', 'Yes, replace the edX transcript with the YouTube transcript'), 'replace': ('.setting-replace', 'Yes, replace the edX transcript with the YouTube transcript'),
'choose': ('.setting-choose', 'Timed Transcript from {}'), 'choose': ('.setting-choose', 'Timed Transcript from {}'),
'use_existing': ('.setting-use-existing', 'Use Current Transcript'), 'use_existing': ('.setting-use-existing', 'Use Current Transcript'),
...@@ -118,8 +119,7 @@ def i_see_status_message(_step, status): ...@@ -118,8 +119,7 @@ def i_see_status_message(_step, status):
assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status]) 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) and not world.css_find(DOWNLOAD_BUTTON)[0].has_class('is-disabled'):
and not world.css_find(DOWNLOAD_BUTTON)[0].has_class('is-disabled'):
assert _transcripts_are_downloaded() assert _transcripts_are_downloaded()
...@@ -210,7 +210,7 @@ def check_text_in_the_captions(_step, text): ...@@ -210,7 +210,7 @@ def check_text_in_the_captions(_step, text):
@step('I see value "([^"]*)" in the field "([^"]*)"$') @step('I see value "([^"]*)" in the field "([^"]*)"$')
def check_transcripts_field(_step, values, field_name): def check_transcripts_field(_step, values, field_name):
world.select_editor_tab('Advanced') world.select_editor_tab('Advanced')
tab = world.css_find('#settings-tab').first; tab = world.css_find('#settings-tab').first
field_id = '#' + tab.find_by_xpath('.//label[text()="%s"]' % field_name.strip())[0]['for'] field_id = '#' + tab.find_by_xpath('.//label[text()="%s"]' % field_name.strip())[0]['for']
values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')] values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')]
assert any(values_list) assert any(values_list)
...@@ -229,19 +229,19 @@ def open_tab(_step, tab_name): ...@@ -229,19 +229,19 @@ def open_tab(_step, tab_name):
@step('I set value "([^"]*)" to the field "([^"]*)"$') @step('I set value "([^"]*)" to the field "([^"]*)"$')
def set_value_transcripts_field(_step, value, field_name): def set_value_transcripts_field(_step, value, field_name):
tab = world.css_find('#settings-tab').first; tab = world.css_find('#settings-tab').first
XPATH = './/label[text()="{name}"]'.format(name=field_name) XPATH = './/label[text()="{name}"]'.format(name=field_name)
SELECTOR = '#' + tab.find_by_xpath(XPATH)[0]['for'] SELECTOR = '#' + tab.find_by_xpath(XPATH)[0]['for']
element = world.css_find(SELECTOR).first element = world.css_find(SELECTOR).first
if element['type'] == 'text': if element['type'] == 'text':
SCRIPT = '$("{selector}").val("{value}").change()'.format( SCRIPT = '$("{selector}").val("{value}").change()'.format(
selector=SELECTOR, selector=SELECTOR,
value=value value=value
) )
world.browser.execute_script(SCRIPT) world.browser.execute_script(SCRIPT)
assert world.css_has_value(SELECTOR, value) assert world.css_has_value(SELECTOR, value)
else: else:
assert False, 'Incorrect element type.'; assert False, 'Incorrect element type.'
world.wait_for_ajax_complete() world.wait_for_ajax_complete()
......
...@@ -26,7 +26,7 @@ describe('Transcripts.Utils', function () { ...@@ -26,7 +26,7 @@ describe('Transcripts.Utils', function () {
} (videoId)), } (videoId)),
html5FileName = 'file_name', html5FileName = 'file_name',
html5LinksList = (function (videoName) { html5LinksList = (function (videoName) {
var videoTypes = ['mp4', 'webm'], var videoTypes = ['mp4', 'webm', 'm4v', 'ogv'],
links = [ links = [
'http://somelink.com/%s.%s?param=1&param=2#hash', 'http://somelink.com/%s.%s?param=1&param=2#hash',
'http://somelink.com/%s.%s#hash', 'http://somelink.com/%s.%s#hash',
...@@ -34,6 +34,7 @@ describe('Transcripts.Utils', function () { ...@@ -34,6 +34,7 @@ describe('Transcripts.Utils', function () {
'http://somelink.com/%s.%s', 'http://somelink.com/%s.%s',
'ftp://somelink.com/%s.%s', 'ftp://somelink.com/%s.%s',
'https://somelink.com/%s.%s', 'https://somelink.com/%s.%s',
'https://somelink.com/sub/sub/%s.%s',
'http://cdn.somecdn.net/v/%s.%s', 'http://cdn.somecdn.net/v/%s.%s',
'somelink.com/%s.%s', 'somelink.com/%s.%s',
'%s.%s' '%s.%s'
...@@ -48,7 +49,25 @@ describe('Transcripts.Utils', function () { ...@@ -48,7 +49,25 @@ describe('Transcripts.Utils', function () {
return data; return data;
} (html5FileName)); } (html5FileName)),
otherLinkId = 'other_link_id',
otherLinksList = (function (linkId) {
var links = [
'http://goo.gl/%s?param=1&param=2#hash',
'http://goo.gl/%s?param=1&param=2',
'http://goo.gl/%s#hash',
'http://goo.gl/%s',
'http://goo.gl/%s',
'ftp://goo.gl/%s',
'https://goo.gl/%s',
'%s'
];
return $.map(links, function (link) {
return _str.sprintf(link, linkId);
});
} (otherLinkId));
describe('Method: getField', function (){ describe('Method: getField', function (){
var collection, var collection,
...@@ -107,7 +126,6 @@ describe('Transcripts.Utils', function () { ...@@ -107,7 +126,6 @@ describe('Transcripts.Utils', function () {
}); });
describe('Wrong arguments ', function () { describe('Wrong arguments ', function () {
beforeEach(function(){ beforeEach(function(){
spyOn(console, 'log'); spyOn(console, 'log');
}); });
...@@ -124,18 +142,9 @@ describe('Transcripts.Utils', function () { ...@@ -124,18 +142,9 @@ describe('Transcripts.Utils', function () {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('videoId is wrong', function () {
var videoId = 'wrong_id',
link = 'http://youtu.be/' + videoId,
result = Utils.parseYoutubeLink(link);
expect(result).toBeUndefined();
});
var wrongUrls = [ var wrongUrls = [
'http://youtu.bee/' + videoId,
'http://youtu.be/', 'http://youtu.be/',
'example.com', '/static/example',
'http://google.com/somevideo.mp4' 'http://google.com/somevideo.mp4'
]; ];
...@@ -163,10 +172,20 @@ describe('Transcripts.Utils', function () { ...@@ -163,10 +172,20 @@ describe('Transcripts.Utils', function () {
}); });
}); });
}); });
$.each(otherLinksList, function (index, link) {
it(link, function () {
var result = Utils.parseHTML5Link(link);
expect(result).toEqual({
video: otherLinkId,
type: 'other'
});
});
});
}); });
describe('Wrong arguments ', function () { describe('Wrong arguments ', function () {
beforeEach(function(){ beforeEach(function(){
spyOn(console, 'log'); spyOn(console, 'log');
}); });
...@@ -184,15 +203,11 @@ describe('Transcripts.Utils', function () { ...@@ -184,15 +203,11 @@ describe('Transcripts.Utils', function () {
}); });
var html5WrongUrls = [ var html5WrongUrls = [
'http://youtu.bee/' + videoId,
'http://youtu.be/', 'http://youtu.be/',
'example.com', 'http://example.com/.mp4',
'http://google.com/somevideo.mp1', 'http://example.com/video_name.',
'http://google.com/somevideomp4', 'http://example.com/',
'http://google.com/somevideo_mp4', 'http://example.com'
'http://google.com/somevideo:mp4',
'http://google.com/somevideo',
'http://google.com/somevideo.webm_'
]; ];
$.each(html5WrongUrls, function (index, link) { $.each(html5WrongUrls, function (index, link) {
...@@ -248,6 +263,13 @@ describe('Transcripts.Utils', function () { ...@@ -248,6 +263,13 @@ describe('Transcripts.Utils', function () {
}); });
describe('Wrong arguments ', function () { describe('Wrong arguments ', function () {
it('youtube videoId is wrong', function () {
var videoId = 'wrong_id',
link = 'http://youtu.be/' + videoId,
result = Utils.parseLink(link);
expect(result).toEqual({ mode : 'incorrect' });
});
it('no arguments', function () { it('no arguments', function () {
var result = Utils.parseLink(); var result = Utils.parseLink();
......
define( define(
[ [
"jquery", "underscore", 'jquery', 'underscore',
"js/views/video/transcripts/utils", "js/views/video/transcripts/metadata_videolist", 'js/views/video/transcripts/utils',
"js/views/metadata", "js/models/metadata", "js/views/abstract_editor", 'js/views/video/transcripts/metadata_videolist', 'js/models/metadata',
"sinon", "xmodule", "jasmine-jquery" 'js/views/abstract_editor',
'sinon', 'xmodule', 'jasmine-jquery'
], ],
function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, sinon) { function ($, _, Utils, VideoList, MetadataModel, AbstractEditor, sinon) {
'use strict';
describe('CMS.Views.Metadata.VideoList', function () { describe('CMS.Views.Metadata.VideoList', function () {
var videoListEntryTemplate = readFixtures( var videoListEntryTemplate = readFixtures(
'video/transcripts/metadata-videolist-entry.underscore' 'video/transcripts/metadata-videolist-entry.underscore'
...@@ -14,19 +16,19 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -14,19 +16,19 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
component_locator = 'component_locator', component_locator = 'component_locator',
videoList = [ videoList = [
{ {
mode: "youtube", mode: 'youtube',
type: "youtube", type: 'youtube',
video: "12345678901" video: '12345678901'
}, },
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
}, },
{ {
mode: "html5", mode: 'html5',
type: "webm", type: 'webm',
video: "video" video: 'video'
} }
], ],
modelStub = { modelStub = {
...@@ -54,7 +56,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -54,7 +56,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
sinonXhr = sinon.fakeServer.create(); sinonXhr = sinon.fakeServer.create();
sinonXhr.respondWith([ sinonXhr.respondWith([
200, 200,
{ "Content-Type": "application/json"}, { 'Content-Type': 'application/json'},
response response
]); ]);
...@@ -65,15 +67,15 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -65,15 +67,15 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
'data-locator': component_locator 'data-locator': component_locator
}), }),
model = new MetadataModel(modelStub), model = new MetadataModel(modelStub),
videoList, $el; $el;
setFixtures(tpl); setFixtures(tpl);
appendSetFixtures( appendSetFixtures(
$("<script>", $('<script>',
{ {
id: "metadata-videolist-entry", id: 'metadata-videolist-entry',
type: "text/template" type: 'text/template'
} }
).text(videoListEntryTemplate) ).text(videoListEntryTemplate)
); );
...@@ -147,7 +149,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -147,7 +149,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
} }
return flag; return flag;
}, "Ajax Timeout", 750); }, 'Ajax Timeout', 750);
runs(expectFunc); runs(expectFunc);
}; };
...@@ -189,21 +191,21 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -189,21 +191,21 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
it('is rendered with opened extra videos bar', function () { it('is rendered with opened extra videos bar', function () {
var videoListLength = [ var videoListLength = [
{ {
mode: "youtube", mode: 'youtube',
type: "youtube", type: 'youtube',
video: "12345678901" video: '12345678901'
}, },
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
} }
], ],
videoListHtml5mode = [ videoListHtml5mode = [
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
} }
]; ];
...@@ -240,9 +242,9 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -240,9 +242,9 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
it('is rendered without opened extra videos bar', function () { it('is rendered without opened extra videos bar', function () {
var videoList = [ var videoList = [
{ {
mode: "youtube", mode: 'youtube',
type: "youtube", type: 'youtube',
video: "12345678901" video: '12345678901'
} }
]; ];
...@@ -263,6 +265,59 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -263,6 +265,59 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
}); });
describe('isUniqOtherVideos', function () {
it('Unique data - return true', function () {
var data = videoList.concat([{
mode: 'html5',
type: 'other',
video: 'pxxZrg'
}]);
waitsForResponse(function () {
var result = view.isUniqOtherVideos(data);
expect(result).toBe(true);
});
});
it('Not Unique data - return false', function () {
var data = [
{
mode: 'html5',
type: 'mp4',
video: 'video'
},
{
mode: 'html5',
type: 'mp4',
video: 'video'
},
{
mode: 'html5',
type: 'other',
video: 'pxxZrg'
},
{
mode: 'html5',
type: 'other',
video: 'pxxZrg'
},
{
mode: 'youtube',
type: 'youtube',
video: '12345678901'
}
];
waitsForResponse(function () {
var result = view.isUniqOtherVideos(data);
expect(result).toBe(false);
});
});
});
describe('isUniqVideoTypes', function () { describe('isUniqVideoTypes', function () {
it('Unique data - return true', function () { it('Unique data - return true', function () {
...@@ -279,19 +334,24 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -279,19 +334,24 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
it('Not Unique data - return false', function () { it('Not Unique data - return false', function () {
var data = [ var data = [
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
}, },
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
}, },
{ {
mode: "youtube", mode: 'html5',
type: "youtube", type: 'other',
video: "12345678901" video: 'pxxZrg'
},
{
mode: 'youtube',
type: 'youtube',
video: '12345678901'
} }
]; ];
...@@ -304,23 +364,27 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -304,23 +364,27 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
}); });
describe('checkIsUniqVideoTypes', function () { describe('checkIsUniqVideoTypes', function () {
it('Error is shown', function () { it('Error is shown', function () {
var data = [ var data = [
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
}, },
{ {
mode: "html5", mode: 'html5',
type: "mp4", type: 'mp4',
video: "video" video: 'video'
}, },
{ {
mode: "youtube", mode: 'html5',
type: "youtube", type: 'other',
video: "12345678901" video: 'pxxZrg'
},
{
mode: 'youtube',
type: 'youtube',
video: '12345678901'
} }
]; ];
...@@ -350,7 +414,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -350,7 +414,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
spyOn(view, 'checkIsUniqVideoTypes').andReturn(true); spyOn(view, 'checkIsUniqVideoTypes').andReturn(true);
}); });
it('Error message are shown', function () { it('Error message is shown', function () {
waitsForResponse(function () { waitsForResponse(function () {
var data = { mode: 'incorrect' }, var data = { mode: 'incorrect' },
result = view.checkValidity(data, true); result = view.checkValidity(data, true);
...@@ -361,7 +425,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -361,7 +425,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
}); });
}); });
it('Error message are shown when flag is not passed', function () { it('Error message is shown when flag is not passed', function () {
waitsForResponse(function () { waitsForResponse(function () {
var data = { mode: 'incorrect' }, var data = { mode: 'incorrect' },
result = view.checkValidity(data); result = view.checkValidity(data);
...@@ -435,6 +499,11 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -435,6 +499,11 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
mode: 'html5', mode: 'html5',
type: 'mp4', type: 'mp4',
video: 'video' video: 'video'
},
{
mode: 'html5',
type: 'other',
video: 'pxxZrg'
} }
]; ];
...@@ -442,6 +511,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -442,6 +511,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
view.setValueInEditor([ view.setValueInEditor([
'http://youtu.be/12345678901', 'http://youtu.be/12345678901',
'video.mp4', 'video.mp4',
'http://goo.gl/pxxZrg',
'video' 'video'
]); ]);
expect(view).assertIsCorrectVideoList(value); expect(view).assertIsCorrectVideoList(value);
...@@ -540,13 +610,17 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -540,13 +610,17 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
expect(messenger.hideError).not.toHaveBeenCalled(); expect(messenger.hideError).not.toHaveBeenCalled();
expect(view.updateModel).not.toHaveBeenCalled(); expect(view.updateModel).not.toHaveBeenCalled();
expect(view.closeExtraVideosBar).not.toHaveBeenCalled(); expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
expect($.fn.prop).toHaveBeenCalledWith('disabled', true); expect($.fn.prop).toHaveBeenCalledWith(
expect($.fn.addClass).toHaveBeenCalledWith('is-disabled'); 'disabled', true
);
expect($.fn.addClass).toHaveBeenCalledWith(
'is-disabled'
);
}); });
} }
); );
it('Main field has invalid value - extra Videos Bar should be closed', it('Main field has invalid value - extra Videos Bar is closed',
function () { function () {
$.fn.hasClass.andReturn(true); $.fn.hasClass.andReturn(true);
view.checkValidity.andReturn(false); view.checkValidity.andReturn(false);
...@@ -556,8 +630,12 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -556,8 +630,12 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
expect(messenger.hideError).not.toHaveBeenCalled(); expect(messenger.hideError).not.toHaveBeenCalled();
expect(view.updateModel).not.toHaveBeenCalled(); expect(view.updateModel).not.toHaveBeenCalled();
expect(view.closeExtraVideosBar).toHaveBeenCalled(); expect(view.closeExtraVideosBar).toHaveBeenCalled();
expect($.fn.prop).toHaveBeenCalledWith('disabled', true); expect($.fn.prop).toHaveBeenCalledWith(
expect($.fn.addClass).toHaveBeenCalledWith('is-disabled'); 'disabled', true
);
expect($.fn.addClass).toHaveBeenCalledWith(
'is-disabled'
);
}); });
} }
); );
...@@ -572,8 +650,12 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -572,8 +650,12 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
expect(messenger.hideError).not.toHaveBeenCalled(); expect(messenger.hideError).not.toHaveBeenCalled();
expect(view.updateModel).toHaveBeenCalled(); expect(view.updateModel).toHaveBeenCalled();
expect(view.closeExtraVideosBar).not.toHaveBeenCalled(); expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
expect($.fn.prop).toHaveBeenCalledWith('disabled', false); expect($.fn.prop).toHaveBeenCalledWith(
expect($.fn.removeClass).toHaveBeenCalledWith('is-disabled'); 'disabled', false
);
expect($.fn.removeClass).toHaveBeenCalledWith(
'is-disabled'
);
}); });
} }
); );
...@@ -588,8 +670,12 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s ...@@ -588,8 +670,12 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
expect(messenger.hideError).toHaveBeenCalled(); expect(messenger.hideError).toHaveBeenCalled();
expect(view.updateModel).not.toHaveBeenCalled(); expect(view.updateModel).not.toHaveBeenCalled();
expect(view.closeExtraVideosBar).not.toHaveBeenCalled(); expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
expect($.fn.prop).toHaveBeenCalledWith('disabled', false); expect($.fn.prop).toHaveBeenCalledWith(
expect($.fn.removeClass).toHaveBeenCalledWith('is-disabled'); 'disabled', false
);
expect($.fn.removeClass).toHaveBeenCalledWith(
'is-disabled'
);
}); });
} }
); );
......
define( define(
[ [
"jquery", "backbone", "underscore", "js/views/abstract_editor", 'jquery', 'backbone', 'underscore', 'js/views/abstract_editor',
"js/views/video/transcripts/utils", "js/views/video/transcripts/message_manager", 'js/views/video/transcripts/utils',
"js/views/metadata" 'js/views/video/transcripts/message_manager'
], ],
function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { function($, Backbone, _, AbstractEditor, Utils, MessageManager) {
'use strict';
var VideoList = AbstractEditor.extend({ var VideoList = AbstractEditor.extend({
// Time that we wait since the last time user typed. // Time that we wait since the last time user typed.
inputDelay: 300, inputDelay: 300,
...@@ -27,10 +28,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -27,10 +28,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
initialize: function () { initialize: function () {
// Initialize MessageManager that is responsible for // Initialize MessageManager that is responsible for
// status messages and errors. // status messages and errors.
var Messenger = this.options.MessageManager || MessageManager;
this.messenger = new Messenger({
var messenger = this.options.MessageManager || MessageManager;
this.messenger = new messenger({
el: this.$el, el: this.$el,
parent: this parent: this
}); });
...@@ -46,7 +46,8 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -46,7 +46,8 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
_.debounce(_.bind(this.inputHandler, this), this.inputDelay) _.debounce(_.bind(this.inputHandler, this), this.inputDelay)
); );
this.component_locator = this.$el.closest('[data-locator]').data('locator'); this.component_locator = this.$el.closest('[data-locator]')
.data('locator');
}, },
render: function () { render: function () {
...@@ -55,11 +56,14 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -55,11 +56,14 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
.apply(this, arguments); .apply(this, arguments);
var self = this, var self = this,
component_locator = this.$el.closest('[data-locator]').data('locator'), component_locator = this.$el.closest('[data-locator]')
.data('locator'),
videoList = this.getVideoObjectsList(), videoList = this.getVideoObjectsList(),
showServerError = function (response) { showServerError = function (response) {
var errorMessage = response.status || 'Error: Connection with server failed.'; var errorMessage = response.status ||
'Error: Connection with server failed.';
self.messenger self.messenger
.render('not_found') .render('not_found')
.showError( .showError(
...@@ -105,13 +109,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -105,13 +109,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Returns the values currently displayed in the editor/view.
* * @return {Array} List of non-empty values.
* Returns the values currently displayed in the editor/view. */
*
* @returns {array} List of non-empty values.
*
*/
getValueFromEditor: function () { getValueFromEditor: function () {
return _.map( return _.map(
this.$el.find('.input'), this.$el.find('.input'),
...@@ -122,29 +122,25 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -122,29 +122,25 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Returns list of objects with information about the values currently
* * displayed in the editor/view.
* Returns list of objects with information about the values currently * @return {Array} List of objects.
* displayed in the editor/view. * @examples
* * this.getValueFromEditor(); // =>
* @returns {array} List of objects. * [
* * 'http://youtu.be/OEoXaMPEzfM',
* @examples * 'video_name.mp4',
* this.getValueFromEditor(); // => * 'video_name.webm'
* [ * ]
* 'http://youtu.be/OEoXaMPEzfM', *
* 'video_name.mp4', * this.getVideoObjectsList(); // =>
* 'video_name.webm' * [
* ] * {mode: `youtube`, type: `youtube`, ...},
* * {mode: `html5`, type: `mp4`, ...},
* this.getVideoObjectsList(); // => * {mode: `html5`, type: `webm`, ...}
* [ * ]
* {mode: `youtube`, type: `youtube`, ...}, *
* {mode: `html5`, type: `mp4`, ...}, */
* {mode: `html5`, type: `webm`, ...}
* ]
*
*/
getVideoObjectsList: function () { getVideoObjectsList: function () {
var links = this.getValueFromEditor(); var links = this.getValueFromEditor();
...@@ -152,16 +148,11 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -152,16 +148,11 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Sets the values currently displayed in the editor/view.
* * @param {Array} value List of values.
* Sets the values currently displayed in the editor/view. */
*
* @params {array} value List of values.
*
*/
setValueInEditor: function (value) { setValueInEditor: function (value) {
var parseLink = Utils.parseLink, var list = this.$el.find('.input'),
list = this.$el.find('.input'),
val = value.filter(_.identity), val = value.filter(_.identity),
placeholders = this.getPlaceholders(val); placeholders = this.getPlaceholders(val);
...@@ -174,19 +165,15 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -174,19 +165,15 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
/** /**
* @function * Returns the placeholders for the values currently displayed in the
* * editor/view.
* Returns the placeholders for the values currently displayed in the * @return {Array} List of placeholders.
* editor/view. */
*
* @returns {array} List of placeholders.
*
*/
getPlaceholders: function (value) { getPlaceholders: function (value) {
var parseLink = Utils.parseLink, var parseLink = Utils.parseLink,
placeholders = _.clone(this.placeholders); placeholders = _.clone(this.placeholders);
// Returned list should have the same size as a count of editors/views. // Returned list should have the same size as a count of editors.
return _.map( return _.map(
this.$el.find('.input'), this.$el.find('.input'),
function (element, index) { function (element, index) {
...@@ -214,13 +201,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -214,13 +201,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Opens video sources box.
* * @param {Object} event Event object.
* Opens video sources box. */
*
* @params {object} event Event object.
*
*/
openExtraVideosBar: function (event) { openExtraVideosBar: function (event) {
if (event && event.preventDefault) { if (event && event.preventDefault) {
event.preventDefault(); event.preventDefault();
...@@ -230,13 +213,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -230,13 +213,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Closes video sources box.
* * @param {Object} event Event object.
* Closes video sources box. */
*
* @params {object} event Event object.
*
*/
closeExtraVideosBar: function (event) { closeExtraVideosBar: function (event) {
if (event && event.preventDefault) { if (event && event.preventDefault) {
event.preventDefault(); event.preventDefault();
...@@ -246,13 +225,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -246,13 +225,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Toggles video sources box.
* * @param {Object} event Event object.
* Toggles video sources box. */
*
* @params {object} event Event object.
*
*/
toggleExtraVideosBar: function (event) { toggleExtraVideosBar: function (event) {
if (event && event.preventDefault) { if (event && event.preventDefault) {
event.preventDefault(); event.preventDefault();
...@@ -266,13 +241,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -266,13 +241,9 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Handle `input` event.
* * @param {Object} event Event object.
* Handle `input` event. */
*
* @params {object} event Event object.
*
*/
inputHandler: function (event) { inputHandler: function (event) {
if (event && event.preventDefault) { if (event && event.preventDefault) {
event.preventDefault(); event.preventDefault();
...@@ -329,77 +300,108 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { ...@@ -329,77 +300,108 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}, },
/** /**
* @function * Checks the values currently displayed in the editor/view have unique
* * types (mp4 | webm | youtube).
* Checks the values currently displayed in the editor/view have unique * @param {Object} videoList List of objects with information about the
* types (mp4 | webm | youtube). * @return {Boolean} Boolean value that indicate if video types are
* * unique.
* @param {object} videoList List of objects with information about the */
* values currently displayed in the editor/view
*
* @returns {boolean} Boolean value that indicate if video types are unique.
*
*/
isUniqVideoTypes: function (videoList) { isUniqVideoTypes: function (videoList) {
// Extract a list of "type" property values. // Extract a list of "type" property values.
var arr = _.pluck(videoList, 'type'), // => ex: ['youtube', 'mp4', 'mp4'] // => ex: ['webm', 'mp4', 'mp4']
// Produces a duplicate-free version of the array. var arr = _.pluck(videoList, 'type').filter(function (item) {
uniqArr = _.uniq(arr); // => ex: ['youtube', 'mp4'] return item !== 'other';
}),
// Produces a duplicate-free version of the array.
uniqArr = _.uniq(arr); // => ex: ['webm', 'mp4']
return arr.length === uniqArr.length; return arr.length === uniqArr.length;
}, },
/** /**
* @function * Checks that links without file format are unique.
* * @param {Object} videoList List of objects with information about the
* Shows error message if the values currently displayed in the * @return {Boolean} Boolean value that indicate if video types are
* editor/view have duplicate types. * unique.
* */
* @param {object} list List of objects with information about the isUniqOtherVideos: function (videoList) {
* values currently displayed in the editor/view // Returns list of video objects with "type" equal "other" or
* // "youtube".
* @returns {boolean} Boolean value that indicate if video types are unique. var otherLinksList = videoList.filter(function (item) {
* return item.type === 'other';
*/ }),
checkIsUniqVideoTypes: function (list) { // Extract a list of "video" property values.
var videoList = list || this.getVideoObjectsList(), namesList = _.pluck(otherLinksList, 'video'),
isUnique = true; // Produces a duplicate-free version of the array.
uniqNamesList = _.uniq(namesList);
return namesList.length === uniqNamesList.length;
},
if (!this.isUniqVideoTypes(videoList)) { /**
this.messenger * Validates video list using provided validator.
.showError('Link types should be unique.', true); * @param {Function} validator Function that validate provided list.
* @param {Object} list List of objects with information about the
* values currently displayed in the editor.
* @param {String} message Error message.
* @return {Boolean}
*/
checkIsValid: function (validator, list, message) {
var videoList = list || this.getVideoObjectsList(),
isValid = true;
isUnique = false; if (!validator(videoList)) {
this.messenger.showError(message, true);
isValid = false;
} }
return isUnique; return isValid;
}, },
/** /**
* @function * Validates if video types are unique.
* * @param {Object} list List of objects with information about the
* Checks if the values currently displayed in the editor/view have * values currently displayed in the editor.
* valid values and show error messages. * @return {Boolean}
* */
* @param {object} data Objects with information about the value checkIsUniqVideoTypes: function (list) {
* currently displayed in the editor/view return this.checkIsValid(
* this.isUniqVideoTypes, list, 'Link types should be unique.'
* @param {boolean} showErrorModeMessage Disable mode validation );
* },
* @returns {boolean} Boolean value that indicate if value is valid.
* /**
*/ * Validates if other videos ids are unique.
* editor/view have duplicate types.
* @param {Object} list List of objects with information about the
* values currently displayed in the editor.
* @return {Boolean}
*/
checkIsUniqOtherVideos: function (list) {
return this.checkIsValid(
this.isUniqOtherVideos, list, 'Links should be unique.'
);
},
/**
* Checks if the values currently displayed in the editor/view have
* valid values and show error messages.
* @param {Object} data Objects with information about the value
* currently displayed in the editor/view
* @param {Boolean} showErrorModeMessage Disable mode validation
* @return {Boolean} Boolean value that indicate if value is valid.
*/
checkValidity: function (data, showErrorModeMessage) { checkValidity: function (data, showErrorModeMessage) {
var self = this, var videoList = this.getVideoObjectsList(),
videoList = this.getVideoObjectsList(); isUniqTypes = this.checkIsUniqVideoTypes.bind(this),
isUniqOtherVideos = this.checkIsUniqOtherVideos.bind(this);
if (!this.checkIsUniqVideoTypes(videoList)) { if (!isUniqTypes(videoList) || !isUniqOtherVideos(videoList)) {
return false; return false;
} }
if (data.mode === 'incorrect' && showErrorModeMessage) { if (data.mode === 'incorrect' && showErrorModeMessage) {
this.messenger this.messenger.showError('Incorrect url format.', true);
.showError('Incorrect url format.', true);
return false; return false;
} }
......
...@@ -4,53 +4,49 @@ return (function () { ...@@ -4,53 +4,49 @@ return (function () {
var Storage = {}; var Storage = {};
/** /**
* Adds some data to the Storage object. If data with existent `data_id` * Adds some data to the Storage object. If data with existent `data_id`
* is added, nothing happens. * is added, nothing happens.
* @function * @function
* @param {String} data_id Unique identifier for the data. * @param {String} data_id Unique identifier for the data.
* @param {Any} data Data that should be stored. * @param {Any} data Data that should be stored.
* @return {Object} Object itself for chaining. * @return {Object} Object itself for chaining.
*/ */
Storage.set = function (data_id, data) { Storage.set = function (data_id, data) {
Storage[data_id] = data; Storage[data_id] = data;
return this; return this;
}; };
/** /**
* Return data from the Storage object by identifier. * Return data from the Storage object by identifier.
* @function * @function
* @param {String} data_id Unique identifier of the data. * @param {String} data_id Unique identifier of the data.
* @return {Any} Stored data. * @return {Any} Stored data.
*/ */
Storage.get= function (data_id) { Storage.get= function (data_id) {
return Storage[data_id]; return Storage[data_id];
}; };
/** /**
* Deletes data from the Storage object by identifier. * Deletes data from the Storage object by identifier.
* @function * @function
* @param {String} data_id Unique identifier of the data. * @param {String} data_id Unique identifier of the data.
* @return {Boolean} Boolean value that indicate if data is removed. * @return {Boolean} Boolean value that indicate if data is removed.
*/ */
Storage.remove = function (data_id) { Storage.remove = function (data_id) {
return (delete Storage[data_id]); return (delete Storage[data_id]);
}; };
/** /**
* Returns model from collection by 'field_name' property. * Returns model from collection by 'field_name' property.
* @function * @function
* @param {Object} collection The model (CMS.Models.Metadata) information * @param {Object} collection The model (CMS.Models.Metadata) information
* about metadata setting editors. * about metadata setting editors.
* @param {String} field_name Name of field that should be found. * @param {String} field_name Name of field that should be found.
* @return { * @return {
* Object: When model exist. * Object: When model exist.
* Undefined: When model doesn't exist. * Undefined: When model doesn't exist.
* } * }
*/ */
var _getField = function (collection, field_name) { var _getField = function (collection, field_name) {
var model; var model;
...@@ -64,30 +60,29 @@ return (function () { ...@@ -64,30 +60,29 @@ return (function () {
}; };
/** /**
* Parses Youtube link and return video id. * Parses Youtube link and return video id.
* @function * @function
* These are the types of URLs supported: * These are the types of URLs supported:
* http://www.youtube.com/watch?v=OEoXaMPEzfM&feature=feedrec_grec_index * http://www.youtube.com/watch?v=OEoXaMPEzfM&feature=feedrec_grec_index
* http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/OEoXaMPEzfM * http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/OEoXaMPEzfM
* http://www.youtube.com/v/OEoXaMPEzfM?fs=1&amp;hl=en_US&amp;rel=0 * http://www.youtube.com/v/OEoXaMPEzfM?fs=1&amp;hl=en_US&amp;rel=0
* http://www.youtube.com/watch?v=OEoXaMPEzfM#t=0m10s * http://www.youtube.com/watch?v=OEoXaMPEzfM#t=0m10s
* http://www.youtube.com/embed/OEoXaMPEzfM?rel=0 * http://www.youtube.com/embed/OEoXaMPEzfM?rel=0
* http://www.youtube.com/watch?v=OEoXaMPEzfM * http://www.youtube.com/watch?v=OEoXaMPEzfM
* http://youtu.be/OEoXaMPEzfM * http://youtu.be/OEoXaMPEzfM
* @param {String} url Url that should be parsed. * @param {String} url Url that should be parsed.
* @return { * @return {
* String: Video Id. * String: Video Id.
* Undefined: When url has incorrect format or argument is * Undefined: When url has incorrect format or argument is
* non-string, video id's length is not equal 11. * non-string, video id's length is not equal 11.
* } * }
*/ */
var _youtubeParser = (function () { var _youtubeParser = (function () {
var cache = {}, var cache = {},
regExp = /(?:http|https|)(?:\:\/\/|)(?:www.|)(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/ytscreeningroom\?v=|\/feeds\/api\/videos\/|\/user\S*[^\w\-\s]|\S*[^\w\-\s]))([\w\-]{11})[a-z0-9;:@#?&%=+\/\$_.-]*/i; regExp = /(?:http|https|)(?:\:\/\/|)(?:www.|)(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/ytscreeningroom\?v=|\/feeds\/api\/videos\/|\/user\S*[^\w\-\s]|\S*[^\w\-\s]))([\w\-]+)/i;
return function (url) { return function (url) {
if (typeof url !== 'string') { if (typeof url !== 'string') {
return void(0); return void(0);
} }
...@@ -96,25 +91,23 @@ return (function () { ...@@ -96,25 +91,23 @@ return (function () {
} }
var match = url.match(regExp); var match = url.match(regExp);
cache[url] = (match && match[1].length === 11) ? cache[url] = (match) ? match[1] : void(0);
match[1] :
void(0);
return cache[url]; return cache[url];
}; };
}()); }());
/** /**
* Parses links with html5 video sources in mp4 or webm formats. * Parses links with html5 video sources in mp4 or webm formats.
* @function * @function
* @param {String} url Url that should be parsed. * @param {String} url Url that should be parsed.
* @return { * @return {
* object: Object with information about the video * Object: Object with information about the video
* (file name, video type), * (file name, video type),
* undefined: when url has incorrect format or argument is * Undefined: when url has incorrect format or argument is
* non-string. * non-string.
* } * }
*/ */
var _videoLinkParser = (function () { var _videoLinkParser = (function () {
var cache = {}; var cache = {};
...@@ -132,57 +125,65 @@ return (function () { ...@@ -132,57 +125,65 @@ return (function () {
match; match;
link.href = url; link.href = url;
match = link.pathname // The regular expression try catches file name and file extension.
.split('/') // '[scheme://hostname/pathname/]filename.extension[?query#hash]'
.pop() match = link.pathname.match(/\/{1}([^\/]+)\.([^\/]+)$/);
.match(/(.+)\.(mp?4v?|webm)$/);
if (match) { if (match) {
cache[url] = { cache[url] = {
video: match[1], video: match[1],
type: match[2] type: match[2]
}; };
} /*else { } else {
cache[url] = { // Links like http://goo.gl/pxxZrg
video: link.pathname // The regular expression try catches file name.
.split('/') // '[scheme://hostname/pathname/]filename[?query#hash]'
.pop(), match = link.pathname.match(/\/{1}([^\/\.]+)$/);
type: 'other' if (match) {
}; cache[url] = {
}*/ video: match[1],
type: 'other'
};
}
}
return cache[url]; return cache[url];
}; };
}()); }());
/** /**
* Facade function that parses html5 and youtube links. * Facade function that parses html5 and youtube links.
* @function * @function
* @param {String} url Url that should be parsed. * @param {String} url Url that should be parsed.
* @return { * @return {
* object: Object with information about the video: * object: Object with information about the video:
* { * {
* mode: "youtube|html5|incorrect", * mode: "youtube|html5|incorrect",
* video: "file_name|youtube_id", * video: "file_name|youtube_id",
* type: "youtube|mp4|webm" * type: "youtube|mp4|webm|other"
* }, * },
* undefined: when argument is non-string. * undefined: when argument is non-string.
* } * }
*/ */
var _linkParser = function (url) { var _linkParser = function (url) {
var result; var youtubeIdLength = 11,
result;
if (typeof url !== 'string') { if (typeof url !== 'string') {
return void(0); return void(0);
} }
if (_youtubeParser(url)) { if (_youtubeParser(url)) {
result = { if (_youtubeParser(url).length === youtubeIdLength) {
mode: 'youtube', result = {
video: _youtubeParser(url), mode: 'youtube',
type: 'youtube' video: _youtubeParser(url),
}; type: 'youtube'
};
} else {
result = {
mode: 'incorrect'
};
}
} else if (_videoLinkParser(url)) { } else if (_videoLinkParser(url)) {
result = $.extend({mode: 'html5'}, _videoLinkParser(url)); result = $.extend({mode: 'html5'}, _videoLinkParser(url));
} else { } else {
...@@ -195,37 +196,36 @@ return (function () { ...@@ -195,37 +196,36 @@ return (function () {
}; };
/** /**
* Returns short-hand youtube url. * Returns short-hand youtube url.
* @function * @function
* @param {string} video_id Youtube Video Id that will be added to the * @param {String} video_id Youtube Video Id that will be added to the link.
* link. * @return {String} Short-hand Youtube url.
* @return {string} Short-hand Youtube url. * @examples
* @examples * _getYoutubeLink('OEoXaMPEzfM'); => 'http://youtu.be/OEoXaMPEzfM'
* _getYoutubeLink('OEoXaMPEzfM'); => 'http://youtu.be/OEoXaMPEzfM' */
*/
var _getYoutubeLink = function (video_id) { var _getYoutubeLink = function (video_id) {
return 'http://youtu.be/' + video_id; return 'http://youtu.be/' + video_id;
}; };
/** /**
* Returns list of objects with information about the passed links. * Returns list of objects with information about the passed links.
* @function * @function
* @param {array} links List of links that will be processed. * @param {Array} links List of links that will be processed.
* @returns {array} List of objects. * @returns {Array} List of objects.
* @examples * @examples
* var links = [ * var links = [
* 'http://youtu.be/OEoXaMPEzfM', * 'http://youtu.be/OEoXaMPEzfM',
* 'video_name.mp4', * 'video_name.mp4',
* 'video_name.webm' * 'video_name.webm'
* ] * ]
* *
* _getVideoList(links); // => * _getVideoList(links); // =>
* [ * [
* {mode: `youtube`, type: `youtube`, ...}, * {mode: `youtube`, type: `youtube`, ...},
* {mode: `html5`, type: `mp4`, ...}, * {mode: `html5`, type: `mp4`, ...},
* {mode: `html5`, type: `webm`, ...} * {mode: `html5`, type: `webm`, ...}
* ] * ]
*/ */
var _getVideoList = function (links) { var _getVideoList = function (links) {
if ($.isArray(links)) { if ($.isArray(links)) {
var arr = [], var arr = [],
...@@ -243,14 +243,13 @@ return (function () { ...@@ -243,14 +243,13 @@ return (function () {
} }
}; };
/** /**
* Synchronizes 2 Backbone collections by 'field_name' property. * Synchronizes 2 Backbone collections by 'field_name' property.
* @function * @function
* @param {Object} fromCollection Collection with which synchronization will * @param {Object} fromCollection Collection with which synchronization will
* happens. * happens.
* @param {Object} toCollection Collection which will synchronized. * @param {Object} toCollection Collection which will synchronized.
*/ */
var _syncCollections = function (fromCollection, toCollection) { var _syncCollections = function (fromCollection, toCollection) {
fromCollection.each(function (m) { fromCollection.each(function (m) {
var model = toCollection.findWhere({ var model = toCollection.findWhere({
...@@ -264,18 +263,18 @@ return (function () { ...@@ -264,18 +263,18 @@ return (function () {
}; };
/** /**
* Sends Ajax requests in appropriate format. * Sends Ajax requests in appropriate format.
* @function * @function
* @param {String} action Action that will be invoked on server. * @param {String} action Action that will be invoked on server.
* @param {String} component_locator the locator of component. * @param {String} component_locator the locator of component.
* @param {Array} videoList List of object with information about inserted * @param {Array} videoList List of object with information about inserted
* urls. * urls.
* @param {Object} extraParams Extra parameters that can be send to the * @param {Object} extraParams Extra parameters that can be send to the
* server. * server.
* @return {Object} XMLHttpRequest object. Using this object, we can * @return {Object} XMLHttpRequest object. Using this object, we can
* attach callbacks to AJAX request events (for example on 'done', * attach callbacks to AJAX request events (for example on 'done',
* 'fail', etc.). * 'fail', etc.).
*/ */
var _command = (function () { var _command = (function () {
// We will store the XMLHttpRequest object that $.ajax() function // We will store the XMLHttpRequest object that $.ajax() function
// returns, to abort an ongoing AJAX request (if necessary) upon // returns, to abort an ongoing AJAX request (if necessary) upon
......
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
data-transcript-translation-url="/transcript/translation" data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations" data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y" data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4" data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api" data-yt-api-url="www.youtube.com/iframe_api"
......
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
data-transcript-translation-url="/transcript/translation" data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations" data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y" data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4" data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api" data-yt-api-url="www.youtube.com/iframe_api"
......
...@@ -79,53 +79,6 @@ ...@@ -79,53 +79,6 @@
expect(state.videos).toBeUndefined(); expect(state.videos).toBeUndefined();
}); });
it('parse Html5 sources', function () {
var html5Sources = {
mp4: null,
webm: null,
ogg: null
}, v = document.createElement('video');
if (
!!(
v.canPlayType &&
v.canPlayType(
'video/webm; codecs="vp8, vorbis"'
).replace(/no/, '')
)
) {
html5Sources['webm'] =
'xmodule/include/fixtures/test.webm';
}
if (
!!(
v.canPlayType &&
v.canPlayType(
'video/mp4; codecs="avc1.42E01E, ' +
'mp4a.40.2"'
).replace(/no/, '')
)
) {
html5Sources['mp4'] =
'xmodule/include/fixtures/test.mp4';
}
if (
!!(
v.canPlayType &&
v.canPlayType(
'video/ogg; codecs="theora"'
).replace(/no/, '')
)
) {
html5Sources['ogg'] =
'xmodule/include/fixtures/test.ogv';
}
expect(state.html5Sources).toEqual(html5Sources);
});
it('parse available video speeds', function () { it('parse available video speeds', function () {
var speeds = jasmine.stubbedHtml5Speeds; var speeds = jasmine.stubbedHtml5Speeds;
......
...@@ -70,7 +70,6 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -70,7 +70,6 @@ function (VideoPlayer, VideoStorage, i18n) {
isFlashMode: isFlashMode, isFlashMode: isFlashMode,
isYoutubeType: isYoutubeType, isYoutubeType: isYoutubeType,
parseSpeed: parseSpeed, parseSpeed: parseSpeed,
parseVideoSources: parseVideoSources,
parseYoutubeStreams: parseYoutubeStreams, parseYoutubeStreams: parseYoutubeStreams,
saveState: saveState, saveState: saveState,
setPlayerMode: setPlayerMode, setPlayerMode: setPlayerMode,
...@@ -280,32 +279,17 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -280,32 +279,17 @@ function (VideoPlayer, VideoStorage, i18n) {
// The function prepare HTML5 video, parse HTML5 // The function prepare HTML5 video, parse HTML5
// video sources etc. // video sources etc.
function _prepareHTML5Video(state) { function _prepareHTML5Video(state) {
state.parseVideoSources(
{
mp4: state.config.mp4Source,
webm: state.config.webmSource,
ogg: state.config.oggSource
}
);
state.speeds = ['0.75', '1.0', '1.25', '1.50']; state.speeds = ['0.75', '1.0', '1.25', '1.50'];
// If none of the supported video formats can be played and there is no
// We must have at least one non-YouTube video source available. // short-hand video links, than hide the spinner and show error message.
// Otherwise, return a negative. if (!state.config.sources.length) {
if (
state.html5Sources.webm === null &&
state.html5Sources.mp4 === null &&
state.html5Sources.ogg === null
) {
// TODO: use 1 class to work with.
state.el.find('.video-player div').addClass('hidden');
state.el.find('.video-player h3').removeClass('hidden');
_hideWaitPlaceholder(state); _hideWaitPlaceholder(state);
state.el
console.log( .find('.video-player div')
'[Video info]: Non-youtube video sources aren\'t available.' .addClass('hidden')
); .end()
.find('.video-player h3')
.removeClass('hidden');
return false; return false;
} }
...@@ -642,48 +626,6 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -642,48 +626,6 @@ function (VideoPlayer, VideoStorage, i18n) {
return _.isString(this.videos['1.0']); return _.isString(this.videos['1.0']);
} }
// function parseVideoSources(, mp4Source, webmSource, oggSource)
//
// Take the HTML5 sources (URLs of videos), and make them available
// explictly for each type of video format (mp4, webm, ogg).
function parseVideoSources(sources) {
var _this = this,
v = document.createElement('video'),
sourceCodecs = {
mp4: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
webm: 'video/webm; codecs="vp8, vorbis"',
ogg: 'video/ogg; codecs="theora"'
};
this.html5Sources = {
mp4: null,
webm: null,
ogg: null
};
$.each(sources, function (name, source) {
if (source && source.length) {
if (
Boolean(
v.canPlayType &&
v.canPlayType(sourceCodecs[name]).replace(/no/, '')
)
) {
_this.html5Sources[name] = source;
}
}
});
// None of the supported video formats can be played. Hide the spinner.
if (!(_.compact(_.values(this.html5Sources)))) {
_hideWaitPlaceholder(state);
console.log(
'[Video info]: This browser cannot play .mp4, .ogg, or .webm ' +
'files'
);
}
}
// function fetchMetadata() // function fetchMetadata()
// //
// When dealing with YouTube videos, we must fetch meta data that has // When dealing with YouTube videos, we must fetch meta data that has
...@@ -763,7 +705,10 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -763,7 +705,10 @@ function (VideoPlayer, VideoStorage, i18n) {
} }
successHandler = ($.isFunction(callback)) ? callback : null; successHandler = ($.isFunction(callback)) ? callback : null;
xhr = $.ajax({ xhr = $.ajax({
url: document.location.protocol + '//' + this.config.ytTestUrl + url + '?v=2&alt=jsonc', url: [
document.location.protocol, '//', this.config.ytTestUrl, url,
'?v=2&alt=jsonc'
].join(''),
dataType: 'jsonp', dataType: 'jsonp',
timeout: this.config.ytTestTimeout, timeout: this.config.ytTestTimeout,
success: successHandler success: successHandler
......
...@@ -94,6 +94,22 @@ function () { ...@@ -94,6 +94,22 @@ function () {
return this.logs; return this.logs;
}; };
Player.prototype.showErrorMessage = function () {
this.el
.find('.video-player div')
.addClass('hidden')
.end()
.find('.video-player h3')
.removeClass('hidden')
.end()
.addClass('is-initialized')
.find('.spinner')
.attr({
'aria-hidden': 'true',
'tabindex': -1
});
};
return Player; return Player;
/* /*
...@@ -113,7 +129,7 @@ function () { ...@@ -113,7 +129,7 @@ function () {
* *
* config = { * config = {
* *
* videoSources: {}, // An object with properties being video * videoSources: [], // An array with properties being video
* // sources. The property name is the * // sources. The property name is the
* // video format of the source. Supported * // video format of the source. Supported
* // video formats are: 'mp4', 'webm', and * // video formats are: 'mp4', 'webm', and
...@@ -134,7 +150,7 @@ function () { ...@@ -134,7 +150,7 @@ function () {
*/ */
function Player(el, config) { function Player(el, config) {
var isTouch = onTouchBasedDevice() || '', var isTouch = onTouchBasedDevice() || '',
sourceStr, _this, errorMessage; sourceList, _this, errorMessage, lastSource;
this.logs = []; this.logs = [];
// Initially we assume that el is a DOM element. If jQuery selector // Initially we assume that el is a DOM element. If jQuery selector
...@@ -167,63 +183,50 @@ function () { ...@@ -167,63 +183,50 @@ function () {
// We should have at least one video source. Otherwise there is no // We should have at least one video source. Otherwise there is no
// point to continue. // point to continue.
if (!config.videoSources) { if (!config.videoSources && !config.videoSources.length) {
return; return;
} }
// From the start, all sources are empty. We will populate this
// object below.
sourceStr = {
mp4: ' ',
webm: ' ',
ogg: ' '
};
// Will be used in inner functions to point to the current object. // Will be used in inner functions to point to the current object.
_this = this; _this = this;
// Create HTML markup for individual sources of the HTML5 <video> // Create HTML markup for individual sources of the HTML5 <video>
// element. // element.
$.each(sourceStr, function (videoType, videoSource) { sourceList = $.map(config.videoSources, function (source) {
var url = _this.config.videoSources[videoType]; return [
if (url && url.length) { '<source ',
sourceStr[videoType] = 'src="', source,
'<source ' + // Following hack allows to open the same video twice
'src="' + url + // https://code.google.com/p/chromium/issues/detail?id=31014
// Following hack allows to open the same video twice // Check whether the url already has a '?' inside, and if so,
// https://code.google.com/p/chromium/issues/detail?id=31014 // use '&' instead of '?' to prevent breaking the url's integrity.
// Check whether the url already has a '?' inside, and if so, (source.indexOf('?') === -1 ? '?' : '&'),
// use '&' instead of '?' to prevent breaking the url's integrity. (new Date()).getTime(), '" />'
(url.indexOf('?') == -1 ? '?' : '&') + (new Date()).getTime() + ].join('');
'" ' + 'type="video/' + videoType + '" ' +
'/> ';
}
}); });
// We should have at least one video source. Otherwise there is no
// point to continue.
if (
sourceStr.mp4 === ' ' &&
sourceStr.webm === ' ' &&
sourceStr.ogg === ' '
) {
return;
}
// Create HTML markup for the <video> element, populating it with // Create HTML markup for the <video> element, populating it with
// sources from previous step. Because of problems with creating // sources from previous step. Because of problems with creating
// video element via jquery (http://bugs.jquery.com/ticket/9174) we // video element via jquery (http://bugs.jquery.com/ticket/9174) we
// create it using native JS. // create it using native JS.
this.video = document.createElement('video'); this.video = document.createElement('video');
errorMessage = gettext('This browser cannot play .mp4, .ogg, or .webm files.')
+ gettext('Try using a different browser, such as Google Chrome.'); errorMessage = [
this.video.innerHTML = _.values(sourceStr).join('') + errorMessage; gettext('This browser cannot play .mp4, .ogg, or .webm files.'),
gettext('Try using a different browser, such as Google Chrome.')
].join('');
this.video.innerHTML = sourceList.join('') + errorMessage;
// Get the jQuery object, and set the player state to UNSTARTED. // Get the jQuery object, and set the player state to UNSTARTED.
// The player state is used by other parts of the VideoPlayer to // The player state is used by other parts of the VideoPlayer to
// determine what the video is currently doing. // determine what the video is currently doing.
this.videoEl = $(this.video); this.videoEl = $(this.video);
lastSource = this.videoEl.find('source').last();
lastSource.on('error', this.showErrorMessage.bind(this));
if (/iP(hone|od)/i.test(isTouch[0])) { if (/iP(hone|od)/i.test(isTouch[0])) {
this.videoEl.prop('controls', true); this.videoEl.prop('controls', true);
} }
...@@ -253,6 +256,7 @@ function () { ...@@ -253,6 +256,7 @@ function () {
'durationchange', 'volumechange' 'durationchange', 'volumechange'
]; ];
this.debug = false;
$.each(events, function(index, eventName) { $.each(events, function(index, eventName) {
_this.video.addEventListener(eventName, function () { _this.video.addEventListener(eventName, function () {
_this.logs.push({ _this.logs.push({
...@@ -260,6 +264,15 @@ function () { ...@@ -260,6 +264,15 @@ function () {
'state': _this.playerState 'state': _this.playerState
}); });
if (_this.debug) {
console.log(
'event name:', eventName,
'state:', _this.playerState,
'readyState:', _this.video.readyState,
'networkState:', _this.video.networkState
);
}
el.trigger('html5:' + eventName, arguments); el.trigger('html5:' + eventName, arguments);
}); });
}); });
......
...@@ -142,7 +142,7 @@ function (HTML5Video, Resizer) { ...@@ -142,7 +142,7 @@ function (HTML5Video, Resizer) {
if (state.videoType === 'html5') { if (state.videoType === 'html5') {
state.videoPlayer.player = new HTML5Video.Player(state.el, { state.videoPlayer.player = new HTML5Video.Player(state.el, {
playerVars: state.videoPlayer.playerVars, playerVars: state.videoPlayer.playerVars,
videoSources: state.html5Sources, videoSources: state.config.sources,
events: { events: {
onReady: state.videoPlayer.onReady, onReady: state.videoPlayer.onReady,
onStateChange: state.videoPlayer.onStateChange onStateChange: state.videoPlayer.onStateChange
......
...@@ -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 opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.video_module import VideoDescriptor, create_youtube_string, get_ext 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
...@@ -107,18 +107,6 @@ class VideoModuleTest(LogicTest): ...@@ -107,18 +107,6 @@ class VideoModuleTest(LogicTest):
'1.50': ''} '1.50': ''}
) )
def test_get_ext(self):
"""Test get the file's extension in a url without query string."""
filename_str = 'http://www.example.com/path/video.mp4'
output = get_ext(filename_str)
self.assertEqual(output, 'mp4')
def test_get_ext_with_query_string(self):
"""Test get the file's extension in a url with query string."""
filename_str = 'http://www.example.com/path/video.mp4?param1=1&p2=2'
output = get_ext(filename_str)
self.assertEqual(output, 'mp4')
class VideoDescriptorTest(unittest.TestCase): class VideoDescriptorTest(unittest.TestCase):
"""Test for VideoDescriptor""" """Test for VideoDescriptor"""
......
...@@ -35,14 +35,6 @@ from .video_utils import create_youtube_string ...@@ -35,14 +35,6 @@ from .video_utils import create_youtube_string
from .video_xfields import VideoFields from .video_xfields import VideoFields
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
from urlparse import urlparse
def get_ext(filename):
# Prevent incorrectly parsing urls like 'http://abc.com/path/video.mp4?xxxx'.
path = urlparse(filename).path
return path.rpartition('.')[-1]
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_ = lambda text: text _ = lambda text: text
...@@ -97,15 +89,15 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule): ...@@ -97,15 +89,15 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
def get_html(self): def get_html(self):
track_url = None track_url = None
download_video_link = None
transcript_download_format = self.transcript_download_format transcript_download_format = self.transcript_download_format
sources = filter(None, self.html5_sources)
sources = {get_ext(src): src for src in self.html5_sources}
if self.download_video: if self.download_video:
if self.source: if self.source:
sources['main'] = self.source download_video_link = self.source
elif self.html5_sources: elif self.html5_sources:
sources['main'] = self.html5_sources[0] download_video_link = self.html5_sources[0]
if self.download_track: if self.download_track:
if self.track: if self.track:
...@@ -149,7 +141,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule): ...@@ -149,7 +141,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
'handout': self.handout, 'handout': self.handout,
'id': self.location.html_id(), 'id': self.location.html_id(),
'show_captions': json.dumps(self.show_captions), 'show_captions': json.dumps(self.show_captions),
'sources': sources, 'download_video_link': download_video_link,
'sources': json.dumps(sources),
'speed': json.dumps(self.speed), 'speed': json.dumps(self.speed),
'general_speed': self.global_speed, 'general_speed': self.global_speed,
'saved_video_position': self.saved_video_position.total_seconds(), 'saved_video_position': self.saved_video_position.total_seconds(),
......
...@@ -26,12 +26,7 @@ class TestVideoYouTube(TestVideo): ...@@ -26,12 +26,7 @@ class TestVideoYouTube(TestVideo):
def test_video_constructor(self): def test_video_constructor(self):
"""Make sure that all parameters extracted correctly from xml""" """Make sure that all parameters extracted correctly from xml"""
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
sources = json.dumps([u'example.mp4', u'example.webm'])
sources = {
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
}
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
...@@ -42,6 +37,7 @@ class TestVideoYouTube(TestVideo): ...@@ -42,6 +37,7 @@ class TestVideoYouTube(TestVideo):
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'download_video_link': u'example.mp4',
'sources': sources, 'sources': sources,
'speed': 'null', 'speed': 'null',
'general_speed': 1.0, 'general_speed': 1.0,
...@@ -56,7 +52,7 @@ class TestVideoYouTube(TestVideo): ...@@ -56,7 +52,7 @@ class TestVideoYouTube(TestVideo):
'transcript_download_format': 'srt', 'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': u'en', 'transcript_language': u'en',
'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"Українська"})), 'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"Українська"})),
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation' self.item_descriptor, 'transcript', 'translation'
).rstrip('/?'), ).rstrip('/?'),
...@@ -93,13 +89,8 @@ class TestVideoNonYouTube(TestVideo): ...@@ -93,13 +89,8 @@ class TestVideoNonYouTube(TestVideo):
"""Make sure that if the 'youtube' attribute is omitted in XML, then """Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams. the template generates an empty string for the YouTube streams.
""" """
sources = {
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
}
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
...@@ -107,6 +98,7 @@ class TestVideoNonYouTube(TestVideo): ...@@ -107,6 +98,7 @@ class TestVideoNonYouTube(TestVideo):
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'display_name': u'A Name', 'display_name': u'A Name',
'download_video_link': u'example.mp4',
'end': 3610.0, 'end': 3610.0,
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
'sources': sources, 'sources': sources,
...@@ -148,7 +140,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -148,7 +140,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
METADATA = {} METADATA = {}
def setUp(self): def setUp(self):
self.setup_course(); self.setup_course()
def test_get_html_track(self): def test_get_html_track(self):
SOURCE_XML = """ SOURCE_XML = """
...@@ -201,19 +193,17 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -201,19 +193,17 @@ class TestGetHtmlMethod(BaseTestXmodule):
'transcripts': '<transcript language="uk" src="ukrainian.srt" />', 'transcripts': '<transcript language="uk" src="ukrainian.srt" />',
}, },
] ]
sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = { expected_context = {
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'display_name': u'A Name', 'display_name': u'A Name',
'download_video_link': u'example.mp4',
'end': 3610.0, 'end': 3610.0,
'id': None, 'id': None,
'sources': { 'sources': sources,
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm'
},
'start': 3603.0, 'start': 3603.0,
'saved_video_position': 0.0, 'saved_video_position': 0.0,
'sub': u'a_sub_file.srt.sjson', 'sub': u'a_sub_file.srt.sjson',
...@@ -284,9 +274,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -284,9 +274,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
<source src="example.webm"/> <source src="example.webm"/>
""", """,
'result': { 'result': {
'main': u'example_source.mp4', 'download_video_link': u'example_source.mp4',
u'mp4': u'example.mp4', 'sources': json.dumps([u'example.mp4', u'example.webm']),
u'webm': u'example.webm',
}, },
}, },
{ {
...@@ -297,9 +286,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -297,9 +286,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
<source src="example.webm"/> <source src="example.webm"/>
""", """,
'result': { 'result': {
'main': u'example.mp4', 'download_video_link': u'example.mp4',
u'mp4': u'example.mp4', 'sources': json.dumps([u'example.mp4', u'example.webm']),
u'webm': u'example.webm',
}, },
}, },
{ {
...@@ -318,20 +306,20 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -318,20 +306,20 @@ class TestGetHtmlMethod(BaseTestXmodule):
<source src="example.webm"/> <source src="example.webm"/>
""", """,
'result': { 'result': {
u'mp4': u'example.mp4', 'sources': json.dumps([u'example.mp4', u'example.webm']),
u'webm': u'example.webm',
}, },
}, },
] ]
expected_context = { initial_context = {
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'display_name': u'A Name', 'display_name': u'A Name',
'download_video_link': None,
'end': 3610.0, 'end': 3610.0,
'id': None, 'id': None,
'sources': None, 'sources': '[]',
'speed': 'null', 'speed': 'null',
'general_speed': 1.0, 'general_speed': 1.0,
'start': 3603.0, 'start': 3603.0,
...@@ -358,6 +346,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -358,6 +346,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.initialize_module(data=DATA) self.initialize_module(data=DATA)
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
expected_context = dict(initial_context)
expected_context.update({ expected_context.update({
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation' self.item_descriptor, 'transcript', 'translation'
...@@ -366,9 +355,9 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -366,9 +355,9 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.item_descriptor, 'transcript', 'available_translations' self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'), ).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'sources': data['result'],
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
}) })
expected_context.update(data['result'])
self.assertEqual( self.assertEqual(
context, context,
...@@ -385,7 +374,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -385,7 +374,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
METADATA = {} METADATA = {}
def setUp(self): def setUp(self):
self.setup_course(); self.setup_course()
def test_source_not_in_html5sources(self): def test_source_not_in_html5sources(self):
metadata = { metadata = {
......
...@@ -13,10 +13,7 @@ ...@@ -13,10 +13,7 @@
${'data-sub="{}"'.format(sub) if sub else ''} ${'data-sub="{}"'.format(sub) if sub else ''}
${'data-autoplay="{}"'.format(autoplay) if autoplay else ''} ${'data-autoplay="{}"'.format(autoplay) if autoplay else ''}
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''} data-sources='${sources}'
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
data-save-state-url="${ajax_url}" data-save-state-url="${ajax_url}"
data-caption-data-dir="${data_dir}" data-caption-data-dir="${data_dir}"
data-show-captions="${show_captions}" data-show-captions="${show_captions}"
...@@ -106,9 +103,9 @@ ...@@ -106,9 +103,9 @@
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
<ul class="wrapper-downloads"> <ul class="wrapper-downloads">
% if sources.get('main'): % if download_video_link:
<li class="video-sources video-download-button"> <li class="video-sources video-download-button">
${('<a href="%s">' + _('Download video') + '</a>') % sources.get('main')} ${('<a href="%s">' + _('Download video') + '</a>') % download_video_link}
</li> </li>
% endif % endif
% if track: % if track:
......
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