diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d6725181ac05..489abfcbd1ad 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,8 @@ the top. Include a label indicating the component affected.
Blades: Add possibility to use multiple LTI tools per page.
+Blades: LTI module can now load external content in a new window.
+
LMS: Disable data download buttons on the instructor dashboard for large courses
LMS: Ported bulk emailing to the beta instructor dashboard.
diff --git a/cms/static/sass/_mixins-inherited.scss b/cms/static/sass/_mixins-inherited.scss
new file mode 120000
index 000000000000..f64a7205612d
--- /dev/null
+++ b/cms/static/sass/_mixins-inherited.scss
@@ -0,0 +1 @@
+../../../common/static/sass/_mixins-inherited.scss
\ No newline at end of file
diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss
index 3bd13f3e5b7f..093d3a957af5 100644
--- a/cms/static/sass/_variables.scss
+++ b/cms/static/sass/_variables.scss
@@ -197,6 +197,10 @@ $lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
+
+//carryover from LMS for xmodules
+$sidebar-color: rgb(246, 246, 246);
+
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);
diff --git a/common/lib/xmodule/xmodule/css/lti/lti.scss b/common/lib/xmodule/xmodule/css/lti/lti.scss
index 97a8f62d5450..152963516c1d 100644
--- a/common/lib/xmodule/xmodule/css/lti/lti.scss
+++ b/common/lib/xmodule/xmodule/css/lti/lti.scss
@@ -2,8 +2,23 @@ div.lti {
// align center
margin: 0 auto;
- h3.error_message {
- display: block;
+ .wrapper-lti-link {
+ @include font-size(14);
+ position: relative;
+ background-color: $sidebar-color;
+ padding: ($baseline*1.8) ($baseline*1.5) ($baseline*1.1) $baseline;
+
+ .lti-link {
+ position: absolute;
+ top: ($baseline*1.8);
+ right: $baseline;
+
+ .link_lti_new_window {
+ @extend .gray-button;
+ @include font-size(13);
+ @include line-height(14);
+ }
+ }
}
form.ltiLaunchForm {
@@ -13,18 +28,8 @@ div.lti {
iframe.ltiLaunchFrame {
width: 100%;
height: 800px;
- display: none;
+ display: block;
border: 0px;
overflow-x: hidden;
}
-
- &.rendered {
- iframe.ltiLaunchFrame {
- display: block;
- }
-
- h3.error_message {
- display: none;
- }
- }
}
diff --git a/common/lib/xmodule/xmodule/js/fixtures/lti.html b/common/lib/xmodule/xmodule/js/fixtures/lti.html
index ae545970ce80..14bea7d7714d 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/lti.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/lti.html
@@ -1,36 +1,31 @@
-
+
diff --git a/common/lib/xmodule/xmodule/js/spec/lti/constructor.js b/common/lib/xmodule/xmodule/js/spec/lti/constructor.js
index f016d42d9d70..cda1d7988d7f 100644
--- a/common/lib/xmodule/xmodule/js/spec/lti/constructor.js
+++ b/common/lib/xmodule/xmodule/js/spec/lti/constructor.js
@@ -5,77 +5,158 @@
*
*
* The front-end part of the LTI module is really simple. If an action
- * is set for the hidden LTI form, then it is submited, and the results are
- * redirected to an iframe.
+ * 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 submited when the action is set (i.e.
- * not empty).
+ * 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 skilful in attack whose opponent does not know what
- * to defend; and he is skilful in defense whose opponent does not know what
+ * "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) {
+ $('
', {
+ 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('constructor', function () {
- describe('before settings were filled in', function () {
- var element, errorMessage, frame;
+ describe('initialize', function () {
+ describe(
+ 'open_in_a_new_page is "true", launch URL is empty',
+ function () {
- // This function will be executed before each of the it() specs
- // in this suite.
beforeEach(function () {
- loadFixtures('lti.html');
+ initialize(IN_NEW_WINDOW, EMPTY_URL);
+ });
+
+ it('form is not submitted', function () {
+ expect('submit').not.toHaveBeenTriggeredOn(form);
+ });
+ });
- element = $('#lti_id');
- errorMessage = element.find('.error_message');
- form = element.find('.ltiLaunchForm');
- frame = element.find('.ltiLaunchFrame');
+ describe(
+ 'open_in_a_new_page is "true", launch URL is default',
+ function () {
- spyOnEvent(form, 'submit');
+ beforeEach(function () {
+ initialize(IN_NEW_WINDOW, DEFAULT_URL);
+ });
- LTI(element);
+ it('form is not submitted', function () {
+ expect('submit').not.toHaveBeenTriggeredOn(form);
});
+ });
- it(
- 'when URL setting is not filled form is not submited',
- function () {
+ 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('After the settings were filled in', function () {
- var element, errorMessage, frame;
+ describe(
+ 'open_in_a_new_page is "false", launch URL is empty',
+ function () {
- // This function will be executed before each of the it() specs
- // in this suite.
beforeEach(function () {
- loadFixtures('lti.html');
+ initialize(IN_IFRAME, EMPTY_URL);
+ });
- element = $('#lti_id');
- errorMessage = element.find('.error_message');
- form = element.find('.ltiLaunchForm');
- frame = element.find('.ltiLaunchFrame');
+ it('form is not submitted', function () {
+ expect('submit').not.toHaveBeenTriggeredOn(form);
+ });
+ });
- spyOnEvent(form, 'submit');
+ describe(
+ 'open_in_a_new_page is "false", launch URL is default',
+ function () {
- // The user "fills in" the necessary settings, and the
- // form will get an action URL.
- form.attr('action', 'http://www.example.com/test_submit');
+ beforeEach(function () {
+ initialize(IN_IFRAME, DEFAULT_URL);
+ });
- LTI(element);
+ 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('when URL setting is filled form is submited', function () {
+ it('form is submitted', function () {
expect('submit').toHaveBeenTriggeredOn(form);
});
});
diff --git a/common/lib/xmodule/xmodule/js/src/lti/lti.js b/common/lib/xmodule/xmodule/js/src/lti/lti.js
index 7d5b183f216b..9454b0c34676 100644
--- a/common/lib/xmodule/xmodule/js/src/lti/lti.js
+++ b/common/lib/xmodule/xmodule/js/src/lti/lti.js
@@ -1,9 +1,43 @@
+/**
+ * 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 iframe.
+ // Initialize the LTI module.
+ //
+ // @param element DOM element, or jQuery element object.
+ //
+ // @return undefined
function initialize(element) {
- var form;
+ var form, openInANewPage, formAction;
// In cms (Studio) the element is already a jQuery object. In lms it is
// a DOM object.
@@ -13,12 +47,36 @@ window.LTI = (function () {
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 submit the form and make the frame shown.
- if (form.attr('action') && form.attr('action') !== 'http://www.example.com') {
+ // 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();
- element.find('.lti').addClass('rendered');
}
}
diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py
index bbc39957a078..6c442ae7258c 100644
--- a/common/lib/xmodule/xmodule/lti_module.py
+++ b/common/lib/xmodule/xmodule/lti_module.py
@@ -8,12 +8,14 @@
import logging
import oauthlib.oauth1
import urllib
+import json
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule
from xmodule.course_module import CourseDescriptor
from pkg_resources import resource_string
from xblock.core import String, Scope, List
+from xblock.fields import Boolean
log = logging.getLogger(__name__)
@@ -45,6 +47,7 @@ class LTIFields(object):
lti_id = String(help="Id of the tool", default='', scope=Scope.settings)
launch_url = String(help="URL of the tool", default='http://www.example.com', scope=Scope.settings)
custom_parameters = List(help="Custom parameters (vbid, book_location, etc..)", scope=Scope.settings)
+ open_in_a_new_page = Boolean(help="Should LTI be opened in new page?", default=True, scope=Scope.settings)
class LTIModule(LTIFields, XModule):
@@ -169,14 +172,15 @@ def get_html(self):
client_key,
client_secret
)
-
context = {
'input_fields': input_fields,
# these params do not participate in oauth signing
'launch_url': self.launch_url,
'element_id': self.location.html_id(),
- 'element_class': self.location.category,
+ 'element_class': self.category,
+ 'open_in_a_new_page': self.open_in_a_new_page,
+ 'display_name': self.display_name,
}
return self.system.render_template('lti.html', context)
diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature
index a7182a3daeea..8255e515b077 100644
--- a/lms/djangoapps/courseware/features/lti.feature
+++ b/lms/djangoapps/courseware/features/lti.feature
@@ -2,17 +2,27 @@
Feature: LMS.LTI component
As a student, I want to view LTI component in LMS.
- Scenario: LTI component in LMS is not rendered
+ Scenario: LTI component in LMS with no launch_url is not rendered
Given the course has correct LTI credentials
- And the course has an LTI component with incorrect fields
- Then I view the LTI and it is not rendered
+ And the course has an LTI component with no_launch_url fields, new_page is false
+ Then I view the LTI and error is shown
- Scenario: LTI component in LMS is rendered
+ Scenario: LTI component in LMS with incorrect lti_id is rendered incorrectly
Given the course has correct LTI credentials
- And the course has an LTI component filled with correct fields
- Then I view the LTI and it is rendered
+ And the course has an LTI component with incorrect_lti_id fields, new_page is false
+ Then I view the LTI but incorrect_signature warning is rendered
Scenario: LTI component in LMS is rendered incorrectly
Given the course has incorrect LTI credentials
- And the course has an LTI component filled with correct fields
+ And the course has an LTI component with correct fields, new_page is false
Then I view the LTI but incorrect_signature warning is rendered
+
+ Scenario: LTI component in LMS is correctly rendered in new page
+ Given the course has correct LTI credentials
+ And the course has an LTI component with correct fields, new_page is true
+ Then I view the LTI and it is rendered in new page
+
+ Scenario: LTI component in LMS is correctly rendered in iframe
+ Given the course has correct LTI credentials
+ And the course has an LTI component with correct fields, new_page is false
+ Then I view the LTI and it is rendered in iframe
diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py
index fa76dbb4f620..58de82ded630 100644
--- a/lms/djangoapps/courseware/features/lti.py
+++ b/lms/djangoapps/courseware/features/lti.py
@@ -1,5 +1,6 @@
#pylint: disable=C0111
+import os
from django.contrib.auth.models import User
from lettuce import world, step
from lettuce.django import django_url
@@ -8,33 +9,19 @@
from student.models import CourseEnrollment
-@step('I view the LTI and it is not rendered$')
+@step('I view the LTI and error is shown$')
def lti_is_not_rendered(_step):
- # lti div has no class rendered
- assert world.is_css_not_present('div.lti.rendered')
-
# error is shown
- assert world.css_visible('.error_message')
+ assert world.is_css_present('.error_message')
- # iframe is not visible
- assert not world.css_visible('iframe')
+ # iframe is not presented
+ assert not world.is_css_present('iframe')
- location = world.scenario_dict['LTI'].location.html_id()
- iframe_name = 'ltiLaunchFrame-' + location
-
- #inside iframe test content is not presented
- with world.browser.get_iframe(iframe_name) as iframe:
- # iframe does not contain functions from terrain/ui_helpers.py
- world.browser.driver.implicitly_wait(1)
- try:
- assert iframe.is_element_not_present_by_css('.result', wait_time=1)
- except:
- raise
- finally:
- world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
+ # link is not presented
+ assert not world.is_css_present('.link_lti_new_window')
-def check_lti_ifarme_content(text):
+def check_lti_iframe_content(text):
#inside iframe test content is presented
location = world.scenario_dict['LTI'].location.html_id()
iframe_name = 'ltiLaunchFrame-' + location
@@ -47,30 +34,33 @@ def check_lti_ifarme_content(text):
))
-@step('I view the LTI and it is rendered$')
-def lti_is_rendered(_step):
- # lti div has class rendered
- assert world.is_css_present('div.lti.rendered')
+@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')
- # error is hidden
- assert not world.css_visible('.error_message')
+ # iframe is visible
+ assert world.css_visible('iframe')
+ check_lti_iframe_content("This is LTI tool. Success.")
- # iframe is visible
- assert world.css_visible('iframe')
- check_lti_ifarme_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')
+ check_lti_popup()
+ else: # incorrent rendered_in parameter
+ assert False
@step('I view the LTI but incorrect_signature warning is rendered$')
def incorrect_lti_is_rendered(_step):
- # lti div has class rendered
- assert world.is_css_present('div.lti.rendered')
-
- # error is hidden
- assert not world.css_visible('.error_message')
-
- # iframe is visible
- assert world.css_visible('iframe')
- check_lti_ifarme_content("Wrong LTI signature")
+ assert world.is_css_present('iframe')
+ assert not world.is_css_present('.link_lti_new_window')
+ assert not world.is_css_present('.error_message')
+ #inside iframe test content is presented
+ check_lti_iframe_content("Wrong LTI signature")
@step('the course has correct LTI credentials$')
@@ -97,44 +87,33 @@ def set_incorrect_lti_passport(_step):
i_am_registered_for_the_course(coursenum, metadata)
-@step('the course has an LTI component filled with correct fields$')
-def add_correct_lti_to_course(_step):
+@step('the course has an LTI component with (.*) fields, new_page is(.*)$')
+def add_correct_lti_to_course(_step, fields, new_page):
category = 'lti'
- world.scenario_dict['LTI'] = world.ItemFactory.create(
- # parent_location=section_location(course),
- parent_location=world.scenario_dict['SEQUENTIAL'].location,
- category=category,
- display_name='LTI',
- metadata={
- 'lti_id': 'correct_lti_id',
- 'launch_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint']
- }
- )
- course = world.scenario_dict["COURSE"]
- chapter_name = world.scenario_dict['SECTION'].display_name.replace(
- " ", "_")
- section_name = chapter_name
- path = "/courses/{org}/{num}/{name}/courseware/{chapter}/{section}".format(
- org=course.org,
- num=course.number,
- name=course.display_name.replace(' ', '_'),
- chapter=chapter_name,
- section=section_name)
- url = django_url(path)
-
- world.browser.visit(url)
-
+ lti_id = 'correct_lti_id'
+ launch_url = world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint']
+ if fields.strip() == 'incorrect_lti_id': # incorrect fields
+ lti_id = 'incorrect_lti_id'
+ elif fields.strip() == 'correct': # correct fields
+ pass
+ elif fields.strip() == 'no_launch_url':
+ launch_url = u''
+ else: # incorrect parameter
+ assert False
+
+ if new_page.strip().lower() == 'false':
+ new_page = False
+ else: # default is True
+ new_page = True
-@step('the course has an LTI component with incorrect fields$')
-def add_incorrect_lti_to_course(_step):
- category = 'lti'
world.scenario_dict['LTI'] = world.ItemFactory.create(
parent_location=world.scenario_dict['SEQUENTIAL'].location,
category=category,
display_name='LTI',
metadata={
- 'lti_id': 'incorrect_lti_id',
- 'lti_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint']
+ 'lti_id': lti_id,
+ 'launch_url': launch_url,
+ 'open_in_a_new_page': new_page
}
)
course = world.scenario_dict["COURSE"]
@@ -192,3 +171,28 @@ def i_am_registered_for_the_course(course, metadata):
CourseEnrollment.enroll(usr, course_id(course))
world.log_in(username='robot', password='test')
+
+
+def check_lti_popup():
+ parent_window = world.browser.current_window # Save the parent window
+ world.css_find('.link_lti_new_window').first.click()
+
+ assert len(world.browser.windows) != 1
+
+ for window in world.browser.windows:
+ world.browser.switch_to_window(window) # Switch to a different window (the pop-up)
+ # Check if this is the one we want by comparing the url
+ url = world.browser.url
+ basename = os.path.basename(url)
+ pathname = os.path.splitext(basename)[0]
+
+ if pathname == u'correct_lti_endpoint':
+ break
+
+ result = world.css_find('.result').first.text
+ assert result == u'This is LTI tool. Success.'
+
+ world.browser.driver.close() # Close the pop-up window
+ world.browser.switch_to_window(parent_window) # Switch to the main window again
+
+
diff --git a/lms/djangoapps/courseware/tests/test_lti.py b/lms/djangoapps/courseware/tests/test_lti.py
index 1996f95cbff4..a176384fd12a 100644
--- a/lms/djangoapps/courseware/tests/test_lti.py
+++ b/lms/djangoapps/courseware/tests/test_lti.py
@@ -72,9 +72,11 @@ def test_lti_constructor(self):
generated_context = self.item_module.render('student_view').content
expected_context = {
'input_fields': self.correct_headers,
+ 'display_name': self.item_module.display_name,
'element_class': self.item_module.location.category,
'element_id': self.item_module.location.html_id(),
'launch_url': 'http://www.example.com', # default value
+ 'open_in_a_new_page': True,
}
self.assertEqual(
generated_context,
diff --git a/lms/templates/lti.html b/lms/templates/lti.html
index 5e1b9e5d772b..7e6a3eac404f 100644
--- a/lms/templates/lti.html
+++ b/lms/templates/lti.html
@@ -1,14 +1,24 @@
-
+<%! import json %>
+<%! from django.utils.translation import ugettext as _ %>
- ## This form will be hidden. Once available on the client, the LTI
- ## module JavaScript will trigget a "submit" on the form, and the
+
+
+ ## 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.
+
+% if launch_url and launch_url != 'http://www.example.com':
+ % if open_in_a_new_page:
+
+ % else:
+ ## The result of the form submit will be rendered here.
+
+ % endif
+% else:
- Please provide launch_url. Click "Edit", and fill in the
- required fields.
+ ${_('Please provide launch_url. Click "Edit", and fill in the required fields.')}
-
- ## The result of the form submit will be rendered here.
-
-
+%endif