Commit f6d9c077 by Alexander Kryklia

Improve LTI module

Add simplifyed template.
Update lti integration test.
Add getting oauth credentials from course settings.
Add user id transferring to provider.
Improve documentation of lti module.
parent fd64ed9a
......@@ -14,6 +14,7 @@ import urllib
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
......@@ -106,9 +107,9 @@ class LTIModule(LTIFields, XModule):
<input name="role" value="student" />
<input name="oauth_signature" value="${oauth_signature}" />
% for param_name, param_value in custom_parameters.items():
<input name="${param_name}" value="${param_value}" />
%endfor
<input name="custom_1" value="${custom_param_1_value}" />
<input name="custom_2" value="${custom_param_2_value}" />
<input name="custom_..." value="${custom_param_..._value}" />
<input type="submit" value="Press to Launch" />
</form>
......@@ -129,13 +130,9 @@ class LTIModule(LTIFields, XModule):
""" Renders parameters to template. """
# Obtains client_key and client_secret credentials from current course:
# course location example: u'i4x://blades/1/course/2013_Spring'
course = self.descriptor.system.load_item(
self.location.tag + '://' +
self.location.org + '/' +
self.location.course +
'/course' +
'/2013_Spring')
course_id = self.runtime.course_id
course_location = CourseDescriptor.id_to_location(course_id)
course = self.descriptor.runtime.modulestore.get_item(course_location)
client_key, client_secret = '', ''
for lti_passport in course.LTIs:
try:
......@@ -147,15 +144,8 @@ class LTIModule(LTIFields, XModule):
client_key, client_secret = key, secret
break
# these params do not participate in oauth signing
params = {
'launch_url': self.launch_url,
'element_id': self.location.html_id(),
'element_class': self.location.category,
}
# parsing custom parameters to dict
parsed_custom_parameters = {}
custom_parameters = {}
for custom_parameter in self.custom_parameters:
try:
param_name, param_value = custom_parameter.split('=')
......@@ -164,17 +154,26 @@ class LTIModule(LTIFields, XModule):
Should be "x=y" string.'.format(custom_parameter))
# LTI specs: 'custom_' should be prepended before each custom parameter
parsed_custom_parameters.update(
custom_parameters.update(
{u'custom_' + unicode(param_name): unicode(param_value)}
)
params.update({'custom_parameters': parsed_custom_parameters})
params.update(self.oauth_params(
parsed_custom_parameters,
input_fields = (self.oauth_params(
custom_parameters,
client_key,
client_secret
))
return self.system.render_template('lti.html', params)
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,
}
return self.system.render_template('lti.html', context)
def oauth_params(self, custom_parameters, client_key, client_secret):
"""Signs request and returns signature and oauth parameters.
......@@ -191,20 +190,19 @@ class LTIModule(LTIFields, XModule):
client_secret=unicode(client_secret)
)
# @ned - why self.runtime.anonymous_student_id is None in dev env?
user_id = self.runtime.anonymous_student_id
user_id = user_id if user_id else 'default_user_id'
assert user_id is not None
# must have parameters for correct signing from LTI:
body = {
'user_id': 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',
'role': 'student'
u'user_id': user_id,
u'oauth_callback': u'about:blank',
u'lis_outcome_service_url': '',
u'lis_result_sourcedid': '',
u'launch_presentation_return_url': '',
u'lti_message_type': u'basic-lti-launch-request',
u'lti_version': 'LTI-1p0',
u'role': u'student'
}
# appending custom parameter for signing
......@@ -220,13 +218,11 @@ class LTIModule(LTIFields, XModule):
headers=headers)
params = headers['Authorization']
# parse headers to pass to template as part of context:
params = dict([param.strip().replace('"', '').split('=') for param in params.split('",')])
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']
params['user_id'] = body['user_id']
# 0.14.2 (current) version of requests oauth library encodes signature,
# with 'Content-Type': 'application/x-www-form-urlencoded'
# so '='' becomes '%3D'.
......@@ -234,6 +230,8 @@ class LTIModule(LTIFields, XModule):
# So we need to decode signature back:
params[u'oauth_signature'] = urllib.unquote(params[u'oauth_signature']).decode('utf8')
# add lti parameters to oauth parameters for sending in form
params.update(body)
return params
......
"""LTI test"""
"""LTI integration tests"""
import requests
from . import BaseTestXmodule
from collections import OrderedDict
class TestLTI(BaseTestXmodule):
"""Integration test for word cloud xmodule."""
"""
Integration test for lti xmodule.
"""
CATEGORY = "lti"
def setUp(self):
"""
Mock oauth1 signing of requests library for testing.
"""
super(TestLTI, self).setUp()
mocked_noonce = u'135685044251684026041377608307'
mocked_timestamp = u'1234567890'
mocked_signed_signature = u'my_signature%3D'
mocked_signature_after_sign = u'my_signature%3D'
mocked_decoded_signature = u'my_signature='
self.correct_headers = {
u'oauth_callback': u'about:blank',
u'lis_outcome_service_url': '',
u'lis_result_sourcedid': '',
u'launch_presentation_return_url': '',
u'lti_message_type': u'basic-lti-launch-request',
u'lti_version': 'LTI-1p0',
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}
u'user_id': self.runtime.anonymous_student_id,
u'role': u'student',
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:
# self is <oauthlib.oauth1.rfc5849.Client object> 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
old_parsed = OrderedDict([param.strip().replace('"', '').split('=') for param in old.split(',')])
old_parsed[u'OAuth oauth_nonce'] = mocked_noonce
old_parsed[u'oauth_timestamp'] = mocked_timestamp
old_parsed[u'oauth_signature'] = mocked_signature_after_sign
headers[u'Authorization'] = ', '.join([k+'="'+v+'"' for k, v in old_parsed.items()])
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')
self.runtime.render_template = lambda template, context: context
generated_context = self.item_module.get_html()
expected_context = {
'input_fields': self.correct_headers,
'element_class': self.item_module.location.category,
'element_id': self.item_module.location.html_id(),
'lti_url': '', # default value
'launch_url': '', # default value
}
self.correct_headers.update(expected_context)
self.assertEqual(
fragment.content,
self.runtime.render_template('lti.html', self.correct_headers)
)
self.assertDictEqual(generated_context, expected_context)
......@@ -11,22 +11,8 @@
target="ltiLaunchFrame"
encType="application/x-www-form-urlencoded"
>
<input name="launch_presentation_return_url" value="" />
<input name="lis_outcome_service_url" value="" />
<input name="lis_result_sourcedid" value="" />
<input name="lti_message_type" value="basic-lti-launch-request" />
<input name="lti_version" value="LTI-1p0" />
<input name="oauth_callback" value="about:blank" />
<input name="oauth_consumer_key" value="${oauth_consumer_key}" />
<input name="oauth_nonce" value="${oauth_nonce}" />
<input name="oauth_signature_method" value="HMAC-SHA1" />
<input name="oauth_timestamp" value="${oauth_timestamp}" />
<input name="oauth_version" value="1.0" />
<input name="user_id" value="${user_id}" />
<input name="role" value="student" />
<input name="oauth_signature" value="${oauth_signature}" />
% for param_name, param_value in custom_parameters.items():
% for param_name, param_value in input_fields.items():
<input name="${param_name}" value="${param_value}" />
%endfor
......
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