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.
241 lines
9.1 KiB
Python
241 lines
9.1 KiB
Python
"""
|
|
Module that allows to insert LTI tools to page.
|
|
|
|
Module uses current edx-platform 0.14.2 version of requests (oauth part).
|
|
Please update code when upgrading requests.
|
|
|
|
Protocol is oauth1, LTI version is 1.1.1:
|
|
http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html
|
|
"""
|
|
|
|
import logging
|
|
import requests
|
|
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
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class LTIFields(object):
|
|
"""
|
|
Fields to define and obtain LTI tool from provider are set here,
|
|
except credentials, which should be set in course settings::
|
|
|
|
`lti_id` is id to connect tool with credentials in course settings.
|
|
`launch_url` is launch url of tool.
|
|
`custom_parameters` are additional parameters to navigate to proper book and book page.
|
|
|
|
For example, for Vitalsource provider, `launch_url` should be
|
|
*https://bc-staging.vitalsource.com/books/book*,
|
|
and to get to proper book and book page, you should set custom parameters as::
|
|
|
|
vbid=put_book_id_here
|
|
book_location=page/put_page_number_here
|
|
|
|
"""
|
|
lti_id = String(help="Id of the tool", default='', scope=Scope.settings)
|
|
launch_url = String(help="URL of the tool", default='', scope=Scope.settings)
|
|
custom_parameters = List(help="Custom parameters (vbid, book_location, etc..)", scope=Scope.settings)
|
|
|
|
|
|
class LTIModule(LTIFields, XModule):
|
|
'''Module provides LTI integration to course.
|
|
|
|
Except usual xmodule structure it proceeds with oauth signing.
|
|
How it works::
|
|
|
|
1. Get credentials from course settings.
|
|
|
|
2. There is minimal set of parameters need to be signed (presented for Vitalsource)::
|
|
|
|
user_id
|
|
oauth_callback
|
|
lis_outcome_service_url
|
|
lis_result_sourcedid
|
|
launch_presentation_return_url
|
|
lti_message_type
|
|
lti_version
|
|
role
|
|
*+ all custom parameters*
|
|
|
|
These parameters should be encoded and signed by *oauth1* together with
|
|
`launch_url` and *POST* request type. Signing proceeds with client key/secret
|
|
pair obtained from course settings. That pair should be obtained from LTI provider
|
|
and set into course settings by course author.
|
|
After that signature and other oauth data are generated.
|
|
|
|
Additional oauth data which is generated after signing is usual::
|
|
|
|
oauth_callback
|
|
oauth_nonce
|
|
oauth_consumer_key
|
|
oauth_signature_method
|
|
oauth_timestamp
|
|
oauth_version
|
|
|
|
|
|
All that data is passed to form and sent to LTI provider server by browser via
|
|
autosubmit via javascript.
|
|
|
|
Form example::
|
|
|
|
<form
|
|
action="${launch_url}"
|
|
name="ltiLaunchForm"
|
|
class="ltiLaunchForm"
|
|
method="post"
|
|
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}" />
|
|
|
|
<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>
|
|
|
|
LTI provider has same secret key and it signs data string via *oauth1* and compares signatures.
|
|
|
|
If signatures are correct, LTI provider redirects iframe source to LTI tool web page,
|
|
and LTI tool is rendered to iframe inside course.
|
|
|
|
Otherwise error message from LTI provider is generated.
|
|
'''
|
|
|
|
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. """
|
|
|
|
# Obtains client_key and client_secret credentials from current course:
|
|
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:
|
|
lti_id, key, secret = lti_passport.split(':')
|
|
except ValueError:
|
|
raise Exception('Could not parse LTI passport: {0}. \
|
|
Should be "id:key:secret" string.'.format(lti_passport))
|
|
if lti_id == self.lti_id:
|
|
client_key, client_secret = key, secret
|
|
break
|
|
|
|
# parsing custom parameters to dict
|
|
custom_parameters = {}
|
|
for custom_parameter in self.custom_parameters:
|
|
try:
|
|
param_name, param_value = custom_parameter.split('=')
|
|
except ValueError:
|
|
raise Exception('Could not parse custom parameter: {0}. \
|
|
Should be "x=y" string.'.format(custom_parameter))
|
|
|
|
# LTI specs: 'custom_' should be prepended before each custom parameter
|
|
custom_parameters.update(
|
|
{u'custom_' + unicode(param_name): unicode(param_value)}
|
|
)
|
|
|
|
input_fields = (self.oauth_params(
|
|
custom_parameters,
|
|
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,
|
|
}
|
|
|
|
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.
|
|
|
|
`custom_paramters` is dict of parsed `custom_parameter` field
|
|
|
|
`client_key` and `client_secret` are LTI tool credentials.
|
|
|
|
Also *anonymous student id* is passed to template and therefore to LTI provider.
|
|
"""
|
|
|
|
client = requests.auth.Client(
|
|
client_key=unicode(client_key),
|
|
client_secret=unicode(client_secret)
|
|
)
|
|
|
|
user_id = self.runtime.anonymous_student_id
|
|
assert user_id is not None
|
|
|
|
# must have parameters for correct signing from LTI:
|
|
body = {
|
|
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
|
|
body.update(custom_parameters)
|
|
|
|
# This is needed for body encoding:
|
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
|
|
_, headers, _ = client.sign(
|
|
unicode(self.launch_url),
|
|
http_method=u'POST',
|
|
body=body,
|
|
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[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'.
|
|
# We send form via browser, so browser will encode it again,
|
|
# 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
|
|
|
|
|
|
class LTIModuleDescriptor(LTIFields, MetadataOnlyEditingDescriptor):
|
|
"""LTI Descriptor. No export/import to xml."""
|
|
module_class = LTIModule
|