Commit 1d748386 by polesye

BLD-237: Persist speed preferences between videos.

parent 5bacfcc3
...@@ -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: Video player persist speed preferences between videos. BLD-237.
Blades: Change the download video field to a dropdown that will allow students Blades: Change the download video field to a dropdown that will allow students
to download the first source listed in the alternate sources. BLD-364. to download the first source listed in the alternate sources. BLD-364.
......
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
<div <div
id="video_id" id="video_id"
class="video closed" class="video closed"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
data-show-captions="true" data-show-captions="true"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
id="video_id" id="video_id"
class="video closed" class="video closed"
data-show-captions="true" data-show-captions="true"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
id="video_id" id="video_id"
class="video closed" class="video closed"
data-show-captions="true" data-show-captions="true"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
......
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
<div <div
id="video_id" id="video_id"
class="video closed" class="video closed"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
data-show-captions="false" data-show-captions="false"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
......
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
<div <div
id="video_id1" id="video_id1"
class="video closed" class="video closed"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
data-show-captions="true" data-show-captions="true"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
......
...@@ -113,6 +113,10 @@ ...@@ -113,6 +113,10 @@
id: 'cogebirgzzM', id: 'cogebirgzzM',
duration: 200 duration: 200
}, },
'abcdefghijkl': {
id: 'abcdefghijkl',
duration: 400
},
bogus: { bogus: {
duration: 100 duration: 100
} }
...@@ -189,6 +193,8 @@ ...@@ -189,6 +193,8 @@
settings.url.match(/.+\/problem_(check|reset|show|save)$/) settings.url.match(/.+\/problem_(check|reset|show|save)$/)
) { ) {
// Do nothing. // Do nothing.
} else if (settings.url == '/save_user_state') {
return {success: true};
} else { } else {
throw 'External request attempted for ' + throw 'External request attempted for ' +
settings.url + settings.url +
......
...@@ -32,7 +32,7 @@ function (CookieStorage) { ...@@ -32,7 +32,7 @@ function (CookieStorage) {
it('unload', function () { it('unload', function () {
var expected = JSON.stringify({ var expected = JSON.stringify({
storage: { storage: {
'item_2': { item_2: {
value: 'value_2', value: 'value_2',
session: false session: false
} }
...@@ -51,7 +51,7 @@ function (CookieStorage) { ...@@ -51,7 +51,7 @@ function (CookieStorage) {
describe('methods: ', function () { describe('methods: ', function () {
var data = { var data = {
storage: { storage: {
'item_1': { item_1: {
value: 'value_1', value: 'value_1',
session: false session: false
} }
...@@ -69,15 +69,15 @@ function (CookieStorage) { ...@@ -69,15 +69,15 @@ function (CookieStorage) {
it('pass correct data', function () { it('pass correct data', function () {
var expected = JSON.stringify({ var expected = JSON.stringify({
storage: { storage: {
'item_1': { item_1: {
value: 'value_1', value: 'value_1',
session: false session: false
}, },
'item_2': { item_2: {
value: 'value_2', value: 'value_2',
session: false session: false
}, },
'item_3': { item_3: {
value: 'value_3', value: 'value_3',
session: true session: true
}, },
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
beforeEach(function () { beforeEach(function () {
jasmine.stubRequests(); jasmine.stubRequests();
this.videosDefinition = '0.75:7tqY6eQzVhE,1.0:cogebirgzzM';
this['7tqY6eQzVhE'] = '7tqY6eQzVhE';
this['cogebirgzzM'] = 'cogebirgzzM';
}); });
afterEach(function () { afterEach(function () {
...@@ -17,7 +14,7 @@ ...@@ -17,7 +14,7 @@
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('video.html'); loadFixtures('video.html');
$.cookie.andReturn('0.75'); $.cookie.andReturn('0.50');
}); });
describe('by default', function () { describe('by default', function () {
...@@ -35,17 +32,18 @@ ...@@ -35,17 +32,18 @@
it('parse the videos', function () { it('parse the videos', function () {
expect(this.state.videos).toEqual({ expect(this.state.videos).toEqual({
'0.75': this['7tqY6eQzVhE'], '0.50': '7tqY6eQzVhE',
'1.0': this['cogebirgzzM'] '1.0': 'cogebirgzzM',
'1.50': 'abcdefghijkl'
}); });
}); });
it('parse available video speeds', function () { it('parse available video speeds', function () {
expect(this.state.speeds).toEqual(['0.75', '1.0']); expect(this.state.speeds).toEqual(['0.50', '1.0', '1.50']);
}); });
it('set current video speed via cookie', function () { it('set current video speed via cookie', function () {
expect(this.state.speed).toEqual('0.75'); expect(this.state.speed).toEqual('1.50');
}); });
}); });
}); });
...@@ -157,7 +155,7 @@ ...@@ -157,7 +155,7 @@
}); });
it('set current video speed via cookie', function () { it('set current video speed via cookie', function () {
expect(state.speed).toEqual('0.75'); expect(state.speed).toEqual('1.50');
}); });
}); });
...@@ -190,16 +188,18 @@ ...@@ -190,16 +188,18 @@
describe('with speed', function () { describe('with speed', function () {
it('return the video id for given speed', function () { it('return the video id for given speed', function () {
expect(state.youtubeId('0.75')) expect(state.youtubeId('0.50'))
.toEqual(this['7tqY6eQzVhE']); .toEqual('7tqY6eQzVhE');
expect(state.youtubeId('1.0')) expect(state.youtubeId('1.0'))
.toEqual(this['cogebirgzzM']); .toEqual('cogebirgzzM');
expect(state.youtubeId('1.50'))
.toEqual('abcdefghijkl');
}); });
}); });
describe('without speed', function () { describe('without speed', function () {
it('return the video id for current speed', function () { it('return the video id for current speed', function () {
expect(state.youtubeId()).toEqual(this.cogebirgzzM); expect(state.youtubeId()).toEqual('abcdefghijkl');
}); });
}); });
}); });
...@@ -314,44 +314,25 @@ ...@@ -314,44 +314,25 @@
}); });
describe('setSpeed', function () { describe('setSpeed', function () {
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('video.html'); loadFixtures('video.html');
state = new Video('#example'); state = new Video('#example');
}); });
describe('when new speed is available', function () { it('check mapping', function () {
beforeEach(function () { var map = {
state.setSpeed('0.75', true); '0.75': '0.50',
}); '1.25': '1.50'
};
it('set new speed', function () {
expect(state.speed).toEqual('0.75');
});
it('save setting for new speed', function () {
expect($.cookie).toHaveBeenCalledWith(
'video_speed',
'0.75',
{
expires: 3650,
path: '/'
}
);
});
});
describe('when new speed is not available', function () {
beforeEach(function () {
state.setSpeed('1.75');
});
it('set speed to 1.0x', function () { $.each(map, function(key, expected) {
expect(state.speed).toEqual('1.0'); state.setSpeed(key, true);
expect(state.speed).toBe(expected);
}); });
}); });
}); });
describe('HTML5', function () { describe('HTML5', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('video_html5.html'); loadFixtures('video_html5.html');
...@@ -368,14 +349,9 @@ ...@@ -368,14 +349,9 @@
}); });
it('save setting for new speed', function () { it('save setting for new speed', function () {
expect($.cookie).toHaveBeenCalledWith(
'video_speed', expect(state.storage.getItem('general_speed')).toBe('0.75');
'0.75', expect(state.storage.getItem('video_speed_' + state.id)).toBe('0.75');
{
expires: 3650,
path: '/'
}
);
}); });
}); });
...@@ -388,6 +364,19 @@ ...@@ -388,6 +364,19 @@
expect(state.speed).toEqual('1.0'); 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);
});
});
}); });
}); });
...@@ -398,7 +387,7 @@ ...@@ -398,7 +387,7 @@
}); });
it('return duration for current video', function () { it('return duration for current video', function () {
expect(state.getDuration()).toEqual(200); expect(state.getDuration()).toEqual(400);
}); });
}); });
......
...@@ -36,9 +36,9 @@ ...@@ -36,9 +36,9 @@
it('create video caption', function () { it('create video caption', function () {
expect(state.videoCaption).toBeDefined(); expect(state.videoCaption).toBeDefined();
expect(state.youtubeId()).toEqual('Z5KLxerq05Y'); expect(state.youtubeId('1.0')).toEqual('Z5KLxerq05Y');
expect(state.speed).toEqual('1.0'); expect(state.speed).toEqual('1.50');
expect(state.config.caption_asset_path) expect(state.config.captionAssetPath)
.toEqual('/static/subs/'); .toEqual('/static/subs/');
}); });
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
expect(state.videoSpeedControl.el).toHaveClass('speeds'); expect(state.videoSpeedControl.el).toHaveClass('speeds');
expect(state.videoSpeedControl.speeds) expect(state.videoSpeedControl.speeds)
.toEqual([ '0.75', '1.0', '1.25', '1.50' ]); .toEqual([ '0.75', '1.0', '1.25', '1.50' ]);
expect(state.speed).toEqual('1.0'); expect(state.speed).toEqual('1.50');
}); });
it('create video progress slider', function () { it('create video progress slider', function () {
...@@ -395,7 +395,7 @@ ...@@ -395,7 +395,7 @@
'speed_change_video', 'speed_change_video',
{ {
current_time: state.videoPlayer.currentTime, current_time: state.videoPlayer.currentTime,
old_speed: '1.0', old_speed: '1.50',
new_speed: '0.75' new_speed: '0.75'
} }
); );
...@@ -406,7 +406,7 @@ ...@@ -406,7 +406,7 @@
}); });
it('set video speed to the new speed', function () { it('set video speed to the new speed', function () {
expect(state.setSpeed).toHaveBeenCalledWith('0.75', false); expect(state.setSpeed).toHaveBeenCalledWith('0.75', true);
}); });
}); });
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
expect(secondaryControls).toContain('.speeds'); expect(secondaryControls).toContain('.speeds');
expect(secondaryControls).toContain('.video_speeds'); expect(secondaryControls).toContain('.video_speeds');
expect(secondaryControls.find('p.active').text()) expect(secondaryControls.find('p.active').text())
.toBe('1.0x'); .toBe('1.50x');
expect(li.filter('.active')).toHaveData( expect(li.filter('.active')).toHaveData(
'speed', state.videoSpeedControl.currentSpeed 'speed', state.videoSpeedControl.currentSpeed
); );
......
...@@ -15,8 +15,6 @@ function() { ...@@ -15,8 +15,6 @@ function() {
* @param {string} namespace Namespace that is used to store data. * @param {string} namespace Namespace that is used to store data.
* @return {object} CookieStorage API. * @return {object} CookieStorage API.
*/ */
var CookieStorage = function (namespace) { var CookieStorage = function (namespace) {
var Storage; var Storage;
...@@ -73,7 +71,7 @@ function() { ...@@ -73,7 +71,7 @@ function() {
}); });
$.cookie(namespace, JSON.stringify(Storage), { $.cookie(namespace, JSON.stringify(Storage), {
expires: -1, expires: 3650,
path: '/' path: '/'
}); });
}; };
......
...@@ -324,7 +324,7 @@ function (HTML5Video, Resizer) { ...@@ -324,7 +324,7 @@ function (HTML5Video, Resizer) {
} }
} }
function onSpeedChange(newSpeed, updateCookie) { function onSpeedChange(newSpeed) {
var time = this.videoPlayer.currentTime, var time = this.videoPlayer.currentTime,
methodName, youtubeId; methodName, youtubeId;
...@@ -347,7 +347,7 @@ function (HTML5Video, Resizer) { ...@@ -347,7 +347,7 @@ function (HTML5Video, Resizer) {
} }
); );
this.setSpeed(newSpeed, updateCookie); this.setSpeed(newSpeed, true);
if ( if (
this.currentPlayerMode === 'html5' && this.currentPlayerMode === 'html5' &&
...@@ -376,6 +376,15 @@ function (HTML5Video, Resizer) { ...@@ -376,6 +376,15 @@ function (HTML5Video, Resizer) {
} }
this.el.trigger('speedchange', arguments); this.el.trigger('speedchange', arguments);
$.ajax({
url: this.config.saveStateUrl,
type: 'POST',
dataType: 'json',
data: {
speed: newSpeed
},
});
} }
// Every 200 ms, if the video is playing, we call the function update, via // Every 200 ms, if the video is playing, we call the function update, via
...@@ -434,7 +443,7 @@ function (HTML5Video, Resizer) { ...@@ -434,7 +443,7 @@ function (HTML5Video, Resizer) {
end: true end: true
}); });
if (this.config.show_captions) { if (this.config.showCaptions) {
this.trigger('videoCaption.pause', null); this.trigger('videoCaption.pause', null);
} }
...@@ -466,7 +475,7 @@ function (HTML5Video, Resizer) { ...@@ -466,7 +475,7 @@ function (HTML5Video, Resizer) {
this.trigger('videoControl.pause', null); this.trigger('videoControl.pause', null);
if (this.config.show_captions) { if (this.config.showCaptions) {
this.trigger('videoCaption.pause', null); this.trigger('videoCaption.pause', null);
} }
...@@ -495,7 +504,7 @@ function (HTML5Video, Resizer) { ...@@ -495,7 +504,7 @@ function (HTML5Video, Resizer) {
end: false end: false
}); });
if (this.config.show_captions) { if (this.config.showCaptions) {
this.trigger('videoCaption.play', null); this.trigger('videoCaption.play', null);
} }
...@@ -579,7 +588,6 @@ function (HTML5Video, Resizer) { ...@@ -579,7 +588,6 @@ function (HTML5Video, Resizer) {
var key = value.toFixed(2).replace(/\.00$/, '.0'); var key = value.toFixed(2).replace(/\.00$/, '.0');
_this.videos[key] = baseSpeedSubs; _this.videos[key] = baseSpeedSubs;
_this.speeds.push(key); _this.speeds.push(key);
}); });
...@@ -590,8 +598,8 @@ function (HTML5Video, Resizer) { ...@@ -590,8 +598,8 @@ function (HTML5Video, Resizer) {
currentSpeed: this.speed currentSpeed: this.speed
} }
); );
this.setSpeed(this.speed);
this.setSpeed($.cookie('video_speed')); this.trigger('videoSpeedControl.setSpeed', this.speed);
} }
} }
......
...@@ -252,7 +252,7 @@ function () { ...@@ -252,7 +252,7 @@ function () {
} }
function captionURL() { function captionURL() {
return '' + this.config.caption_asset_path + return '' + this.config.captionAssetPath +
this.youtubeId('1.0') + '.srt.sjson'; this.youtubeId('1.0') + '.srt.sjson';
} }
...@@ -356,7 +356,7 @@ function () { ...@@ -356,7 +356,7 @@ function () {
_this = this, _this = this,
autohideHtml5 = this.config.autohideHtml5; autohideHtml5 = this.config.autohideHtml5;
this.elVideoWrapper.after(this.videoCaption.subtitlesEl); this.container.after(this.videoCaption.subtitlesEl);
this.el.find('.video-controls .secondary-controls') this.el.find('.video-controls .secondary-controls')
.append(this.videoCaption.hideSubtitlesEl); .append(this.videoCaption.hideSubtitlesEl);
...@@ -745,7 +745,7 @@ function () { ...@@ -745,7 +745,7 @@ function () {
0.5 * this.videoControl.sliderEl.height() - 0.5 * this.videoControl.sliderEl.height() -
2 * paddingTop; 2 * paddingTop;
} else { } else {
return this.elVideoWrapper.height(); return this.container.height();
} }
} }
......
...@@ -20,7 +20,6 @@ import datetime ...@@ -20,7 +20,6 @@ import datetime
import copy import copy
from webob import Response from webob import Response
from django.http import Http404
from django.conf import settings from django.conf import settings
from xmodule.x_module import XModule, module_attr from xmodule.x_module import XModule, module_attr
...@@ -31,7 +30,7 @@ from xmodule.contentstore.django import contentstore ...@@ -31,7 +30,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, Boolean, List, Integer, ScopeIds from xblock.fields import Scope, String, Float, Boolean, List, Integer, ScopeIds
from xmodule.fields import RelativeTime from xmodule.fields import RelativeTime
from xmodule.modulestore.inheritance import InheritanceKeyValueStore from xmodule.modulestore.inheritance import InheritanceKeyValueStore
...@@ -137,6 +136,15 @@ class VideoFields(object): ...@@ -137,6 +136,15 @@ class VideoFields(object):
scope=Scope.settings, scope=Scope.settings,
default="" default=""
) )
speed = Float(
help="The last speed that was explicitly set by user for the video.",
scope=Scope.user_state,
)
global_speed = Float(
help="Default speed in cases when speed wasn't explicitly for specific video",
scope=Scope.preferences,
default=1.0
)
class VideoModule(VideoFields, XModule): class VideoModule(VideoFields, XModule):
...@@ -178,10 +186,21 @@ class VideoModule(VideoFields, XModule): ...@@ -178,10 +186,21 @@ class VideoModule(VideoFields, XModule):
js_module_name = "Video" js_module_name = "Video"
def handle_ajax(self, dispatch, data): def handle_ajax(self, dispatch, data):
"""This is not being called right now and we raise 404 error.""" ACCEPTED_KEYS = ['speed']
if dispatch == 'save_user_state':
for key in data:
if hasattr(self, key) and key in ACCEPTED_KEYS:
setattr(self, key, json.loads(data[key]))
if key == 'speed':
self.global_speed = self.speed
return json.dumps({'success': True})
log.debug(u"GET {0}".format(data)) log.debug(u"GET {0}".format(data))
log.debug(u"DISPATCH {0}".format(dispatch)) log.debug(u"DISPATCH {0}".format(dispatch))
raise Http404()
raise NotFoundError('Unexpected dispatch type')
def get_html(self): def get_html(self):
track_url = None track_url = None
...@@ -203,24 +222,26 @@ class VideoModule(VideoFields, XModule): ...@@ -203,24 +222,26 @@ class VideoModule(VideoFields, XModule):
track_url = self.runtime.handler_url(self, 'download_transcript') track_url = self.runtime.handler_url(self, 'download_transcript')
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'youtube_streams': _create_youtube_string(self), 'ajax_url': self.system.ajax_url + '/save_user_state',
'id': self.location.html_id(), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
'sub': self.sub,
'sources': sources,
'track': track_url,
'display_name': self.display_name_with_default,
# This won't work when we move to data that # This won't work when we move to data that
# isn't on the filesystem # isn't on the filesystem
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'display_name': self.display_name_with_default,
'caption_asset_path': caption_asset_path, 'caption_asset_path': caption_asset_path,
'end': self.end_time.total_seconds(),
'id': self.location.html_id(),
'show_captions': json.dumps(self.show_captions), 'show_captions': json.dumps(self.show_captions),
'sources': sources,
'speed': self.speed or self.global_speed,
'start': self.start_time.total_seconds(), 'start': self.start_time.total_seconds(),
'end': self.end_time.total_seconds(), 'sub': self.sub,
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'track': track_url,
'youtube_streams': _create_youtube_string(self),
# TODO: Later on the value 1500 should be taken from some global # TODO: Later on the value 1500 should be taken from some global
# configuration setting field. # configuration setting field.
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
'yt_test_url': settings.YOUTUBE_TEST_URL 'yt_test_url': settings.YOUTUBE_TEST_URL,
}) })
def get_transcript(self, subs_id): def get_transcript(self, subs_id):
......
...@@ -45,3 +45,24 @@ Feature: LMS.Video component ...@@ -45,3 +45,24 @@ Feature: LMS.Video component
Given the course has a Video component in HTML5_Unsupported_Video mode Given the course has a Video component in HTML5_Unsupported_Video mode
Then error message is shown Then error message is shown
And error message has correct text And error message has correct text
# 8
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
And a video "C" in "Youtube" mode in position "3" of sequential
And I open the section with videos
And I select the "2.0" speed on video "A"
And I select the "0.50" speed on video "B"
When I open video "C"
Then video "C" should start playing at speed "0.50"
When I open video "A"
Then video "A" should start playing at speed "2.0"
And I reload the page
When I open video "A"
Then video "A" should start playing at speed "2.0"
When I open video "B"
Then video "B" should start playing at speed "0.50"
When I open video "C"
Then video "C" should start playing at speed "0.50"
...@@ -15,6 +15,9 @@ HTML5_SOURCES_INCORRECT = [ ...@@ -15,6 +15,9 @@ HTML5_SOURCES_INCORRECT = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99' 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99'
] ]
coursenum = 'test_course'
sequence = {}
@step('when I view the (.*) it does not have autoplay enabled$') @step('when I view the (.*) it does not have autoplay enabled$')
def does_not_autoplay(_step, video_type): def does_not_autoplay(_step, video_type):
assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False') assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False')
...@@ -22,21 +25,48 @@ def does_not_autoplay(_step, video_type): ...@@ -22,21 +25,48 @@ def does_not_autoplay(_step, video_type):
@step('the course has a Video component in (.*) mode$') @step('the course has a Video component in (.*) mode$')
def view_video(_step, player_mode): def view_video(_step, player_mode):
coursenum = 'test_course'
i_am_registered_for_the_course(step, coursenum) i_am_registered_for_the_course(_step, coursenum)
# Make sure we have a video # Make sure we have a video
add_video_to_course(coursenum, player_mode.lower()) add_video_to_course(coursenum, player_mode.lower())
visit_scenario_item('SECTION') visit_scenario_item('SECTION')
def add_video_to_course(course, player_mode): @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)
@step('I open the section with videos$')
def visit_video_section(_step):
visit_scenario_item('SECTION')
@step('I select the "([^"]*)" speed on video "([^"]*)"$')
def change_video_speed(_step, speed, player_id):
_navigate_to_an_item_in_a_sequence(sequence[player_id])
_change_video_speed(speed)
@step('I open video "([^"]*)"$')
def open_video(_step, player_id):
_navigate_to_an_item_in_a_sequence(sequence[player_id])
@step('video "([^"]*)" should start playing at speed "([^"]*)"$')
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'):
category = 'video' category = 'video'
kwargs = { kwargs = {
'parent_location': section_location(course), 'parent_location': section_location(course),
'category': category, 'category': category,
'display_name': 'Video' 'display_name': display_name
} }
if player_mode == 'html5': if player_mode == 'html5':
...@@ -112,3 +142,12 @@ def error_message_has_correct_text(_step): ...@@ -112,3 +142,12 @@ def error_message_has_correct_text(_step):
assert world.css_has_text(selector, text) assert world.css_has_text(selector, text)
def _navigate_to_an_item_in_a_sequence(number):
sequence_css = 'a[data-element="{0}"]'.format(number)
world.css_click(sequence_css)
def _change_video_speed(speed):
world.browser.execute_script("$('.speeds').addClass('open')")
speed_css = 'li[data-speed="{0}"] a'.format(speed)
world.css_click(speed_css)
...@@ -19,6 +19,7 @@ from xmodule.exceptions import NotFoundError ...@@ -19,6 +19,7 @@ from xmodule.exceptions import NotFoundError
class TestVideo(BaseTestXmodule): class TestVideo(BaseTestXmodule):
"""Integration tests: web client + mongo.""" """Integration tests: web client + mongo."""
CATEGORY = "video" CATEGORY = "video"
DATA = SOURCE_XML DATA = SOURCE_XML
METADATA = {} METADATA = {}
...@@ -57,6 +58,7 @@ class TestVideoYouTube(TestVideo): ...@@ -57,6 +58,7 @@ class TestVideoYouTube(TestVideo):
} }
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/static/subs/', 'caption_asset_path': '/static/subs/',
'show_captions': 'true', 'show_captions': 'true',
...@@ -64,6 +66,7 @@ class TestVideoYouTube(TestVideo): ...@@ -64,6 +66,7 @@ class TestVideoYouTube(TestVideo):
'end': 3610.0, 'end': 3610.0,
'id': self.item_module.location.html_id(), 'id': self.item_module.location.html_id(),
'sources': sources, 'sources': sources,
'speed': 1.0,
'start': 3603.0, 'start': 3603.0,
'sub': u'a_sub_file.srt.sjson', 'sub': u'a_sub_file.srt.sjson',
'track': None, 'track': None,
...@@ -75,7 +78,7 @@ class TestVideoYouTube(TestVideo): ...@@ -75,7 +78,7 @@ class TestVideoYouTube(TestVideo):
self.assertEqual( self.assertEqual(
context, context,
self.item_module.xmodule_runtime.render_template('video.html', expected_context) self.item_module.xmodule_runtime.render_template('video.html', expected_context),
) )
...@@ -93,9 +96,10 @@ class TestVideoNonYouTube(TestVideo): ...@@ -93,9 +96,10 @@ class TestVideoNonYouTube(TestVideo):
</video> </video>
""" """
MODEL_DATA = { MODEL_DATA = {
'data': DATA 'data': DATA,
} }
METADATA = {} METADATA = {}
def test_video_constructor(self): def test_video_constructor(self):
"""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.
...@@ -107,8 +111,8 @@ class TestVideoNonYouTube(TestVideo): ...@@ -107,8 +111,8 @@ class TestVideoNonYouTube(TestVideo):
} }
context = self.item_module.render('student_view').content context = self.item_module.render('student_view').content
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/static/subs/', 'caption_asset_path': '/static/subs/',
'show_captions': 'true', 'show_captions': 'true',
...@@ -116,6 +120,7 @@ class TestVideoNonYouTube(TestVideo): ...@@ -116,6 +120,7 @@ class TestVideoNonYouTube(TestVideo):
'end': 3610.0, 'end': 3610.0,
'id': self.item_module.location.html_id(), 'id': self.item_module.location.html_id(),
'sources': sources, 'sources': sources,
'speed': 1.0,
'start': 3603.0, 'start': 3603.0,
'sub': u'a_sub_file.srt.sjson', 'sub': u'a_sub_file.srt.sjson',
'track': None, 'track': None,
...@@ -127,7 +132,7 @@ class TestVideoNonYouTube(TestVideo): ...@@ -127,7 +132,7 @@ class TestVideoNonYouTube(TestVideo):
self.assertEqual( self.assertEqual(
context, context,
self.item_module.xmodule_runtime.render_template('video.html', expected_context) self.item_module.xmodule_runtime.render_template('video.html', expected_context),
) )
...@@ -137,6 +142,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -137,6 +142,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
''' '''
CATEGORY = "video" CATEGORY = "video"
DATA = SOURCE_XML DATA = SOURCE_XML
maxDiff = None
METADATA = {} METADATA = {}
def setUp(self): def setUp(self):
...@@ -195,7 +201,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -195,7 +201,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
}, },
'start': 3603.0, 'start': 3603.0,
'sub': u'a_sub_file.srt.sjson', 'sub': u'a_sub_file.srt.sjson',
'track': '', 'speed': 1.0,
'track': None,
'youtube_streams': '1.00:OEoXaMPEzfM', 'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
...@@ -212,16 +219,18 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -212,16 +219,18 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.initialize_module(data=DATA) self.initialize_module(data=DATA)
track_url = self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'download_transcript') track_url = self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'download_transcript')
context = self.item_module.render('student_view').content
expected_context.update({ expected_context.update({
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'], 'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
'sub': data['sub'], 'sub': data['sub'],
'id': self.item_module.location.html_id(), 'id': self.item_module.location.html_id(),
}) })
context = self.item_module.render('student_view').content
self.assertEqual( self.assertEqual(
context, context,
self.item_module.xmodule_runtime.render_template('video.html', expected_context) self.item_module.xmodule_runtime.render_template('video.html', expected_context),
) )
def test_get_html_source(self): def test_get_html_source(self):
...@@ -293,6 +302,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -293,6 +302,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
'end': 3610.0, 'end': 3610.0,
'id': None, 'id': None,
'sources': None, 'sources': None,
'speed': 1.0,
'start': 3603.0, 'start': 3603.0,
'sub': u'a_sub_file.srt.sjson', 'sub': u'a_sub_file.srt.sjson',
'track': None, 'track': None,
...@@ -309,14 +319,14 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -309,14 +319,14 @@ class TestGetHtmlMethod(BaseTestXmodule):
sources=data['sources'] sources=data['sources']
) )
self.initialize_module(data=DATA) self.initialize_module(data=DATA)
context = self.item_module.render('student_view').content
expected_context.update({ expected_context.update({
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'sources': data['result'], 'sources': data['result'],
'id': self.item_module.location.html_id(), 'id': self.item_module.location.html_id(),
}) })
context = self.item_module.render('student_view').content
self.assertEqual( self.assertEqual(
context, context,
self.item_module.xmodule_runtime.render_template('video.html', expected_context) self.item_module.xmodule_runtime.render_template('video.html', expected_context)
......
...@@ -15,11 +15,7 @@ common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the ...@@ -15,11 +15,7 @@ common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
course, section, subsection, unit, etc. course, section, subsection, unit, etc.
""" """
import unittest from xmodule.video_module import VideoDescriptor
from django.conf import settings
from xmodule.video_module import VideoDescriptor, _create_youtube_string
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import get_test_system, LogicTest, get_test_descriptor_system from xmodule.tests import get_test_system, LogicTest, get_test_descriptor_system
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
...@@ -63,40 +59,6 @@ class VideoFactory(object): ...@@ -63,40 +59,6 @@ class VideoFactory(object):
return descriptor return descriptor
class VideoModuleUnitTest(unittest.TestCase):
"""Unit tests for Video Xmodule."""
def test_video_get_html(self):
"""Make sure that all parameters extracted correclty from xml"""
module = VideoFactory.create()
sources = {
'main': 'example.mp4',
'mp4': 'example.mp4',
'webm': 'example.webm',
}
expected_context = {
'caption_asset_path': '/static/subs/',
'sub': 'a_sub_file.srt.sjson',
'data_dir': getattr(self, 'data_dir', None),
'display_name': 'A Name',
'end': 3610.0,
'start': 3603.0,
'id': module.location.html_id(),
'show_captions': 'true',
'sources': sources,
'youtube_streams': _create_youtube_string(module),
'track': None,
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(
module.render('student_view').content,
module.runtime.render_template('video.html', expected_context)
)
class VideoModuleLogicTest(LogicTest): class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule.""" """Tests for logic of Video Xmodule."""
......
...@@ -17,8 +17,10 @@ ...@@ -17,8 +17,10 @@
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''} ${'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-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
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}"
data-speed="${speed}"
data-start="${start}" data-start="${start}"
data-end="${end}" data-end="${end}"
data-caption-asset-path="${caption_asset_path}" data-caption-asset-path="${caption_asset_path}"
...@@ -108,5 +110,3 @@ ...@@ -108,5 +110,3 @@
% endif % endif
</ul> </ul>
</div> </div>
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