Commit 417ddcaa by Valera Rozuvan Committed by Alexander Kryklia

LTI module with tests

parent 0052b87c
......@@ -52,7 +52,8 @@ NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = [
'annotatable',
'word_cloud',
'graphical_slider_tool'
'graphical_slider_tool',
'lti'
] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
......
......@@ -56,6 +56,7 @@ setup(
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor",
"lti = xmodule.lti_module:LTIModuleDescriptor"
],
'console_scripts': [
'xmodule_assets = xmodule.static_content:main',
......
div.lti {
h2.error_message {
&.hidden {
display: none;
}
}
}
<div align="center" id="lti_id" class="lti">
<form
action=""
name="ltiLaunchForm"
id="ltiLaunchForm"
method="post"
target="ltiLaunchFrame"
encType="application/x-www-form-urlencoded"
>
<input type="hidden" name="launch_presentation_return_url" value="">
<input type="hidden" name="lis_outcome_service_url" value="">
<input type="hidden" name="lis_result_sourcedid" value="">
<input type="hidden" name="lti_message_type" value="basic-lti-launch-request">
<input type="hidden" name="lti_version" value="LTI-1p0">
<input type="hidden" name="oauth_callback" value="about:blank">
<input type="hidden" name="oauth_consumer_key" value=""/>
<input type="hidden" name="oauth_nonce" value=""/>
<input type="hidden" name="oauth_signature_method" value="HMAC-SHA1"/>
<input type="hidden" name="oauth_timestamp" value=""/>
<input type="hidden" name="oauth_version" value="1.0"/>
<input type="hidden" name="user_id" value="default_user_id">
<input type="hidden" name="oauth_signature" value=""/>
<input type="submit" value="Press to Launch" style="display: none"/>
</form>
<h2 class="error_message hidden">
Please provide LTI url. Click "Edit", and fill in the
required fields.
</h2>
<iframe
name="ltiLaunchFrame"
id="ltiLaunchFrame"
width="0"
height="0"
src=""
style="border: 0px; overflow-x: hidden;"
></iframe>
</div>
/*
* "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
* to attack."
*
* ~ Sun Tzu
*/
(function () {
describe('LTI', function () {
var documentReady = false,
element, errorMessage, frame,
editSettings = false;
// This function will be executed before each of the it() specs
// in this suite.
beforeEach(function () {
$(document).ready(function () {
documentReady = true;
});
spyOn(LTI, 'init').andCallThrough();
spyOn($.fn, 'submit').andCallThrough();
loadFixtures('lti.html');
element = $('#lti_id');
errorMessage = element.find('h2.error_message');
form = element.find('form#ltiLaunchForm');
frame = element.find('iframe#ltiLaunchFrame');
// First part of the tests will be running without the settings
// filled in. Once we reach the describe() spec
//
// "After the settings were filled in"
//
// the variable `editSettings` will be changed to `true`.
if (editSettings) {
form.attr('action', 'http://google.com/');
}
LTI(element);
});
describe('constructor', function () {
it('init() is called after document is ready', function () {
waitsFor(
function () {
return documentReady;
},
'The document is ready',
1000
);
runs(function () {
expect(LTI.init).toHaveBeenCalled();
});
});
describe('before settings were filled in', function () {
it('init() is called with element', function () {
expect(LTI.init).toHaveBeenCalledWith(element);
});
it(
'when URL setting is empty error message is shown',
function () {
expect(errorMessage).not.toHaveClass('hidden');
});
it('when URL setting is empty iframe is hidden', function () {
expect(frame.css('display')).toBe('none');
});
});
describe('After the settings were filled in', function () {
it('editSettings is disabled', function () {
expect(editSettings).toBe(false);
// Let us toggle edit settings switch. Next beforeEach()
// will populate element's attributes with settings.
editSettings = true;
});
it('when URL setting is filled form is submited', function () {
expect($.fn.submit).toHaveBeenCalled();
});
it(
'when URL setting is filled error message is hidden',
function () {
expect(errorMessage).toHaveClass('hidden');
});
it('when URL setting is filled iframe is shown', function () {
expect(frame.css('display')).not.toBe('none');
});
it(
'when URL setting is filled iframe is resized',
function () {
expect(frame.width()).toBe(form.parent().width());
expect(frame.height()).toBe(800);
});
});
});
});
}());
window.LTI = (function () {
var LTI;
// Function LTI()
//
// The LTI module constructor. It will be called by XModule for any
// LTI module DIV that is found on the page.
LTI = function (element) {
$(document).ready(function () {
LTI.init(element);
});
}
// Function init()
//
// Initialize the LTI iframe.
LTI.init = function (element) {
var form, frame;
// 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');
frame = element.find('#ltiLaunchFrame');
// If the Form's action attribute is set (i.e. we can perform a normal
// submit), then we submit the form and make it big enough so that the
// received response can fit in it. Hide the error message, if shown.
if (form.attr('action')) {
form.submit();
element.find('h2.error_message').addClass('hidden');
frame.show();
frame.width('100%').height(800);
}
// If no action URL was specified, we show an error message.
else {
frame.hide();
element.find('h2.error_message').removeClass('hidden');
}
}
return LTI;
}());
"""
Module that allows to insert LTI tools to page.
"""
import logging
import requests
import urllib
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule
from pkg_resources import resource_string
from xblock.core import String, Scope
log = logging.getLogger(__name__)
class LTIFields(object):
"""provider_url and tool_id together is unique location of LTI in the web.
Scope settings should be scope content:
Cale: There is no difference in presentation to the user yet because
there is no sharing between courses. However, when we get to the point of being
able to have multiple courses using the same content,
then the distinction between Scope.settings (local to the current course),
and Scope.content (shared across all uses of this content in any course)
becomes much more clear/necessary.
"""
client_key = String(help="Client key", default='', scope=Scope.settings)
client_secret = String(help="Client secret", default='', scope=Scope.settings)
lti_url = String(help="URL of the tool", default='', scope=Scope.settings)
class LTIModule(LTIFields, XModule):
'''LTI Module'''
js = {'js': [resource_string(__name__, 'js/src/lti/lti.js')]}
css = {'scss': [resource_string(__name__, 'css/lti/lti.scss')]}
js_module_name = "LTI"
def get_html(self):
""" Renders parameters to template. """
params = {
'lti_url': self.lti_url,
'element_id': self.location.html_id(),
'element_class': self.location.category,
}
params.update(self.oauth_params())
return self.system.render_template('lti.html', params)
def oauth_params(self):
"""Obtains LTI html from provider"""
client = requests.auth.Client(
client_key=unicode(self.client_key),
client_secret=unicode(self.client_secret)
)
# must have parameters for correct signing from LTI:
body = {
'user_id': 'default_user_id',
'oauth_callback': 'about:blank',
'lis_outcome_service_url': '',
'lis_result_sourcedid': '',
'launch_presentation_return_url': '',
'lti_message_type': 'basic-lti-launch-request',
'lti_version': 'LTI-1p0',
}
# This is needed for body encoding:
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
_, headers, _ = client.sign(
unicode(self.lti_url),
http_method=u'POST',
body=body,
headers=headers)
params = headers['Authorization']
# import ipdb; ipdb.set_trace()
# parse headers to pass to template as part of context:
params = dict([param.strip().replace('"', '').split('=') for param in params.split('",')])
params[u'oauth_nonce'] = params[u'OAuth oauth_nonce']
del params[u'OAuth oauth_nonce']
# 0.14.2 (current) version of requests oauth library encodes signature,
# with 'Content-Type': 'application/x-www-form-urlencoded'
# so '='' becomes '%3D', but server waits for unencoded signature.
# Decode signature back:
params[u'oauth_signature'] = urllib.unquote(params[u'oauth_signature']).decode('utf8')
return params
class LTIModuleDescriptor(LTIFields, MetadataOnlyEditingDescriptor):
"""LTI Descriptor. No export/import to html."""
module_class = LTIModule
"""LTI test"""
import requests
from . import BaseTestXmodule
class TestLTI(BaseTestXmodule):
"""Integration test for word cloud xmodule."""
CATEGORY = "lti"
def setUp(self):
super(TestLTI, self).setUp()
mocked_noonce = u'135685044251684026041377608307'
mocked_timestamp = u'1234567890'
mocked_signed_signature = u'my_signature%3D'
mocked_decoded_signature = u'my_signature='
self.correct_headers = {
u'oauth_nonce': mocked_noonce,
u'oauth_timestamp': mocked_timestamp,
u'oauth_consumer_key': u'',
u'oauth_signature_method': u'HMAC-SHA1',
u'oauth_version': u'1.0',
u'oauth_signature': mocked_decoded_signature}
saved_sign = requests.auth.Client.sign
def mocked_sign(self, *args, **kwargs):
"""Mocked oauth1 sign function"""
# self is <oauthlib.oauth1.rfc5849.Client object at 0x107456e90> here:
_, headers, _ = saved_sign(self, *args, **kwargs)
# we should replace noonce, timestamp and signed_signature in headers:
old = headers[u'Authorization']
new = old[:19] + mocked_noonce + old[49:69] + mocked_timestamp + \
old[79:179] + mocked_signed_signature + old[-1]
headers[u'Authorization'] = new
return None, headers, None
requests.auth.Client.sign = mocked_sign
def test_lti_constructor(self):
"""Make sure that all parameters extracted """
fragment = self.runtime.render(self.item_module, None, 'student_view')
expected_context = {
'element_class': self.item_module.location.category,
'element_id': self.item_module.location.html_id(),
'lti_url': '', # default value
}
self.correct_headers.update(expected_context)
# import ipdb; ipdb.set_trace()
self.assertEqual(
fragment.content,
self.runtime.render_template('lti.html', self.correct_headers)
)
<div align="center" id="${element_id}" class="${element_class}">
## This form will be hidden. Once available on the client, the LTI
## module JavaScript will trigget a "submit" on the form, and the
## result will be rendered to the below iFrame.
<form
action="${lti_url}"
name="ltiLaunchForm"
id="ltiLaunchForm"
method="post"
target="ltiLaunchFrame"
encType="application/x-www-form-urlencoded"
>
<input type="hidden" name="launch_presentation_return_url" value="">
<input type="hidden" name="lis_outcome_service_url" value="">
<input type="hidden" name="lis_result_sourcedid" value="">
<input type="hidden" name="lti_message_type" value="basic-lti-launch-request">
<input type="hidden" name="lti_version" value="LTI-1p0">
<input type="hidden" name="oauth_callback" value="about:blank">
<input type="hidden" name="oauth_consumer_key" value="${oauth_consumer_key}"/>
<input type="hidden" name="oauth_nonce" value="${oauth_nonce}"/>
<input type="hidden" name="oauth_signature_method" value="HMAC-SHA1"/>
<input type="hidden" name="oauth_timestamp" value="${oauth_timestamp}"/>
<input type="hidden" name="oauth_version" value="1.0"/>
<input type="hidden" name="user_id" value="default_user_id">
<input type="hidden" name="oauth_signature" value="${oauth_signature}"/>
<input type="submit" value="Press to Launch" style="display: none"/>
</form>
<h3 class="error_message hidden">
Please provide LTI url. Click "Edit", and fill in the
required fields.
</h3>
## The result of the form submit will be rendered here.
<iframe
name="ltiLaunchFrame"
id="ltiLaunchFrame"
width="0"
height="0"
src=""
style="border: 0px; overflow-x: hidden;"
></iframe>
</div>
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