Commit ee4178e7 by polesye

Add CookieStorage.

parent 7fa5ae9c
......@@ -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.
......
(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));
......@@ -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() {
......
(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));
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.
......@@ -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'),
......
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