e-reader error when popping out window
Moving work on BLD-465 from PR 1811. Fixing missing import clause in Python. Addressing DB's comment. BLD-465.
This commit is contained in:
@@ -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: LTI additional Python tests. LTI fix bug e-reader error when popping
|
||||
out window. BLD-465.
|
||||
|
||||
Common: Switch from mitx.db to edx.db for sqlite databases. This will effectively
|
||||
reset state for local instances of the code, unless you manually rename your
|
||||
mitx.db file to edx.db.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<div class="lti-wrapper">
|
||||
<div id="lti_id" class="lti" data-open_in_a_new_page="true">
|
||||
<div
|
||||
id="lti_id"
|
||||
class="lti"
|
||||
data-open_in_a_new_page="true"
|
||||
data-ajax_url="www.example.com/ajax_url"
|
||||
>
|
||||
|
||||
<form
|
||||
action=""
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* File: constructor.js
|
||||
*
|
||||
* Purpose: Jasmine tests for LTI module (front-end part).
|
||||
*
|
||||
*
|
||||
* The front-end part of the LTI module is really simple. If an action
|
||||
* is set for the hidden LTI form, then it is submitted, and the results are
|
||||
* redirected to an iframe or to a new window (based on the
|
||||
* "open_in_a_new_page" attribute).
|
||||
*
|
||||
* We will test that the form is only submitted when the action is set (i.e.
|
||||
* not empty, and not the default one).
|
||||
*
|
||||
* Other aspects of LTI module will be covered by Python unit tests and
|
||||
* acceptance tests.
|
||||
*/
|
||||
|
||||
/*
|
||||
* "Hence that general is skillful in attack whose opponent does not know what
|
||||
* to defend; and he is skillful in defense whose opponent does not know what
|
||||
* to attack."
|
||||
*
|
||||
* ~ Sun Tzu
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var element, container, form, link,
|
||||
IN_NEW_WINDOW = 'true',
|
||||
IN_IFRAME = 'false',
|
||||
EMPTY_URL = '',
|
||||
DEFAULT_URL = 'http://www.example.com',
|
||||
NEW_URL = 'http://www.example.com/some_book';
|
||||
|
||||
function initialize(target, action) {
|
||||
var tempEl;
|
||||
|
||||
loadFixtures('lti.html');
|
||||
|
||||
element = $('.lti-wrapper');
|
||||
container = element.find('.lti');
|
||||
form = container.find('.ltiLaunchForm');
|
||||
|
||||
if (target === IN_IFRAME) {
|
||||
container.data('open_in_a_new_page', 'false');
|
||||
form.attr('target', 'ltiLaunchFrame');
|
||||
}
|
||||
|
||||
form.attr('action', action);
|
||||
|
||||
// If we have a new proper action (non-default), we create either
|
||||
// a link that will submit the form, or an iframe that will contain
|
||||
// the answer of auto submitted form.
|
||||
if (action !== EMPTY_URL && action !== DEFAULT_URL) {
|
||||
if (target === IN_NEW_WINDOW) {
|
||||
$('<a />', {
|
||||
href: '#',
|
||||
class: 'link_lti_new_window'
|
||||
}).appendTo(container);
|
||||
|
||||
link = container.find('.link_lti_new_window');
|
||||
} else {
|
||||
$('<iframe />', {
|
||||
name: 'ltiLaunchFrame',
|
||||
class: 'ltiLaunchFrame',
|
||||
src: ''
|
||||
}).appendTo(container);
|
||||
}
|
||||
}
|
||||
|
||||
spyOnEvent(form, 'submit');
|
||||
|
||||
LTI(element);
|
||||
}
|
||||
|
||||
describe('LTI', function () {
|
||||
describe('initialize', function () {
|
||||
describe(
|
||||
'open_in_a_new_page is "true", launch URL is empty',
|
||||
function () {
|
||||
|
||||
beforeEach(function () {
|
||||
initialize(IN_NEW_WINDOW, EMPTY_URL);
|
||||
});
|
||||
|
||||
it('form is not submitted', function () {
|
||||
expect('submit').not.toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'open_in_a_new_page is "true", launch URL is default',
|
||||
function () {
|
||||
|
||||
beforeEach(function () {
|
||||
initialize(IN_NEW_WINDOW, DEFAULT_URL);
|
||||
});
|
||||
|
||||
it('form is not submitted', function () {
|
||||
expect('submit').not.toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'open_in_a_new_page is "true", launch URL is not empty, and ' +
|
||||
'not default',
|
||||
function () {
|
||||
|
||||
beforeEach(function () {
|
||||
initialize(IN_NEW_WINDOW, NEW_URL);
|
||||
});
|
||||
|
||||
it('form is not submitted', function () {
|
||||
expect('submit').not.toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
|
||||
it('after link is clicked, form is submitted', function () {
|
||||
link.trigger('click');
|
||||
|
||||
expect('submit').toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'open_in_a_new_page is "false", launch URL is empty',
|
||||
function () {
|
||||
|
||||
beforeEach(function () {
|
||||
initialize(IN_IFRAME, EMPTY_URL);
|
||||
});
|
||||
|
||||
it('form is not submitted', function () {
|
||||
expect('submit').not.toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'open_in_a_new_page is "false", launch URL is default',
|
||||
function () {
|
||||
|
||||
beforeEach(function () {
|
||||
initialize(IN_IFRAME, DEFAULT_URL);
|
||||
});
|
||||
|
||||
it('form is not submitted', function () {
|
||||
expect('submit').not.toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'open_in_a_new_page is "false", launch URL is not empty, ' +
|
||||
'and not default',
|
||||
function () {
|
||||
|
||||
beforeEach(function () {
|
||||
initialize(IN_IFRAME, NEW_URL);
|
||||
});
|
||||
|
||||
it('form is submitted', function () {
|
||||
expect('submit').toHaveBeenTriggeredOn(form);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}());
|
||||
370
common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js
Normal file
370
common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* File: constructor.js
|
||||
*
|
||||
* Purpose: Jasmine tests for LTI module (front-end part).
|
||||
*
|
||||
*
|
||||
* Because LTI module is constructed so that all methods are available via the
|
||||
* prototype chain, many times we can test methods without having to
|
||||
* instantiate a new LTI object.
|
||||
*/
|
||||
|
||||
/*
|
||||
* "Hence that general is skillful in attack whose opponent does not know what
|
||||
* to defend; and he is skillful in defense whose opponent does not know what
|
||||
* to attack."
|
||||
*
|
||||
* ~ Sun Tzu
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var IN_NEW_WINDOW = 'true',
|
||||
IN_IFRAME = 'false',
|
||||
EMPTY_URL = '',
|
||||
DEFAULT_URL = 'http://www.example.com',
|
||||
NEW_URL = 'http://www.example.com/some_book';
|
||||
|
||||
describe('LTI XModule', function () {
|
||||
describe('LTIConstructor method', function () {
|
||||
describe('[in iframe, new url]', function () {
|
||||
var lti;
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('lti.html');
|
||||
setUpLtiElement($('.lti-wrapper'), IN_IFRAME, NEW_URL);
|
||||
|
||||
spyOnEvent(
|
||||
$('.lti-wrapper').find('.ltiLaunchForm'), 'submit'
|
||||
);
|
||||
|
||||
lti = new window.LTI('.lti-wrapper');
|
||||
});
|
||||
|
||||
it('new LTI object contains all properties', function () {
|
||||
expect(lti.el).toBeDefined();
|
||||
expect(lti.el).toExist();
|
||||
|
||||
expect(lti.formEl).toBeDefined();
|
||||
expect(lti.formEl).toExist();
|
||||
expect(lti.formEl).toHaveAttr('action');
|
||||
|
||||
expect(lti.ltiEl).toBeDefined();
|
||||
expect(lti.ltiEl).toExist();
|
||||
|
||||
expect(lti.formAction).toEqual(NEW_URL);
|
||||
expect(lti.openInANewPage).toEqual(false);
|
||||
expect(lti.ajaxUrl).toEqual(jasmine.any(String));
|
||||
|
||||
expect('submit').toHaveBeenTriggeredOn(lti.formEl);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
lti = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('[in new window, new url]', function () {
|
||||
var lti;
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('lti.html');
|
||||
setUpLtiElement($('.lti-wrapper'), IN_NEW_WINDOW, NEW_URL);
|
||||
|
||||
lti = new window.LTI('.lti-wrapper');
|
||||
});
|
||||
|
||||
it('check extra properties and values', function () {
|
||||
expect(lti.openInANewPage).toEqual(true);
|
||||
expect(lti.signatureIsNew).toBeTruthy();
|
||||
|
||||
expect(lti.newWindowBtnEl).toBeDefined();
|
||||
expect(lti.newWindowBtnEl).toExist();
|
||||
|
||||
expect(lti.disableOpenNewWindowBtn).toBe(false);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
lti = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('[in iframe, NO new url]', function () {
|
||||
var testCases = [{
|
||||
itDescription: 'URL is blank',
|
||||
action: EMPTY_URL
|
||||
}, {
|
||||
itDescription: 'URL is default',
|
||||
action: DEFAULT_URL
|
||||
}];
|
||||
|
||||
$.each(testCases, function (index, test) {
|
||||
it(test.itDescription, function () {
|
||||
var lti;
|
||||
|
||||
loadFixtures('lti.html');
|
||||
setUpLtiElement(
|
||||
$('.lti-wrapper'), IN_IFRAME, test.action
|
||||
);
|
||||
|
||||
lti = new window.LTI('.lti-wrapper');
|
||||
|
||||
expect(lti.openInANewPage).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitFormHandler method', function () {
|
||||
var thisObj;
|
||||
|
||||
beforeEach(function () {
|
||||
thisObj = {
|
||||
signatureIsNew: undefined,
|
||||
getNewSignature: jasmine.createSpy('getNewSignature'),
|
||||
formEl: {
|
||||
submit: jasmine.createSpy('submit')
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('signature is new', function () {
|
||||
thisObj.signatureIsNew = true;
|
||||
|
||||
window.LTI.prototype.submitFormHandler.call(thisObj);
|
||||
|
||||
expect(thisObj.formEl.submit).toHaveBeenCalled();
|
||||
expect(thisObj.signatureIsNew).toBe(false);
|
||||
});
|
||||
|
||||
it('signature is old', function () {
|
||||
thisObj.signatureIsNew = false;
|
||||
|
||||
window.LTI.prototype.submitFormHandler.call(thisObj);
|
||||
|
||||
expect(thisObj.formEl.submit).not.toHaveBeenCalled();
|
||||
expect(thisObj.signatureIsNew).toBe(false);
|
||||
expect(thisObj.getNewSignature).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
thisObj = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewSignature method', function () {
|
||||
var lti;
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('lti.html');
|
||||
setUpLtiElement($('.lti-wrapper'), IN_NEW_WINDOW, NEW_URL);
|
||||
|
||||
spyOn($, 'postWithPrefix').andCallFake(
|
||||
function (url, data, callback) {
|
||||
callback({
|
||||
input_fields: {}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
lti = new window.LTI('.lti-wrapper');
|
||||
|
||||
spyOn(lti, 'submitFormHandler').andCallThrough();
|
||||
lti.submitFormHandler.reset();
|
||||
|
||||
spyOn(lti, 'handleAjaxUpdateSignature');
|
||||
});
|
||||
|
||||
it(
|
||||
'"Open in new page" clicked twice, signature requested once',
|
||||
function () {
|
||||
lti.newWindowBtnEl.click();
|
||||
lti.newWindowBtnEl.click();
|
||||
|
||||
expect(lti.submitFormHandler).toHaveBeenCalled();
|
||||
expect(lti.submitFormHandler.callCount).toBe(2);
|
||||
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith(
|
||||
lti.ajaxUrl + '/regenerate_signature',
|
||||
{},
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
expect(lti.disableOpenNewWindowBtn).toBe(true);
|
||||
|
||||
expect(lti.handleAjaxUpdateSignature)
|
||||
.toHaveBeenCalledWith({
|
||||
input_fields: {}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
afterEach(function () {
|
||||
lti = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleAjaxUpdateSignature method', function () {
|
||||
var lti, oldInputFields, newInputFields,
|
||||
AjaxCallbackData = {};
|
||||
|
||||
function fakePostWithPrefix(url, data, callback) {
|
||||
return callback(AjaxCallbackData);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
oldInputFields = {
|
||||
oauth_nonce: '28347958723982798572',
|
||||
oauth_timestamp: '2389479832',
|
||||
oauth_signature: '89ru3289r3ry283y3r82ryr38yr'
|
||||
};
|
||||
|
||||
newInputFields = {
|
||||
oauth_nonce: 'ru3902ru239ru',
|
||||
oauth_timestamp: '24ru309rur39r8u',
|
||||
oauth_signature: '08923ru3082u2rur'
|
||||
};
|
||||
|
||||
AjaxCallbackData.error = 0;
|
||||
AjaxCallbackData.input_fields = newInputFields;
|
||||
|
||||
loadFixtures('lti.html');
|
||||
setUpLtiElement($('.lti-wrapper'), IN_NEW_WINDOW, NEW_URL);
|
||||
|
||||
spyOn($, 'postWithPrefix').andCallFake(fakePostWithPrefix);
|
||||
|
||||
lti = new window.LTI('.lti-wrapper');
|
||||
|
||||
spyOn(lti, 'submitFormHandler').andCallThrough();
|
||||
spyOn(lti, 'handleAjaxUpdateSignature').andCallThrough();
|
||||
spyOn(lti.formEl, 'submit');
|
||||
spyOn(window.console, 'log').andCallThrough();
|
||||
|
||||
lti.submitFormHandler.reset();
|
||||
lti.handleAjaxUpdateSignature.reset();
|
||||
lti.formEl.submit.reset();
|
||||
window.console.log.reset();
|
||||
});
|
||||
|
||||
it('On second click form is updated, and submitted', function () {
|
||||
// Setup initial OAuth values in the form.
|
||||
lti.formEl.find("input[name='oauth_nonce']")
|
||||
.val(oldInputFields.oauth_nonce);
|
||||
lti.formEl.find("input[name='oauth_timestamp']")
|
||||
.val(oldInputFields.oauth_timestamp);
|
||||
lti.formEl.find("input[name='oauth_signature']")
|
||||
.val(oldInputFields.oauth_signature);
|
||||
|
||||
// First click. Signature is new. Should just submit the form.
|
||||
lti.newWindowBtnEl.click();
|
||||
|
||||
// Initial OAuth values should not have changed.
|
||||
expect(lti.formEl.find("input[name='oauth_nonce']").val())
|
||||
.toBe(oldInputFields.oauth_nonce);
|
||||
expect(lti.formEl.find("input[name='oauth_timestamp']").val())
|
||||
.toBe(oldInputFields.oauth_timestamp);
|
||||
expect(lti.formEl.find("input[name='oauth_signature']").val())
|
||||
.toBe(oldInputFields.oauth_signature);
|
||||
|
||||
expect(lti.submitFormHandler).toHaveBeenCalled();
|
||||
expect(lti.submitFormHandler.callCount).toBe(1);
|
||||
|
||||
expect(lti.handleAjaxUpdateSignature).not.toHaveBeenCalled();
|
||||
expect(lti.handleAjaxUpdateSignature.callCount).toBe(0);
|
||||
|
||||
expect(lti.formEl.submit).toHaveBeenCalled();
|
||||
expect(lti.formEl.submit.callCount).toBe(1);
|
||||
|
||||
lti.submitFormHandler.reset();
|
||||
lti.handleAjaxUpdateSignature.reset();
|
||||
lti.formEl.submit.reset();
|
||||
|
||||
// Second click. Signature is old. Should request for a new
|
||||
// signature, and then submit the form.
|
||||
lti.newWindowBtnEl.click();
|
||||
|
||||
expect(lti.submitFormHandler).toHaveBeenCalled();
|
||||
expect(lti.submitFormHandler.callCount).toBe(2);
|
||||
|
||||
expect(lti.handleAjaxUpdateSignature).toHaveBeenCalled();
|
||||
expect(lti.handleAjaxUpdateSignature.callCount).toBe(1);
|
||||
|
||||
expect(lti.formEl.submit).toHaveBeenCalled();
|
||||
expect(lti.formEl.submit.callCount).toBe(1);
|
||||
|
||||
expect(lti.disableOpenNewWindowBtn).toBe(false);
|
||||
|
||||
// The new OAuth values should be in the form.
|
||||
expect(lti.formEl.find("input[name='oauth_nonce']").val())
|
||||
.toBe(newInputFields.oauth_nonce);
|
||||
expect(lti.formEl.find("input[name='oauth_timestamp']").val())
|
||||
.toBe(newInputFields.oauth_timestamp);
|
||||
expect(lti.formEl.find("input[name='oauth_signature']").val())
|
||||
.toBe(newInputFields.oauth_signature);
|
||||
});
|
||||
|
||||
it('invalid response for new OAuth signature', function () {
|
||||
AjaxCallbackData.input_fields = 0;
|
||||
AjaxCallbackData.error = 'error';
|
||||
|
||||
lti.newWindowBtnEl.click();
|
||||
|
||||
lti.submitFormHandler.reset();
|
||||
lti.handleAjaxUpdateSignature.reset();
|
||||
window.console.log.reset();
|
||||
lti.formEl.submit.reset();
|
||||
|
||||
lti.newWindowBtnEl.click();
|
||||
|
||||
expect(lti.submitFormHandler).toHaveBeenCalled();
|
||||
expect(lti.submitFormHandler.callCount).toBe(1);
|
||||
|
||||
expect(lti.handleAjaxUpdateSignature).toHaveBeenCalled();
|
||||
expect(lti.handleAjaxUpdateSignature.callCount).toBe(1);
|
||||
|
||||
expect(window.console.log).toHaveBeenCalledWith(
|
||||
jasmine.any(String)
|
||||
);
|
||||
|
||||
expect(lti.formEl.submit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
lti = undefined;
|
||||
oldInputFields = undefined;
|
||||
newInputFields = undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setUpLtiElement(element, target, action) {
|
||||
var container, form;
|
||||
|
||||
container = element.find('.lti');
|
||||
form = container.find('.ltiLaunchForm');
|
||||
|
||||
if (target === IN_IFRAME) {
|
||||
container.data('open_in_a_new_page', 'false');
|
||||
form.attr('target', 'ltiLaunchFrame');
|
||||
}
|
||||
|
||||
form.attr('action', action);
|
||||
|
||||
// If we have a new proper action (non-default), we create either
|
||||
// a link that will submit the form, or an iframe that will contain
|
||||
// the answer of auto submitted form.
|
||||
if (action !== EMPTY_URL && action !== DEFAULT_URL) {
|
||||
if (target === IN_NEW_WINDOW) {
|
||||
$('<a />', {
|
||||
href: '#',
|
||||
class: 'link_lti_new_window'
|
||||
}).appendTo(container);
|
||||
} else {
|
||||
$('<iframe />', {
|
||||
name: 'ltiLaunchFrame',
|
||||
class: 'ltiLaunchFrame',
|
||||
src: ''
|
||||
}).appendTo(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
192
common/lib/xmodule/xmodule/js/src/lti/01_lti.js
Normal file
192
common/lib/xmodule/xmodule/js/src/lti/01_lti.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* File: lti.js
|
||||
*
|
||||
* Purpose: LTI module constructor. Given an LTI element, we process it.
|
||||
*
|
||||
*
|
||||
* Inside the element there is a form. If that form has a valid action
|
||||
* attribute, then we do one of:
|
||||
*
|
||||
* 1.) Submit the form. The results will be shown on the current page in an
|
||||
* iframe.
|
||||
* 2.) Attach a handler function to a link which will submit the form. The
|
||||
* results will be shown in a new window.
|
||||
*
|
||||
* The 'open_in_a_new_page' data attribute of the LTI element dictates which of
|
||||
* the two actions will be performed.
|
||||
*/
|
||||
|
||||
/*
|
||||
* So the thing to do when working on a motorcycle, as in any other task, is to
|
||||
* cultivate the peace of mind which does not separate one's self from one's
|
||||
* surroundings. When that is done successfully, then everything else follows
|
||||
* naturally. Peace of mind produces right values, right values produce right
|
||||
* thoughts. Right thoughts produce right actions and right actions produce
|
||||
* work which will be a material reflection for others to see of the serenity
|
||||
* at the center of it all.
|
||||
*
|
||||
* ~ Robert M. Pirsig
|
||||
*/
|
||||
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
// JavaScript LTI XModule
|
||||
define(
|
||||
'lti/01_lti.js',
|
||||
[],
|
||||
function () {
|
||||
|
||||
var LTI = LTIConstructor;
|
||||
|
||||
LTI.prototype = {
|
||||
submitFormHandler: submitFormHandler,
|
||||
getNewSignature: getNewSignature,
|
||||
handleAjaxUpdateSignature: handleAjaxUpdateSignature
|
||||
};
|
||||
|
||||
return LTI;
|
||||
|
||||
// JavaScript LTI XModule constructor
|
||||
function LTIConstructor(element) {
|
||||
var _this = this;
|
||||
|
||||
// In cms (Studio) the element is already a jQuery object. In lms it is
|
||||
// a DOM object.
|
||||
//
|
||||
// To make sure that there is no error, we pass it through the $()
|
||||
// function. This will make it a jQuery object if it isn't already so.
|
||||
this.el = $(element);
|
||||
|
||||
this.formEl = this.el.find('.ltiLaunchForm');
|
||||
this.formAction = this.formEl.attr('action');
|
||||
|
||||
// If action is empty string, or action is the default URL that should
|
||||
// not cause a form submit.
|
||||
if (!this.formAction || this.formAction === 'http://www.example.com') {
|
||||
|
||||
// Nothing to do - no valid action provided. Error message will be
|
||||
// displaced in browser (HTML).
|
||||
return;
|
||||
}
|
||||
|
||||
this.ltiEl = this.el.find('.lti');
|
||||
|
||||
// We want a Boolean 'true' or 'false'. First we will retrieve the data
|
||||
// attribute.
|
||||
this.openInANewPage = this.ltiEl.data('open_in_a_new_page');
|
||||
// Then we will parse it via native JSON.parse().
|
||||
this.openInANewPage = JSON.parse(this.openInANewPage);
|
||||
|
||||
// The URL where we can request for a new OAuth signature for form
|
||||
// submission to the LTI provider.
|
||||
this.ajaxUrl = this.ltiEl.data('ajax_url');
|
||||
|
||||
// The OAuth signature can only be used once (because of timestamp
|
||||
// and nonce). This will be reset each time the form is submitted so
|
||||
// that we know to fetch a new OAuth signature on subsequent form
|
||||
// submit.
|
||||
this.signatureIsNew = true;
|
||||
|
||||
// If the Form's action attribute is set (i.e. we can perform a normal
|
||||
// submit), then we (depending on instance settings) submit the form
|
||||
// when user will click on a link, or submit the form immediately.
|
||||
if (this.openInANewPage === true) {
|
||||
// From the start, the button is enabled.
|
||||
this.disableOpenNewWindowBtn = false;
|
||||
|
||||
this.newWindowBtnEl = this.el.find('.link_lti_new_window')
|
||||
.on(
|
||||
'click',
|
||||
function () {
|
||||
// Don't allow clicking repeatedly on this button
|
||||
// if we are waiting for an AJAX response (with new
|
||||
// OAuth signature).
|
||||
if (_this.disableOpenNewWindowBtn === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
return _this.submitFormHandler();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// At this stage the form exists on the page and has a valid
|
||||
// action. We are safe to submit it, even if `openInANewPage` is
|
||||
// set to some weird value.
|
||||
this.submitFormHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// The form submit handler. Before the form is submitted, we must check if
|
||||
// the OAuth signature is new (valid). If it is not new, block form
|
||||
// submission and request for a signature. After a new signature is
|
||||
// fetched, the form will be submitted.
|
||||
function submitFormHandler() {
|
||||
if (this.signatureIsNew) {
|
||||
// Continue with submitting the form.
|
||||
this.formEl.submit();
|
||||
|
||||
// If the OAuth signature is new, mark it as old.
|
||||
this.signatureIsNew = false;
|
||||
|
||||
// If we have an "Open LTI in a new window" button.
|
||||
if (this.newWindowBtnEl) {
|
||||
// Enable clicking on the button again.
|
||||
this.disableOpenNewWindowBtn = false;
|
||||
}
|
||||
} else {
|
||||
// The OAuth signature is old. Request for a new OAuth signature.
|
||||
//
|
||||
// Don't submit the form. It will be submitted once a new OAuth
|
||||
// signature is received.
|
||||
this.getNewSignature();
|
||||
}
|
||||
}
|
||||
|
||||
// Request form the server a new OAuth signature.
|
||||
function getNewSignature() {
|
||||
var _this = this;
|
||||
|
||||
// If we have an "Open LTI in a new window" button.
|
||||
if (this.newWindowBtnEl) {
|
||||
// Make sure that while we are waiting for a new signature, the
|
||||
// user can't click on the "Open LTI in a new window" button
|
||||
// repeatedly.
|
||||
this.disableOpenNewWindowBtn = true;
|
||||
}
|
||||
|
||||
$.postWithPrefix(
|
||||
this.ajaxUrl + '/regenerate_signature',
|
||||
{},
|
||||
function (response) {
|
||||
return _this.handleAjaxUpdateSignature(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// When a new OAuth signature is received, and if the data received back is
|
||||
// OK, update the form, and submit it.
|
||||
function handleAjaxUpdateSignature(response) {
|
||||
var _this = this;
|
||||
|
||||
// If the response is valid, and contains expected data.
|
||||
if ($.isPlainObject(response.input_fields)) {
|
||||
// We received a new OAuth signature.
|
||||
this.signatureIsNew = true;
|
||||
|
||||
// Update the form fields with new data, and new OAuth
|
||||
// signature.
|
||||
$.each(response.input_fields, function (name, value) {
|
||||
var inputEl = _this.formEl.find("input[name='" + name + "']");
|
||||
|
||||
inputEl.val(value);
|
||||
});
|
||||
|
||||
// Submit the form.
|
||||
this.submitFormHandler();
|
||||
} else {
|
||||
console.log('[LTI info]: ' + response.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
57
common/lib/xmodule/xmodule/js/src/lti/02_main.js
Normal file
57
common/lib/xmodule/xmodule/js/src/lti/02_main.js
Normal file
@@ -0,0 +1,57 @@
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
// In the case when the LTI constructor will be called before
|
||||
// RequireJS finishes loading all of the LTI dependencies, we will have
|
||||
// a mock function that will collect all the elements that must be
|
||||
// initialized as LTI elements.
|
||||
//
|
||||
// Once RequireJS will load all of the necessary dependencies, main code
|
||||
// will invoke the mock function with the second parameter set to truthy value.
|
||||
// This will trigger the actual LTI constructor on all elements that
|
||||
// are stored in a temporary list.
|
||||
window.LTI = (function () {
|
||||
// Temporary storage place for elements that must be initialized as LTI
|
||||
// elements.
|
||||
var tempCallStack = [];
|
||||
|
||||
return function (element, processTempCallStack) {
|
||||
// If mock function was called with second parameter set to truthy
|
||||
// value, we invoke the real `window.LTI` on all the stored elements
|
||||
// so far.
|
||||
if (processTempCallStack) {
|
||||
$.each(tempCallStack, function (index, element) {
|
||||
// By now, `window.LTI` is the real constructor.
|
||||
window.LTI(element);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If normal call to `window.LTI` constructor, store the element
|
||||
// for later initializing.
|
||||
tempCallStack.push(element);
|
||||
|
||||
// Real LTI constructor returns `undefined`. The mock constructor will
|
||||
// return the same value. Making this explicit.
|
||||
return undefined;
|
||||
};
|
||||
}());
|
||||
|
||||
// Main module.
|
||||
require(
|
||||
[
|
||||
'lti/01_lti.js'
|
||||
],
|
||||
function (
|
||||
LTIConstructor
|
||||
) {
|
||||
var oldLTI = window.LTI;
|
||||
|
||||
window.LTI = LTIConstructor;
|
||||
|
||||
// Invoke the mock LTI constructor so that the elements stored within
|
||||
// it can be processed by the real `window.LTI` constructor.
|
||||
oldLTI(null, true);
|
||||
});
|
||||
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* File: lti.js
|
||||
*
|
||||
* Purpose: LTI module constructor. Given an LTI element, we process it.
|
||||
*
|
||||
*
|
||||
* Inside the element there is a form. If that form has a valid action
|
||||
* attribute, then we do one of:
|
||||
*
|
||||
* 1.) Submit the form. The results will be shown on the current page in an
|
||||
* iframe.
|
||||
* 2.) Attach a handler function to a link which will submit the form. The
|
||||
* results will be shown in a new window.
|
||||
*
|
||||
* The 'open_in_a_new_page' data attribute of the LTI element dictates which of
|
||||
* the two actions will be performed.
|
||||
*/
|
||||
|
||||
/*
|
||||
* So the thing to do when working on a motorcycle, as in any other task, is to
|
||||
* cultivate the peace of mind which does not separate one's self from one's
|
||||
* surroundings. When that is done successfully, then everything else follows
|
||||
* naturally. Peace of mind produces right values, right values produce right
|
||||
* thoughts. Right thoughts produce right actions and right actions produce
|
||||
* work which will be a material reflection for others to see of the serenity
|
||||
* at the center of it all.
|
||||
*
|
||||
* ~ Robert M. Pirsig
|
||||
*/
|
||||
|
||||
window.LTI = (function () {
|
||||
// Function initialize(element)
|
||||
//
|
||||
// Initialize the LTI module.
|
||||
//
|
||||
// @param element DOM element, or jQuery element object.
|
||||
//
|
||||
// @return undefined
|
||||
function initialize(element) {
|
||||
var form, openInANewPage, formAction;
|
||||
|
||||
// In cms (Studio) the element is already a jQuery object. In lms it is
|
||||
// a DOM object.
|
||||
//
|
||||
// To make sure that there is no error, we pass it through the $()
|
||||
// function. This will make it a jQuery object if it isn't already so.
|
||||
element = $(element);
|
||||
|
||||
form = element.find('.ltiLaunchForm');
|
||||
formAction = form.attr('action');
|
||||
|
||||
// If action is empty string, or action is the default URL that should
|
||||
// not cause a form submit.
|
||||
if (!formAction || formAction === 'http://www.example.com') {
|
||||
|
||||
// Nothing to do - no valid action provided. Error message will be
|
||||
// displaced in browser (HTML).
|
||||
return;
|
||||
}
|
||||
|
||||
// We want a Boolean 'true' or 'false'. First we will retrieve the data
|
||||
// attribute, and then we will parse it via native JSON.parse().
|
||||
openInANewPage = element.find('.lti').data('open_in_a_new_page');
|
||||
openInANewPage = JSON.parse(openInANewPage);
|
||||
|
||||
// If the Form's action attribute is set (i.e. we can perform a normal
|
||||
// submit), then we (depending on instance settings) submit the form
|
||||
// when user will click on a link, or submit the form immediately.
|
||||
if (openInANewPage === true) {
|
||||
element.find('.link_lti_new_window').on('click', function () {
|
||||
form.submit();
|
||||
});
|
||||
} else {
|
||||
// At this stage the form exists on the page and has a valid
|
||||
// action. We are safe to submit it, even if `openInANewPage` is
|
||||
// set to some weird value.
|
||||
//
|
||||
// Best case scenario is that `openInANewPage` is set to `true`.
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
return initialize;
|
||||
}());
|
||||
@@ -42,6 +42,7 @@ import hashlib
|
||||
import base64
|
||||
import urllib
|
||||
import textwrap
|
||||
import json
|
||||
from lxml import etree
|
||||
from webob import Response
|
||||
import mock
|
||||
@@ -179,15 +180,16 @@ class LTIModule(LTIFields, XModule):
|
||||
Otherwise error message from LTI provider is generated.
|
||||
"""
|
||||
|
||||
js = {'js': [resource_string(__name__, 'js/src/lti/lti.js')]}
|
||||
js = {
|
||||
'js': [
|
||||
resource_string(__name__, 'js/src/lti/01_lti.js'),
|
||||
resource_string(__name__, 'js/src/lti/02_main.js')
|
||||
]
|
||||
}
|
||||
css = {'scss': [resource_string(__name__, 'css/lti/lti.scss')]}
|
||||
js_module_name = "LTI"
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Renders parameters to template.
|
||||
"""
|
||||
|
||||
def get_input_fields(self):
|
||||
# LTI provides a list of default parameters that might be passed as
|
||||
# part of the POST data. These parameters should not be prefixed.
|
||||
# Likewise, The creator of an LTI link can add custom key/value parameters
|
||||
@@ -245,13 +247,19 @@ class LTIModule(LTIFields, XModule):
|
||||
|
||||
custom_parameters[unicode(param_name)] = unicode(param_value)
|
||||
|
||||
input_fields = self.oauth_params(
|
||||
return self.oauth_params(
|
||||
custom_parameters,
|
||||
client_key,
|
||||
client_secret,
|
||||
)
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Renders parameters to template.
|
||||
"""
|
||||
|
||||
context = {
|
||||
'input_fields': input_fields,
|
||||
'input_fields': self.get_input_fields(),
|
||||
|
||||
# These parameters do not participate in OAuth signing.
|
||||
'launch_url': self.launch_url.strip(),
|
||||
@@ -259,10 +267,26 @@ class LTIModule(LTIFields, XModule):
|
||||
'element_class': self.category,
|
||||
'open_in_a_new_page': self.open_in_a_new_page,
|
||||
'display_name': self.display_name,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
}
|
||||
|
||||
return self.system.render_template('lti.html', context)
|
||||
|
||||
def handle_ajax(self, dispatch, __):
|
||||
"""
|
||||
Ajax handler.
|
||||
|
||||
Args:
|
||||
dispatch: string request slug
|
||||
|
||||
Returns:
|
||||
json string
|
||||
"""
|
||||
if dispatch == 'regenerate_signature':
|
||||
return json.dumps({ 'input_fields': self.get_input_fields() })
|
||||
else: # return error message
|
||||
return json.dumps({ 'error': '[handle_ajax]: Unknown Command!' })
|
||||
|
||||
def get_user_id(self):
|
||||
user_id = self.runtime.anonymous_student_id
|
||||
assert user_id is not None
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from mock import Mock, patch, PropertyMock
|
||||
import textwrap
|
||||
import json
|
||||
from lxml import etree
|
||||
from webob.request import Request
|
||||
from copy import copy
|
||||
@@ -250,6 +251,28 @@ class LTIModuleTest(LogicTest):
|
||||
def test_client_key_secret(self):
|
||||
pass
|
||||
|
||||
def test_handle_ajax(self):
|
||||
dispatch = 'regenerate_signature'
|
||||
data = ''
|
||||
self.xmodule.get_input_fields = Mock(return_value={'test_input_field_key': 'test_input_field_value'})
|
||||
json_dump = self.xmodule.handle_ajax(dispatch, data)
|
||||
expected_json_dump = '{"input_fields": {"test_input_field_key": "test_input_field_value"}}'
|
||||
self.assertEqual(
|
||||
json.loads(json_dump),
|
||||
json.loads(expected_json_dump)
|
||||
)
|
||||
|
||||
def test_handle_ajax_bad_dispatch(self):
|
||||
dispatch = 'bad_dispatch'
|
||||
data = ''
|
||||
self.xmodule.get_input_fields = Mock(return_value={'test_input_field_key': 'test_input_field_value'})
|
||||
json_dump = self.xmodule.handle_ajax(dispatch, data)
|
||||
expected_json_dump = '{"error": "[handle_ajax]: Unknown Command!"}'
|
||||
self.assertEqual(
|
||||
json.loads(json_dump),
|
||||
json.loads(expected_json_dump)
|
||||
)
|
||||
|
||||
def test_max_score(self):
|
||||
self.xmodule.weight = 100.0
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ Feature: LMS.LTI component
|
||||
Given the course has correct LTI credentials
|
||||
And the course has an LTI component with correct fields:
|
||||
| open_in_a_new_page | weight | is_graded | has_score |
|
||||
| False | 10 | True | True |
|
||||
| False | 10 | True | True |
|
||||
And I submit answer to LTI question
|
||||
And I click on the "Progress" tab
|
||||
Then I see text "Problem Scores: 5/10"
|
||||
|
||||
@@ -93,6 +93,7 @@ class TestLTI(BaseTestXmodule):
|
||||
'element_id': self.item_module.location.html_id(),
|
||||
'launch_url': 'http://www.example.com', # default value
|
||||
'open_in_a_new_page': True,
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
id="${element_id}"
|
||||
class="${element_class}"
|
||||
data-open_in_a_new_page="${json.dumps(open_in_a_new_page)}"
|
||||
data-ajax_url="${ajax_url}"
|
||||
>
|
||||
|
||||
## This form will be hidden.
|
||||
|
||||
Reference in New Issue
Block a user