Commit 9072b9cc by Valera Rozuvan

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.
parent 0684f6a4
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 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: 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 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 reset state for local instances of the code, unless you manually rename your
mitx.db file to edx.db. mitx.db file to edx.db.
......
<div class="lti-wrapper"> <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 <form
action="" action=""
......
/**
* 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);
});
});
});
});
}());
/**
* 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);
}
}
}
}());
/**
* 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));
(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));
/**
* 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 ...@@ -42,6 +42,7 @@ import hashlib
import base64 import base64
import urllib import urllib
import textwrap import textwrap
import json
from lxml import etree from lxml import etree
from webob import Response from webob import Response
import mock import mock
...@@ -179,15 +180,16 @@ class LTIModule(LTIFields, XModule): ...@@ -179,15 +180,16 @@ class LTIModule(LTIFields, XModule):
Otherwise error message from LTI provider is generated. 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')]} css = {'scss': [resource_string(__name__, 'css/lti/lti.scss')]}
js_module_name = "LTI" js_module_name = "LTI"
def get_html(self): def get_input_fields(self):
"""
Renders parameters to template.
"""
# LTI provides a list of default parameters that might be passed as # LTI provides a list of default parameters that might be passed as
# part of the POST data. These parameters should not be prefixed. # part of the POST data. These parameters should not be prefixed.
# Likewise, The creator of an LTI link can add custom key/value parameters # Likewise, The creator of an LTI link can add custom key/value parameters
...@@ -245,13 +247,19 @@ class LTIModule(LTIFields, XModule): ...@@ -245,13 +247,19 @@ class LTIModule(LTIFields, XModule):
custom_parameters[unicode(param_name)] = unicode(param_value) custom_parameters[unicode(param_name)] = unicode(param_value)
input_fields = self.oauth_params( return self.oauth_params(
custom_parameters, custom_parameters,
client_key, client_key,
client_secret, client_secret,
) )
def get_html(self):
"""
Renders parameters to template.
"""
context = { context = {
'input_fields': input_fields, 'input_fields': self.get_input_fields(),
# These parameters do not participate in OAuth signing. # These parameters do not participate in OAuth signing.
'launch_url': self.launch_url.strip(), 'launch_url': self.launch_url.strip(),
...@@ -259,10 +267,26 @@ class LTIModule(LTIFields, XModule): ...@@ -259,10 +267,26 @@ class LTIModule(LTIFields, XModule):
'element_class': self.category, 'element_class': self.category,
'open_in_a_new_page': self.open_in_a_new_page, 'open_in_a_new_page': self.open_in_a_new_page,
'display_name': self.display_name, 'display_name': self.display_name,
'ajax_url': self.system.ajax_url,
} }
return self.system.render_template('lti.html', context) 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): def get_user_id(self):
user_id = self.runtime.anonymous_student_id user_id = self.runtime.anonymous_student_id
assert user_id is not None assert user_id is not None
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from mock import Mock, patch, PropertyMock from mock import Mock, patch, PropertyMock
import textwrap import textwrap
import json
from lxml import etree from lxml import etree
from webob.request import Request from webob.request import Request
from copy import copy from copy import copy
...@@ -250,6 +251,28 @@ class LTIModuleTest(LogicTest): ...@@ -250,6 +251,28 @@ class LTIModuleTest(LogicTest):
def test_client_key_secret(self): def test_client_key_secret(self):
pass 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): def test_max_score(self):
self.xmodule.weight = 100.0 self.xmodule.weight = 100.0
......
...@@ -45,7 +45,7 @@ Feature: LMS.LTI component ...@@ -45,7 +45,7 @@ Feature: LMS.LTI component
Given the course has correct LTI credentials Given the course has correct LTI credentials
And the course has an LTI component with correct fields: And the course has an LTI component with correct fields:
| open_in_a_new_page | weight | is_graded | has_score | | 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 submit answer to LTI question
And I click on the "Progress" tab And I click on the "Progress" tab
Then I see text "Problem Scores: 5/10" Then I see text "Problem Scores: 5/10"
......
...@@ -93,6 +93,7 @@ class TestLTI(BaseTestXmodule): ...@@ -93,6 +93,7 @@ class TestLTI(BaseTestXmodule):
'element_id': self.item_module.location.html_id(), 'element_id': self.item_module.location.html_id(),
'launch_url': 'http://www.example.com', # default value 'launch_url': 'http://www.example.com', # default value
'open_in_a_new_page': True, 'open_in_a_new_page': True,
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
} }
self.assertEqual( self.assertEqual(
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
id="${element_id}" id="${element_id}"
class="${element_class}" class="${element_class}"
data-open_in_a_new_page="${json.dumps(open_in_a_new_page)}" data-open_in_a_new_page="${json.dumps(open_in_a_new_page)}"
data-ajax_url="${ajax_url}"
> >
## This form will be hidden. ## This form will be hidden.
......
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