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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,53 +1,69 @@
|
||||
"""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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user