diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6455e439e3..a9f17d6c7f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Blades: Adds CookieStorage utility for video player that provides convenient + way to work with cookies. + Blades: Fix comparison of float numbers. BLD-434. Blades: Allow regexp strings as the correct answer to a string response question. BLD-475. diff --git a/common/lib/xmodule/xmodule/js/spec/video/cookie_storage_spec.js b/common/lib/xmodule/xmodule/js/spec/video/cookie_storage_spec.js new file mode 100644 index 0000000000..dff25bb141 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/cookie_storage_spec.js @@ -0,0 +1,149 @@ +(function (requirejs, require, define) { +require( +['video/00_cookie_storage.js'], +function (CookieStorage) { + describe('CookieStorage', function () { + var mostRecentCall; + + beforeEach(function () { + mostRecentCall = $.cookie.mostRecentCall; + }); + + afterEach(function () { + CookieStorage('test_storage').clear(); + }); + + describe('intialize', function () { + it('with namespace', function () { + var storage = CookieStorage('test_storage'); + + storage.setItem('item_1', 'value_1'); + expect(mostRecentCall.args[0]).toBe('test_storage'); + }); + + it('without namespace', function () { + var storage = CookieStorage(); + + storage.setItem('item_1', 'value_1'); + expect(mostRecentCall.args[0]).toBe('cookieStorage'); + }); + }); + + it('unload', function () { + var expected = JSON.stringify({ + storage: { + 'item_2': { + value: 'value_2', + session: false + } + }, + keys: ['item_2'] + }), + storage = CookieStorage('test_storage'); + + storage.setItem('item_1', 'value_1', true); + storage.setItem('item_2', 'value_2'); + + $(window).trigger('unload'); + expect(mostRecentCall.args[1]).toBe(expected); + }); + + describe('methods: ', function () { + var data = { + storage: { + 'item_1': { + value: 'value_1', + session: false + } + }, + keys: ['item_1'] + }, + storage; + + beforeEach(function () { + $.cookie.andReturn(JSON.stringify(data)); + storage = CookieStorage('test_storage'); + }); + + describe('setItem', function () { + it('pass correct data', function () { + var expected = JSON.stringify({ + storage: { + 'item_1': { + value: 'value_1', + session: false + }, + 'item_2': { + value: 'value_2', + session: false + }, + 'item_3': { + value: 'value_3', + session: true + }, + }, + keys: ['item_1', 'item_2', 'item_3'] + }); + + storage.setItem('item_2', 'value_2'); + storage.setItem('item_3', 'value_3', true); + expect(mostRecentCall.args[0]).toBe('test_storage'); + expect(mostRecentCall.args[1]).toBe(expected); + }); + + it('pass broken arguments', function () { + $.cookie.reset(); + storage.setItem(null, 'value_1'); + expect($.cookie).not.toHaveBeenCalled(); + }); + }); + + describe('getItem', function () { + it('item exist', function () { + $.each(data['storage'], function(key, value) { + expect(storage.getItem(key)).toBe(value['value']); + }); + }); + + it('item does not exist', function () { + expect(storage.getItem('nonexistent')).toBe(null); + }); + }); + + describe('removeItem', function () { + it('item exist', function () { + var expected = JSON.stringify({ + storage: {}, + keys: [] + }); + + storage.removeItem('item_1'); + expect(mostRecentCall.args[1]).toBe(expected); + }); + + it('item does not exist', function () { + storage.removeItem('nonexistent'); + expect(mostRecentCall.args[1]).toBe(JSON.stringify(data)); + }); + }); + + it('clear', function () { + storage.clear(); + expect(mostRecentCall.args[1]).toBe(null); + }); + + describe('key', function () { + it('key exist', function () { + $.each(data['keys'], function(index, name) { + expect(storage.key(index)).toBe(name); + }); + }); + + it('key is grater than keys list', function () { + expect(storage.key(100)).toBe(null); + }); + }); + }); + }); +}); +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js index c8e2db97f7..8f09030189 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js @@ -22,11 +22,12 @@ describe('constructor', function() { beforeEach(function() { spyOn($.fn, 'slider').andCallThrough(); + $.cookie.andReturn('75'); initialize(); }); - it('initialize currentVolume to 100', function() { - expect(state.videoVolumeControl.currentVolume).toEqual(1); + it('initialize currentVolume to 75', function() { + expect(state.videoVolumeControl.currentVolume).toEqual(75); }); it('render the volume control', function() { diff --git a/common/lib/xmodule/xmodule/js/src/video/00_cookie_storage.js b/common/lib/xmodule/xmodule/js/src/video/00_cookie_storage.js new file mode 100644 index 0000000000..fb46dfb9dc --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/00_cookie_storage.js @@ -0,0 +1,196 @@ +(function (requirejs, require, define) { + +define( +'video/00_cookie_storage.js', +[], +function() { + "use strict"; +/** + * Provides convenient way to work with cookies. + * + * Maximum 4096 bytes can be stored per namespace. + * + * @TODO: Uses localStorage if available. + * + * @param {string} namespace Namespace that is used to store data. + * @return {object} CookieStorage API. + */ + + + var CookieStorage = function (namespace) { + var Storage; + + /** + * Returns an empty storage with proper data structure. + * + * @private + * @return {object} Empty storage. + */ + var _getEmptyStorage = function () { + return { + storage: {}, + keys: [] + }; + }; + + /** + * Returns the current value associated with the given namespace. + * If data doesn't exist or has data with incorrect interface, it creates + * an empty storage with proper data structure. + * + * @private + * @param {string} namespace Namespace that is used to store data. + * @return {object} Stored data or an empty storage. + */ + var _getData = function (namespace) { + var data; + + try { + data = JSON.parse($.cookie(namespace)); + } catch (err) { } + + if (!data || !data['storage'] || !data['keys']) { + return _getEmptyStorage(); + } + + return data; + }; + + /** + * Clears cookies that has flag `session` equals true. + * + * @private + */ + var _clearSession = function () { + Storage['keys'] = $.grep(Storage['keys'], function(key_name, index) { + if (Storage['storage'][key_name]['session']) { + delete Storage['storage'][key_name]; + + return false; + } + + return true; + }); + + $.cookie(namespace, JSON.stringify(Storage), { + expires: -1, + path: '/' + }); + }; + + /** + * Adds new value to the storage or rewrites existent. + * + * @param {string} name Identifier of the data. + * @param {any} value Data to store. + * @param {boolean} useSession Data with this flag will be removed on + * window unload. + */ + var setItem = function (name, value, useSession) { + if (name) { + if ($.inArray(name, Storage['keys']) === -1) { + Storage['keys'].push(name); + } + + Storage['storage'][name] = { + value: value, + session: useSession ? true : false + }; + + $.cookie(namespace, JSON.stringify(Storage), { + expires: 3650, + path: '/' + }); + } + }; + + /** + * Returns the current value associated with the given name. + * + * @param {string} name Identifier of the data. + * @return {any} The current value associated with the given name. + * If the given key does not exist in the list + * associated with the object then this method must return null. + */ + var getItem = function (name) { + try { + return Storage['storage'][name]['value']; + } catch (err) { } + + return null; + }; + + /** + * Removes the current value associated with the given name. + * + * @param {string} name Identifier of the data. + */ + var removeItem = function (name) { + delete Storage['storage'][name]; + + Storage['keys'] = $.grep(Storage['keys'], function(key_name, index) { + return name !== key_name; + }); + + $.cookie(namespace, JSON.stringify(Storage), { + expires: 3650, + path: '/' + }); + }; + + /** + * Empties the storage. + * + */ + var clear = function () { + Storage = _getEmptyStorage(); + $.cookie(namespace, null, { + expires: -1, + path: '/' + }); + }; + + /** + * Returns the name of the `n`th key in the list. + * + * @param {number} n Index of the key. + * @return {string} Name of the `n`th key in the list. + * If `n` is greater than or equal to the number of key/value pairs + * in the object, then this method must return `null`. + */ + var key = function (n) { + if (n >= Storage['keys'].length) { + return null; + } + + return Storage['keys'][n]; + }; + + /** + * Initializes the module: creates a storage with proper namespace, binds + * `unload` event. + * + * @private + */ + (function initialize() { + if (!namespace) { + namespace = 'cookieStorage'; + } + Storage = _getData(namespace); + + $(window).unload(_clearSession); + }()); + + return { + clear: clear, + getItem: getItem, + key: key, + removeItem: removeItem, + setItem: setItem + }; + }; + + return CookieStorage; +}); + +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/README.rst b/common/lib/xmodule/xmodule/js/src/video/README.rst new file mode 100644 index 0000000000..d27ab86517 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/README.rst @@ -0,0 +1,16 @@ +Video player persists some user preferences between videos and these +preferences are stored on server. + +Content for sequential positions is loaded just once on page load and is not +updated when the user navigates between sequential positions. So, we doesn't +have an actual data from server. +To resolve this issue, cookies are used as temporary storage and are removed +on page unload. + +How it works: +1) On page load: cookies are empty and player get an actual data from server. +2) When user change some preferences, new value is stored to cookie; +3) If we navigate to another sequential position, video player get an actual data +from cookies. +4) Close the page: `unload` event fires and we clear our cookies and send user +preferences to the server. diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 364a8ee417..2a27c03c45 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -25,7 +25,6 @@ from xmodule.x_module import XModule from xmodule.editing_module import TabsEditingDescriptor from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field -from xmodule.modulestore import Location from xblock.fields import Scope, String, Boolean, List, Integer, ScopeIds from xmodule.fields import RelativeTime @@ -134,8 +133,11 @@ class VideoModule(VideoFields, XModule): video_time = 0 icon_class = 'video' + # To make sure that js files are called in proper order we use numerical + # index. We do that to avoid issues that occurs in tests. js = { 'js': [ + resource_string(__name__, 'js/src/video/00_cookie_storage.js'), resource_string(__name__, 'js/src/video/00_resizer.js'), resource_string(__name__, 'js/src/video/01_initialize.js'), resource_string(__name__, 'js/src/video/025_focus_grabber.js'),