Merge pull request #1891 from edx/anton/fix-lti-dnd
LTI: fix reordering bug in Studio
This commit is contained in:
@@ -30,6 +30,5 @@ div.lti {
|
||||
height: 800px;
|
||||
display: block;
|
||||
border: 0px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
@@ -1,192 +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
|
||||
*/
|
||||
|
||||
(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));
|
||||
@@ -1,57 +0,0 @@
|
||||
(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));
|
||||
@@ -180,14 +180,7 @@ class LTIModule(LTIFields, XModule):
|
||||
Otherwise error message from LTI provider is generated.
|
||||
"""
|
||||
|
||||
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_input_fields(self):
|
||||
# LTI provides a list of default parameters that might be passed as
|
||||
@@ -253,12 +246,11 @@ class LTIModule(LTIFields, XModule):
|
||||
client_secret,
|
||||
)
|
||||
|
||||
def get_html(self):
|
||||
def get_context(self):
|
||||
"""
|
||||
Renders parameters to template.
|
||||
Returns a context.
|
||||
"""
|
||||
|
||||
context = {
|
||||
return {
|
||||
'input_fields': self.get_input_fields(),
|
||||
|
||||
# These parameters do not participate in OAuth signing.
|
||||
@@ -267,12 +259,27 @@ 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,
|
||||
'form_url': self.get_form_path(),
|
||||
}
|
||||
|
||||
return self.system.render_template('lti.html', context)
|
||||
|
||||
def handle_ajax(self, dispatch, __):
|
||||
def get_form_path(self):
|
||||
return self.runtime.handler_url(self, 'preview_handler').rstrip('/?')
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Renders parameters to template.
|
||||
"""
|
||||
return self.system.render_template('lti.html', self.get_context())
|
||||
|
||||
def get_form(self):
|
||||
"""
|
||||
Renders parameters to form template.
|
||||
"""
|
||||
return self.system.render_template('lti_form.html', self.get_context())
|
||||
|
||||
@XBlock.handler
|
||||
def preview_handler(self, request, dispatch):
|
||||
"""
|
||||
Ajax handler.
|
||||
|
||||
@@ -282,10 +289,7 @@ class LTIModule(LTIFields, XModule):
|
||||
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!' })
|
||||
return Response(self.get_form(), content_type='text/html')
|
||||
|
||||
def get_user_id(self):
|
||||
user_id = self.runtime.anonymous_student_id
|
||||
@@ -614,3 +618,4 @@ class LTIDescriptor(LTIFields, MetadataOnlyEditingDescriptor, EmptyDataRawDescri
|
||||
"""
|
||||
module_class = LTIModule
|
||||
grade_handler = module_attr('grade_handler')
|
||||
preview_handler = module_attr('preview_handler')
|
||||
|
||||
@@ -229,6 +229,12 @@ class LTIModuleTest(LogicTest):
|
||||
real_outcome_service_url = self.xmodule.get_outcome_service_url()
|
||||
self.assertEqual(real_outcome_service_url, expected_outcome_service_url)
|
||||
|
||||
def test_get_form_path(self):
|
||||
expected_form_path = self.xmodule.runtime.handler_url(self.xmodule, 'preview_handler').rstrip('/?')
|
||||
|
||||
real_form_path = self.xmodule.get_form_path()
|
||||
self.assertEqual(real_form_path, expected_form_path)
|
||||
|
||||
def test_resource_link_id(self):
|
||||
with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id:
|
||||
mock_id.return_value = self.module_id
|
||||
@@ -251,28 +257,6 @@ 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
|
||||
|
||||
|
||||
@@ -13,22 +13,22 @@ from courseware.tests.factories import InstructorFactory
|
||||
@step('I view the LTI and error is shown$')
|
||||
def lti_is_not_rendered(_step):
|
||||
# error is shown
|
||||
assert world.is_css_present('.error_message')
|
||||
assert world.is_css_present('.error_message', wait_time=0)
|
||||
|
||||
# iframe is not presented
|
||||
assert not world.is_css_present('iframe')
|
||||
assert not world.is_css_present('iframe', wait_time=0)
|
||||
|
||||
# link is not presented
|
||||
assert not world.is_css_present('.link_lti_new_window')
|
||||
assert not world.is_css_present('.link_lti_new_window', wait_time=0)
|
||||
|
||||
|
||||
def check_lti_iframe_content(text):
|
||||
#inside iframe test content is presented
|
||||
location = world.scenario_dict['LTI'].location.html_id()
|
||||
iframe_name = 'ltiLaunchFrame-' + location
|
||||
iframe_name = 'ltiFrame-' + location
|
||||
with world.browser.get_iframe(iframe_name) as iframe:
|
||||
# iframe does not contain functions from terrain/ui_helpers.py
|
||||
assert iframe.is_element_present_by_css('.result', wait_time=5)
|
||||
assert iframe.is_element_present_by_css('.result', wait_time=0)
|
||||
assert (text == world.retry_on_exception(
|
||||
lambda: iframe.find_by_css('.result')[0].text,
|
||||
max_attempts=5
|
||||
@@ -38,18 +38,18 @@ def check_lti_iframe_content(text):
|
||||
@step('I view the LTI and it is rendered in (.*)$')
|
||||
def lti_is_rendered(_step, rendered_in):
|
||||
if rendered_in.strip() == 'iframe':
|
||||
assert world.is_css_present('iframe')
|
||||
assert not world.is_css_present('.link_lti_new_window')
|
||||
assert not world.is_css_present('.error_message')
|
||||
assert world.is_css_present('iframe', wait_time=2)
|
||||
assert not world.is_css_present('.link_lti_new_window', wait_time=0)
|
||||
assert not world.is_css_present('.error_message', wait_time=0)
|
||||
|
||||
# iframe is visible
|
||||
assert world.css_visible('iframe')
|
||||
check_lti_iframe_content("This is LTI tool. Success.")
|
||||
|
||||
elif rendered_in.strip() == 'new page':
|
||||
assert not world.is_css_present('iframe')
|
||||
assert world.is_css_present('.link_lti_new_window')
|
||||
assert not world.is_css_present('.error_message')
|
||||
assert not world.is_css_present('iframe', wait_time=2)
|
||||
assert world.is_css_present('.link_lti_new_window', wait_time=0)
|
||||
assert not world.is_css_present('.error_message', wait_time=0)
|
||||
check_lti_popup()
|
||||
else: # incorrent rendered_in parameter
|
||||
assert False
|
||||
@@ -57,9 +57,9 @@ def lti_is_rendered(_step, rendered_in):
|
||||
|
||||
@step('I view the LTI but incorrect_signature warning is rendered$')
|
||||
def incorrect_lti_is_rendered(_step):
|
||||
assert world.is_css_present('iframe')
|
||||
assert not world.is_css_present('.link_lti_new_window')
|
||||
assert not world.is_css_present('.error_message')
|
||||
assert world.is_css_present('iframe', wait_time=2)
|
||||
assert not world.is_css_present('.link_lti_new_window', wait_time=0)
|
||||
assert not world.is_css_present('.error_message', wait_time=0)
|
||||
#inside iframe test content is presented
|
||||
check_lti_iframe_content("Wrong LTI signature")
|
||||
|
||||
@@ -234,10 +234,11 @@ def check_progress(_step, text):
|
||||
@step('I see graph with total progress "([^"]*)"$')
|
||||
def see_graph(_step, progress):
|
||||
SELECTOR = 'grade-detail-graph'
|
||||
node = world.browser.find_by_xpath('//div[@id="{parent}"]//div[text()="{progress}"]'.format(
|
||||
XPATH = '//div[@id="{parent}"]//div[text()="{progress}"]'.format(
|
||||
parent=SELECTOR,
|
||||
progress=progress,
|
||||
))
|
||||
)
|
||||
node = world.browser.find_by_xpath(XPATH)
|
||||
|
||||
assert node
|
||||
|
||||
@@ -259,7 +260,7 @@ def see_value_in_the_gradebook(_step, label, text):
|
||||
@step('I submit answer to LTI question$')
|
||||
def click_grade(_step):
|
||||
location = world.scenario_dict['LTI'].location.html_id()
|
||||
iframe_name = 'ltiLaunchFrame-' + location
|
||||
iframe_name = 'ltiFrame-' + location
|
||||
with world.browser.get_iframe(iframe_name) as iframe:
|
||||
iframe.find_by_name('submit-button').first.click()
|
||||
assert iframe.is_text_present('LTI consumer (edX) responded with XML content')
|
||||
|
||||
@@ -5,8 +5,6 @@ from . import BaseTestXmodule
|
||||
from collections import OrderedDict
|
||||
import mock
|
||||
import urllib
|
||||
from xmodule.lti_module import LTIModule
|
||||
from mock import Mock
|
||||
|
||||
|
||||
class TestLTI(BaseTestXmodule):
|
||||
@@ -85,7 +83,6 @@ class TestLTI(BaseTestXmodule):
|
||||
Makes sure that all parameters extracted.
|
||||
"""
|
||||
generated_context = self.item_module.render('student_view').content
|
||||
|
||||
expected_context = {
|
||||
'display_name': self.item_module.display_name,
|
||||
'input_fields': self.correct_headers,
|
||||
@@ -93,7 +90,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,
|
||||
'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'preview_handler').rstrip('/?'),
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
|
||||
@@ -4,40 +4,15 @@
|
||||
<div
|
||||
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.
|
||||
## If open_in_a_new_page is false then, once available on the client, the
|
||||
## LTI module JavaScript will trigger a "submit" on the form, and the
|
||||
## result will be rendered to the below iFrame.
|
||||
## If open_in_a_new_page is true, then link will be shown, and by clicking
|
||||
## on it, LTI will pop up in new window.
|
||||
<form
|
||||
action="${launch_url}"
|
||||
name="ltiLaunchForm-${element_id}"
|
||||
class="ltiLaunchForm"
|
||||
method="post"
|
||||
target=${"_blank" if open_in_a_new_page else "ltiLaunchFrame-{0}".format(element_id)}
|
||||
encType="application/x-www-form-urlencoded"
|
||||
>
|
||||
|
||||
% for param_name, param_value in input_fields.items():
|
||||
<input name="${param_name}" value="${param_value}" />
|
||||
%endfor
|
||||
|
||||
<input type="submit" value="Press to Launch" />
|
||||
</form>
|
||||
|
||||
|
||||
% if launch_url and launch_url != 'http://www.example.com':
|
||||
% if open_in_a_new_page:
|
||||
<div class="wrapper-lti-link">
|
||||
<h3 class="title">
|
||||
${display_name} (${_('External resource')})
|
||||
</h3>
|
||||
<p class="lti-link external"><a href="#" class='link_lti_new_window'>
|
||||
<p class="lti-link external"><a target="_blank" class="link_lti_new_window" href="${form_url}">
|
||||
${_('View resource in a new window')}
|
||||
<i class="icon-external-link"></i>
|
||||
</a></p>
|
||||
@@ -45,9 +20,9 @@
|
||||
% else:
|
||||
## The result of the form submit will be rendered here.
|
||||
<iframe
|
||||
name="ltiLaunchFrame-${element_id}"
|
||||
class="ltiLaunchFrame"
|
||||
src=""
|
||||
name="ltiFrame-${element_id}"
|
||||
src="${form_url}"
|
||||
></iframe>
|
||||
% endif
|
||||
% else:
|
||||
|
||||
36
lms/templates/lti_form.html
Normal file
36
lms/templates/lti_form.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<%! import json %>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>LTI</title>
|
||||
</head>
|
||||
<body>
|
||||
## This form will be hidden.
|
||||
## LTI module JavaScript will trigger a "submit" on the form, and the
|
||||
## result will be rendered instead.
|
||||
<form
|
||||
id="lti-${element_id}"
|
||||
action="${launch_url}"
|
||||
method="post"
|
||||
encType="application/x-www-form-urlencoded"
|
||||
style="display:none;"
|
||||
>
|
||||
|
||||
% for param_name, param_value in input_fields.items():
|
||||
<input name="${param_name}" value="${param_value}" />
|
||||
%endfor
|
||||
|
||||
<input type="submit" value="Press to Launch" />
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
(function (d) {
|
||||
var element = d.getElementById("lti-${element_id}");
|
||||
if (element) {
|
||||
element.submit();
|
||||
}
|
||||
}(document));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user