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,
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.
......
<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=""
......
/**
* 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: 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
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.
......
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