', {
- href: '#',
- class: 'link_lti_new_window'
- }).appendTo(container);
-
- link = container.find('.link_lti_new_window');
- } else {
- $('
', {
- 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);
- });
- });
- });
- });
-}());
diff --git a/common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js b/common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js
new file mode 100644
index 0000000000..9a9369eac1
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js
@@ -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) {
+ $('
', {
+ href: '#',
+ class: 'link_lti_new_window'
+ }).appendTo(container);
+ } else {
+ $('
', {
+ name: 'ltiLaunchFrame',
+ class: 'ltiLaunchFrame',
+ src: ''
+ }).appendTo(container);
+ }
+ }
+ }
+}());
diff --git a/common/lib/xmodule/xmodule/js/src/lti/01_lti.js b/common/lib/xmodule/xmodule/js/src/lti/01_lti.js
new file mode 100644
index 0000000000..4803c05e78
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/lti/01_lti.js
@@ -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));
diff --git a/common/lib/xmodule/xmodule/js/src/lti/02_main.js b/common/lib/xmodule/xmodule/js/src/lti/02_main.js
new file mode 100644
index 0000000000..9a6dacd83b
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/lti/02_main.js
@@ -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));
diff --git a/common/lib/xmodule/xmodule/js/src/lti/lti.js b/common/lib/xmodule/xmodule/js/src/lti/lti.js
deleted file mode 100644
index 9454b0c346..0000000000
--- a/common/lib/xmodule/xmodule/js/src/lti/lti.js
+++ /dev/null
@@ -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;
-}());
diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py
index 9199bac6b0..087bf67770 100644
--- a/common/lib/xmodule/xmodule/lti_module.py
+++ b/common/lib/xmodule/xmodule/lti_module.py
@@ -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
diff --git a/common/lib/xmodule/xmodule/tests/test_lti_unit.py b/common/lib/xmodule/xmodule/tests/test_lti_unit.py
index bb04b20868..3dd6c011fd 100644
--- a/common/lib/xmodule/xmodule/tests/test_lti_unit.py
+++ b/common/lib/xmodule/xmodule/tests/test_lti_unit.py
@@ -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
diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature
index eb2da7436b..b9f03e7f02 100644
--- a/lms/djangoapps/courseware/features/lti.feature
+++ b/lms/djangoapps/courseware/features/lti.feature
@@ -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"
diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py
index 9a70d217cc..ab2a575a0e 100644
--- a/lms/djangoapps/courseware/tests/test_lti_integration.py
+++ b/lms/djangoapps/courseware/tests/test_lti_integration.py
@@ -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(
diff --git a/lms/templates/lti.html b/lms/templates/lti.html
index 7e6a3eac40..4a6ee870c7 100644
--- a/lms/templates/lti.html
+++ b/lms/templates/lti.html
@@ -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.