Skip to content
Snippets Groups Projects
Commit 07335119 authored by Douglas Hall's avatar Douglas Hall
Browse files

Merge pull request #1 from edx/task/douglashall/PHX-201/xblock_lti_consumer

Sprint Solutions:Zug PHX-200 PHX-201 XBlock LTI Consumer
parents 3ee67dcc e633f55b
No related branches found
No related tags found
No related merge requests found
Showing
with 2595 additions and 4 deletions
### Python artifacts
*.pyc
*.egg-info
### Editor and IDE artifacts
*~
*.swp
*.orig
/nbproject
.idea/
.redcar/
codekit-config.json
.pycharm_helpers/
### Testing artifacts
.coverage
var/
\ No newline at end of file
.pep8 0 → 100644
[pep8]
ignore=E501
max_line_length=120
exclude=settings
language: python
python: "2.7"
install:
- "make install"
sudo: false
script:
- make quality
- make test
branches:
only:
- master
after_success: coveralls
LICENSE 0 → 100644
This diff is collapsed.
Makefile 0 → 100644
all: install compile-sass quality test
install-test:
pip install -q -r test_requirements.txt
install: install-test
compile-sass:
./scripts/sass.sh
quality:
./scripts/quality.sh
test:
./scripts/test.sh
NOTICE 0 → 100644
Copyright (C) 2015 edX
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
# LTI Consumer XBlock
This is a Python package containing an implementation of an LTI consumer using the XBlock API.
LTI Consumer XBlock |Build Status| |Coveralls|
----------------------------------------------
This XBlock implements the consumer side of the LTI specification enabling
integration of third-party LTI provider tools.
Installation
------------
Install the requirements into the python virtual environment of your
``edx-platform`` installation by running the following command from the
root folder:
.. code:: bash
$ pip install -r requirements.txt
Enabling in Studio
------------------
You can enable the LTI Consumer XBlock in Studio through the
advanced settings.
1. From the main page of a specific course, navigate to
``Settings -> Advanced Settings`` from the top menu.
2. Check for the ``advanced_modules`` policy key, and add
``"lti_consumer"`` to the policy value list.
3. Click the "Save changes" button.
Workbench installation and settings
-----------------------------------
Install to the workbench's virtualenv by running the following command
from the xblock-lti-consumer repo root with the workbench's virtualenv activated:
.. code:: bash
$ make install
Running tests
-------------
From the xblock-lti-consumer repo root, run the tests with the following command:
.. code:: bash
$ make test
Running code quality check
--------------------------
From the xblock-lti-consumer repo root, run the quality checks with the following command:
.. code:: bash
$ make quality
Compiling Sass
--------------
This XBlock uses Sass for writing style rules. The Sass is compiled
and committed to the git repo using:
.. code:: bash
$ make compile-sass
Changes to style rules should be made to the Sass files, compiled to CSS,
and committed to the git repository.
License
-------
The LTI Consumer XBlock is available under the Apache Version 2.0 License.
.. |Build Status| image:: https://travis-ci.org/edx/xblock-lti-consumer.svg
:target: https://travis-ci.org/edx/xblock-lti-consumer
.. |Coveralls| image:: https://coveralls.io/repos/edx/xblock-lti-consumer/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/edx/xblock-lti-consumer?branch=master
"""
Runtime will load the XBlock class from here.
"""
from .lti_consumer import LtiConsumerXBlock
"""
Exceptions for the LTI Consumer XBlock.
"""
class LtiError(Exception):
"""
General error class for LTI XBlock.
"""
pass
"""
This module encapsulates code which implements the LTI specification.
For more details see:
https://www.imsglobal.org/activity/learning-tools-interoperability
"""
import logging
import urllib
import json
from .exceptions import LtiError
from .oauth import get_oauth_request_signature, verify_oauth_body_signature
log = logging.getLogger(__name__)
def parse_result_json(json_str):
"""
Helper method for verifying LTI 2.0 JSON object contained in the body of the request.
The json_str must be loadable. It can either be an dict (object) or an array whose first element is an dict,
in which case that first dict is considered.
The dict must have the "@type" key with value equal to "Result",
"resultScore" key with value equal to a number [0, 1], if "resultScore" is not
included in the JSON body, score will be returned as None
The "@context" key must be present, but we don't do anything with it. And the "comment" key may be
present, in which case it must be a string.
Arguments:
json_str (unicode): The body of the LTI 2.0 results service request, which is a JSON string
Returns:
(float, str): (score, [optional]comment) if parsing is successful
Raises:
LtiError: if verification fails
"""
try:
json_obj = json.loads(json_str)
except (ValueError, TypeError):
msg = "Supplied JSON string in request body could not be decoded: {}".format(json_str)
log.error("[LTI] %s", msg)
raise LtiError(msg)
# The JSON object must be a dict. If a non-empty list is passed in,
# use the first element, but only if it is a dict
if isinstance(json_obj, list) and len(json_obj) >= 1:
json_obj = json_obj[0]
if not isinstance(json_obj, dict):
msg = ("Supplied JSON string is a list that does not contain an object as the first element. {}"
.format(json_str))
log.error("[LTI] %s", msg)
raise LtiError(msg)
# '@type' must be "Result"
result_type = json_obj.get("@type")
if result_type != "Result":
msg = "JSON object does not contain correct @type attribute (should be 'Result', is z{})".format(result_type)
log.error("[LTI] %s", msg)
raise LtiError(msg)
# '@context' must be present as a key
if '@context' not in json_obj:
msg = "JSON object does not contain required key @context"
log.error("[LTI] %s", msg)
raise LtiError(msg)
# Return None if the resultScore key is missing, this condition
# will be handled by the upstream caller of this function
if "resultScore" not in json_obj:
score = None
else:
# if present, 'resultScore' must be a number between 0 and 1 inclusive
try:
score = float(json_obj.get('resultScore', "unconvertable")) # Check if float is present and the right type
if not 0.0 <= score <= 1.0:
msg = 'score value outside the permitted range of 0.0-1.0.'
log.error("[LTI] %s", msg)
raise LtiError(msg)
except (TypeError, ValueError) as err:
msg = "Could not convert resultScore to float: {}".format(err.message)
log.error("[LTI] %s", msg)
raise LtiError(msg)
return score, json_obj.get('comment', "")
class LtiConsumer(object):
"""
Limited implementation of the LTI 1.1/2.0 specification.
For the LTI 1.1 specification see:
https://www.imsglobal.org/specs/ltiv1p1
For the LTI 2.0 specification see:
https://www.imsglobal.org/specs/ltiv2p0
"""
CONTENT_TYPE_RESULT_JSON = 'application/vnd.ims.lis.v2.result+json'
def __init__(self, xblock):
self.xblock = xblock
def get_signed_lti_parameters(self):
"""
Signs LTI launch request and returns signature and OAuth parameters.
Arguments:
None
Returns:
dict: LTI launch parameters
"""
# Must have parameters for correct signing from LTI:
lti_parameters = {
u'user_id': self.xblock.user_id,
u'oauth_callback': u'about:blank',
u'launch_presentation_return_url': '',
u'lti_message_type': u'basic-lti-launch-request',
u'lti_version': 'LTI-1p0',
u'roles': self.xblock.role,
# Parameters required for grading:
u'resource_link_id': self.xblock.resource_link_id,
u'lis_result_sourcedid': self.xblock.lis_result_sourcedid,
u'context_id': self.xblock.context_id,
}
if self.xblock.has_score:
lti_parameters.update({
u'lis_outcome_service_url': self.xblock.outcome_service_url
})
self.xblock.user_email = ""
self.xblock.user_username = ""
# Username and email can't be sent in studio mode, because the user object is not defined.
# To test functionality test in LMS
if callable(self.xblock.runtime.get_real_user):
real_user_object = self.xblock.runtime.get_real_user(self.xblock.runtime.anonymous_student_id)
self.xblock.user_email = getattr(real_user_object, "email", "")
self.xblock.user_username = getattr(real_user_object, "username", "")
if self.xblock.ask_to_send_username and self.xblock.user_username:
lti_parameters["lis_person_sourcedid"] = self.xblock.user_username
if self.xblock.ask_to_send_email and self.xblock.user_email:
lti_parameters["lis_person_contact_email_primary"] = self.xblock.user_email
# Appending custom parameter for signing.
lti_parameters.update(self.xblock.prefixed_custom_parameters)
headers = {
# This is needed for body encoding:
'Content-Type': 'application/x-www-form-urlencoded',
}
key, secret = self.xblock.lti_provider_key_secret
oauth_signature = get_oauth_request_signature(key, secret, self.xblock.launch_url, headers, lti_parameters)
# Parse headers to pass to template as part of context:
oauth_signature = dict([param.strip().replace('"', '').split('=') for param in oauth_signature.split(',')])
oauth_signature[u'oauth_nonce'] = oauth_signature.pop(u'OAuth oauth_nonce')
# oauthlib 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:
oauth_signature[u'oauth_signature'] = urllib.unquote(oauth_signature[u'oauth_signature']).decode('utf8')
# Add LTI parameters to OAuth parameters for sending in form.
lti_parameters.update(oauth_signature)
return lti_parameters
def get_result(self, user): # pylint: disable=unused-argument
"""
Helper request handler for GET requests to LTI 2.0 result endpoint
GET handler for lti_2_0_result. Assumes all authorization has been checked.
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object (unused)
real_user (django.contrib.auth.models.User): Actual user linked to anon_id in request path suffix
Returns:
webob.response: response to this request, in JSON format with status 200 if success
"""
self.xblock.runtime.rebind_noauth_module_to_user(self, user)
response = {
"@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
"@type": "Result"
}
if self.xblock.module_score is not None:
response['resultScore'] = round(self.xblock.module_score, 2)
response['comment'] = self.xblock.score_comment
return response
def delete_result(self, user): # pylint: disable=unused-argument
"""
Helper request handler for DELETE requests to LTI 2.0 result endpoint
DELETE handler for lti_2_0_result. Assumes all authorization has been checked.
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object (unused)
real_user (django.contrib.auth.models.User): Actual user linked to anon_id in request path suffix
Returns:
webob.response: response to this request. status 200 if success
"""
self.xblock.clear_user_module_score(user)
return {}
def put_result(self, user, result_json):
"""
Helper request handler for PUT requests to LTI 2.0 result endpoint
PUT handler for lti_2_0_result. Assumes all authorization has been checked.
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object
real_user (django.contrib.auth.models.User): Actual user linked to anon_id in request path suffix
Returns:
webob.response: response to this request. status 200 if success. 404 if body of PUT request is malformed
"""
score, comment = parse_result_json(result_json)
if score is None:
# According to http://www.imsglobal.org/lti/ltiv2p0/ltiIMGv2p0.html#_Toc361225514
# PUTting a JSON object with no "resultScore" field is equivalent to a DELETE.
self.xblock.clear_user_module_score(user)
else:
self.xblock.set_user_module_score(user, score, self.xblock.max_score(), comment)
return {}
def verify_result_headers(self, request, verify_content_type=True):
"""
Helper method to validate LTI 2.0 REST result service HTTP headers. returns if correct, else raises LtiError
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object
verify_content_type (bool): If true, verifies the content type of the request is that spec'ed by LTI 2.0
Returns:
nothing, but will only return if verification succeeds
Raises:
LtiError if verification fails
"""
content_type = request.headers.get('Content-Type')
if verify_content_type and content_type != LtiConsumer.CONTENT_TYPE_RESULT_JSON:
log.error("[LTI]: v2.0 result service -- bad Content-Type: %s", content_type)
raise LtiError(
"For LTI 2.0 result service, Content-Type must be %s. Got %s",
LtiConsumer.CONTENT_TYPE_RESULT_JSON,
content_type
)
__, secret = self.xblock.lti_provider_key_secret
try:
return verify_oauth_body_signature(request, secret, self.xblock.outcome_service_url)
except (ValueError, LtiError) as err:
log.error("[LTI]: v2.0 result service -- OAuth body verification failed: %s", err.message)
raise LtiError(err.message)
This diff is collapsed.
"""
Utility functions for working with OAuth signatures.
"""
import logging
import hashlib
import base64
import urllib
from oauthlib import oauth1
from .exceptions import LtiError
log = logging.getLogger(__name__)
class SignedRequest(object):
"""
Encapsulates request attributes needed when working
with the `oauthlib.oauth1` API
"""
def __init__(self, **kwargs):
self.uri = kwargs.get('uri')
self.http_method = kwargs.get('http_method')
self.params = kwargs.get('params')
self.oauth_params = kwargs.get('oauth_params')
self.headers = kwargs.get('headers')
self.body = kwargs.get('body')
self.decoded_body = kwargs.get('decoded_body')
self.signature = kwargs.get('signature')
def get_oauth_request_signature(key, secret, url, headers, body):
"""
Returns Authorization header for a signed oauth request.
Arguments:
key (str): LTI provider key
secret (str): LTI provider secret
url (str): URL for the signed request
header (str): HTTP headers for the signed request
body (str): Body of the signed request
Returns:
str: Authorization header for the OAuth signed request
"""
client = oauth1.Client(client_key=unicode(key), client_secret=unicode(secret))
try:
# Add Authorization header which looks like:
# Authorization: OAuth oauth_nonce="80966668944732164491378916897",
# oauth_timestamp="1378916897", oauth_version="1.0", oauth_signature_method="HMAC-SHA1",
# oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"
__, headers, __ = client.sign(
unicode(url.strip()),
http_method=u'POST',
body=body,
headers=headers
)
except ValueError: # Scheme not in url.
raise LtiError("Failed to sign oauth request")
return headers['Authorization']
def verify_oauth_body_signature(request, lti_provider_secret, service_url):
"""
Verify grade request from LTI provider using OAuth body signing.
Uses http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html::
This specification extends the OAuth signature to include integrity checks on HTTP request bodies
with content types other than application/x-www-form-urlencoded.
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request
lti_provider_secret (str): Secret key for the LTI provider
service_url (str): URL that the request was made to
content_type (str): HTTP content type of the request
Raises:
LtiError if request is incorrect.
"""
headers = {
'Authorization': unicode(request.headers.get('Authorization')),
'Content-Type': request.content_type,
}
sha1 = hashlib.sha1()
sha1.update(request.body)
oauth_body_hash = base64.b64encode(sha1.digest()) # pylint: disable=E1121
oauth_params = oauth1.rfc5849.signature.collect_parameters(headers=headers, exclude_oauth_signature=False)
oauth_headers = dict(oauth_params)
oauth_signature = oauth_headers.pop('oauth_signature')
mock_request_lti_1 = SignedRequest(
uri=unicode(urllib.unquote(service_url)),
http_method=unicode(request.method),
params=oauth_headers.items(),
signature=oauth_signature
)
mock_request_lti_2 = SignedRequest(
uri=unicode(urllib.unquote(request.url)),
http_method=unicode(request.method),
params=oauth_headers.items(),
signature=oauth_signature
)
if oauth_body_hash != oauth_headers.get('oauth_body_hash'):
log.error(
"OAuth body hash verification failed, provided: %s, "
"calculated: %s, for url: %s, body is: %s",
oauth_headers.get('oauth_body_hash'),
oauth_body_hash,
service_url,
request.body
)
raise LtiError("OAuth body hash verification is failed.")
if (not oauth1.rfc5849.signature.verify_hmac_sha1(mock_request_lti_1, lti_provider_secret) and not
oauth1.rfc5849.signature.verify_hmac_sha1(mock_request_lti_2, lti_provider_secret)):
log.error(
"OAuth signature verification failed, for "
"headers:%s url:%s method:%s",
oauth_headers,
service_url,
unicode(request.method)
)
raise LtiError("OAuth signature verification has failed.")
return True
def log_authorization_header(request, client_key, client_secret):
"""
Helper function that logs proper HTTP Authorization header for a given request
Used only in debug situations, this logs the correct Authorization header based on
the request header and body according to OAuth 1 Body signing
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object to log Authorization header for
Returns:
nothing
"""
sha1 = hashlib.sha1()
sha1.update(request.body)
oauth_body_hash = unicode(base64.b64encode(sha1.digest())) # pylint: disable=too-many-function-args
log.debug("[LTI] oauth_body_hash = %s", oauth_body_hash)
client = oauth1.Client(client_key, client_secret)
params = client.get_oauth_params(request)
params.append((u'oauth_body_hash', oauth_body_hash))
mock_request = SignedRequest(
uri=unicode(urllib.unquote(request.url)),
headers=request.headers,
body=u"",
decoded_body=u"",
oauth_params=params,
http_method=unicode(request.method),
)
sig = client.get_oauth_signature(mock_request)
mock_request.oauth_params.append((u'oauth_signature', sig))
__, headers, _ = client._render(mock_request) # pylint: disable=protected-access
log.debug(
"\n\n#### COPY AND PASTE AUTHORIZATION HEADER ####\n%s\n####################################\n\n",
headers['Authorization']
)
"""
This module adds support for the LTI Outcomes Management Service.
For more details see:
https://www.imsglobal.org/specs/ltiomv1p0
"""
import logging
import urllib
from lxml import etree
from xml.sax.saxutils import escape
from xblockutils.resources import ResourceLoader
from .exceptions import LtiError
from .oauth import verify_oauth_body_signature
log = logging.getLogger(__name__)
def parse_grade_xml_body(body):
"""
Parses values from the Outcome Service XML.
XML body should contain nsmap with namespace, that is specified in LTI specs.
Arguments:
body (str): XML Outcome Service request body
Returns:
tuple: imsx_messageIdentifier, sourcedId, score, action
Raises:
LtiError
if submitted score is outside the permitted range
if the XML is missing required entities
if there was a problem parsing the XML body
"""
lti_spec_namespace = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"
namespaces = {'def': lti_spec_namespace}
data = body.strip().encode('utf-8')
try:
parser = etree.XMLParser(ns_clean=True, recover=True, encoding='utf-8') # pylint: disable=no-member
root = etree.fromstring(data, parser=parser) # pylint: disable=no-member
except etree.XMLSyntaxError as ex:
raise LtiError(ex.message or 'Body is not valid XML')
try:
imsx_message_identifier = root.xpath("//def:imsx_messageIdentifier", namespaces=namespaces)[0].text or ''
except IndexError:
raise LtiError('Failed to parse imsx_messageIdentifier from XML request body')
try:
body = root.xpath("//def:imsx_POXBody", namespaces=namespaces)[0]
except IndexError:
raise LtiError('Failed to parse imsx_POXBody from XML request body')
try:
action = body.getchildren()[0].tag.replace('{' + lti_spec_namespace + '}', '')
except IndexError:
raise LtiError('Failed to parse action from XML request body')
try:
sourced_id = root.xpath("//def:sourcedId", namespaces=namespaces)[0].text
except IndexError:
raise LtiError('Failed to parse sourcedId from XML request body')
try:
score = root.xpath("//def:textString", namespaces=namespaces)[0].text
except IndexError:
raise LtiError('Failed to parse score textString from XML request body')
# Raise exception if score is not float or not in range 0.0-1.0 regarding spec.
score = float(score)
if not 0.0 <= score <= 1.0:
raise LtiError('score value outside the permitted range of 0.0-1.0')
return imsx_message_identifier, sourced_id, score, action
class OutcomeService(object):
"""
Service for handling LTI Outcome Management Service requests.
For more details see:
https://www.imsglobal.org/specs/ltiomv1p0
"""
def __init__(self, xblock):
self.xblock = xblock
def handle_request(self, request):
"""
Handler for Outcome Service requests.
Parses and validates XML request body. Currently, only the
replaceResultRequest action is supported.
Example of request body from LTI provider::
<?xml version = "1.0" encoding = "UTF-8"?>
<imsx_POXEnvelopeRequest xmlns = "some_link (may be not required)">
<imsx_POXHeader>
<imsx_POXRequestHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>528243ba5241b</imsx_messageIdentifier>
</imsx_POXRequestHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<replaceResultRequest>
<resultRecord>
<sourcedGUID>
<sourcedId>feb-123-456-2929::28883</sourcedId>
</sourcedGUID>
<result>
<resultScore>
<language>en-us</language>
<textString>0.4</textString>
</resultScore>
</result>
</resultRecord>
</replaceResultRequest>
</imsx_POXBody>
</imsx_POXEnvelopeRequest>
See /templates/xml/outcome_service_response.xml for the response body format.
Arguments:
request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request
Returns:
str: Outcome Service XML response
"""
resource_loader = ResourceLoader(__name__)
response_xml_template = resource_loader.load_unicode('/templates/xml/outcome_service_response.xml')
# Returns when `action` is unsupported.
# Supported actions:
# - replaceResultRequest.
unsupported_values = {
'imsx_codeMajor': 'unsupported',
'imsx_description': 'Target does not support the requested operation.',
'imsx_messageIdentifier': 'unknown',
'response': ''
}
# Returns if:
# - past due grades are not accepted and grade is past due
# - score is out of range
# - can't parse response from TP;
# - can't verify OAuth signing or OAuth signing is incorrect.
failure_values = {
'imsx_codeMajor': 'failure',
'imsx_description': 'The request has failed.',
'imsx_messageIdentifier': 'unknown',
'response': ''
}
if not self.xblock.accept_grades_past_due and self.xblock.is_past_due:
failure_values['imsx_description'] = "Grade is past due"
return response_xml_template.format(**failure_values)
try:
imsx_message_identifier, sourced_id, score, action = parse_grade_xml_body(request.body)
except LtiError as ex: # pylint: disable=no-member
body = escape(request.body) if request.body else ''
error_message = "Request body XML parsing error: {} {}".format(ex.message, body)
log.debug("[LTI]: %s" + error_message)
failure_values['imsx_description'] = error_message
return response_xml_template.format(**failure_values)
# Verify OAuth signing.
__, secret = self.xblock.lti_provider_key_secret
try:
verify_oauth_body_signature(request, secret, self.xblock.outcome_service_url)
except (ValueError, LtiError) as ex:
failure_values['imsx_messageIdentifier'] = escape(imsx_message_identifier)
error_message = "OAuth verification error: " + escape(ex.message)
failure_values['imsx_description'] = error_message
log.debug("[LTI]: " + error_message)
return response_xml_template.format(**failure_values)
real_user = self.xblock.runtime.get_real_user(urllib.unquote(sourced_id.split(':')[-1]))
if not real_user: # that means we can't save to database, as we do not have real user id.
failure_values['imsx_messageIdentifier'] = escape(imsx_message_identifier)
failure_values['imsx_description'] = "User not found."
return response_xml_template.format(**failure_values)
if action == 'replaceResultRequest':
self.xblock.set_user_module_score(real_user, score, self.xblock.max_score())
values = {
'imsx_codeMajor': 'success',
'imsx_description': 'Score for {sourced_id} is now {score}'.format(sourced_id=sourced_id, score=score),
'imsx_messageIdentifier': escape(imsx_message_identifier),
'response': '<replaceResultResponse/>'
}
log.debug("[LTI]: Grade is saved.")
return response_xml_template.format(**values)
unsupported_values['imsx_messageIdentifier'] = escape(imsx_message_identifier)
log.debug("[LTI]: Incorrect action.")
return response_xml_template.format(**unsupported_values)
.xblock-student_view.xblock-student_view-lti_consumer h2.problem-header{display:inline-block}.xblock-student_view.xblock-student_view-lti_consumer div.problem-progress{display:inline-block;padding-left:5px;color:#666;font-weight:100;font-size:1em}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer{margin:0 auto}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer .wrapper-lti-link{font-size:14px;background-color:#f6f6f6;padding:20px}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer .wrapper-lti-link .lti-link{margin-bottom:0;text-align:right}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer .wrapper-lti-link .lti-link button{font-size:13px;line-height:20.72px}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer .lti-modal{top:80px !important}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer .lti-modal .inner-wrapper{height:100%;padding:0 0 0 0}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer form.ltiLaunchForm{display:none}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer iframe.ltiLaunchFrame{width:100%;height:100%;display:block;border:0px}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer h4.problem-feedback-label{font-weight:100;font-size:1em;font-family:"Source Sans", "Open Sans", Verdana, Geneva, sans-serif, sans-serif}.xblock-student_view.xblock-student_view-lti_consumer div.lti_consumer div.problem-feedback{margin-top:5px;margin-bottom:5px}
function LtiConsumerXBlock(runtime, element) {
$(function ($) {
// Adapted from leanModal v1.1 by Ray Stone - http://finelysliced.com.au
// Dual licensed under the MIT and GPL
// Renamed leanModal to iframeModal to avoid clash with platform-provided leanModal
// which removes the href attribute from iframe elements upon modal closing
$.fn.extend({
iframeModal: function (options) {
var $trigger = $(this);
var defaults = {top: 100, overlay: 0.5, closeButton: null};
var overlay = $("<div id='lean_overlay'></div>");
$("body").append(overlay);
options = $.extend(defaults, options);
return this.each(function () {
var o = options;
$(this).click(function (e) {
var modal_id = $(this).data("target");
$("#lean_overlay").click(function () {
close_modal(modal_id)
});
$(o.closeButton).click(function () {
close_modal(modal_id)
});
var modal_height = $(modal_id).outerHeight();
var modal_width = $(modal_id).outerWidth();
$("#lean_overlay").css({"display": "block", opacity: 0});
$("#lean_overlay").fadeTo(200, o.overlay);
$(modal_id).css({
"display": "block",
"position": "fixed",
"opacity": 0,
"z-index": 11000,
"left": 50 + "%",
"margin-left": -(modal_width / 2) + "px",
"top": o.top + "px"
});
$(modal_id).fadeTo(200, 1);
e.preventDefault();
/* Manage focus for modal dialog */
var iframe = $(modal_id).find('iframe')[0].contentWindow;
/* Set focus on close button */
$(o.closeButton).focus();
/* Redirect close button to iframe */
$(o.closeButton).on('keydown', function (e) {
if (e.which === 9) {
e.preventDefault();
$(modal_id).find('iframe')[0].contentWindow.focus();
}
});
/* Redirect non-iframe tab to close button */
var $inputs = $('select, input, textarea, button, a').filter(':visible').not(o.closeButton);
$inputs.on('focus', function(e) {
e.preventDefault();
$(options.closeButton).focus();
});
});
});
function close_modal(modal_id) {
$('select, input, textarea, button, a').off('focus');
$("#lean_overlay").fadeOut(200);
$(modal_id).css({"display": "none"})
$trigger.focus();
}
}
});
var $element = $(element);
var $ltiContainer = $element.find('.lti-consumer-container');
var askToSendUsername = $ltiContainer.data('ask-to-send-username') == 'True';
var askToSendEmail = $ltiContainer.data('ask-to-send-email') == 'True';
// Apply click handler to modal launch button
$element.find('.btn-lti-modal').iframeModal({top: 200, closeButton: '.close-modal'});
// Apply click handler to new window launch button
$element.find('.btn-lti-new-window').click(function(){
var launch = true;
// If this instance is configured to require username and/or email, ask user if it is okay to send them
// Do not launch if it is not okay
if(askToSendUsername && askToSendEmail) {
launch = confirm(gettext("Click OK to have your username and e-mail address sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information."));
} else if (askToSendUsername) {
launch = confirm(gettext("Click OK to have your username sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information."));
} else if (askToSendEmail) {
launch = confirm(gettext("Click OK to have your e-mail address sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information."));
}
if (launch) {
window.open($(this).data('target'));
}
});
});
}
.xblock-student_view.xblock-student_view-lti_consumer {
h2.problem-header {
display: inline-block;
}
div.problem-progress {
display: inline-block;
padding-left: 5px;
color: #666;
font-weight: 100;
font-size: 1em;
}
div.lti_consumer {
margin: 0 auto;
.wrapper-lti-link {
font-size: 14px;
background-color: #f6f6f6;
padding: 20px;
.lti-link {
margin-bottom: 0;
text-align: right;
button {
font-size: 13px;
line-height: 20.72px;
}
}
}
.lti-modal {
top: 80px !important;
.inner-wrapper {
height: 100%;
padding: 0 0 0 0;
}
}
form.ltiLaunchForm {
display: none;
}
iframe.ltiLaunchFrame {
width: 100%;
height: 100%;
display: block;
border: 0px;
}
h4.problem-feedback-label {
font-weight: 100;
font-size: 1em;
font-family: "Source Sans", "Open Sans", Verdana, Geneva, sans-serif, sans-serif;
}
div.problem-feedback {
margin-top: 5px;
margin-bottom: 5px;
}
}
}
<iframe
title="External Tool Content"
class="ltiLaunchFrame"
name="ltiFrame-${element_id}"
src="${form_url}"
allowfullscreen="true"
webkitallowfullscreen="true"
mozallowfullscreen="true"
></iframe>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>LTI</title>
</head>
<body>
## This form will be hidden.
## LTI module JavaScript will trigger a "submit" on the form, and the
## result will be rendered instead.
<form
id="lti-${element_id}"
action="${launch_url}"
method="post"
encType="application/x-www-form-urlencoded"
style="display:none;"
>
% for param_name, param_value in lti_parameters.items():
<input name="${param_name}" value="${param_value}" />
% endfor
<input type="submit" value="Press to Launch" />
</form>
<script type="text/javascript">
(function (d) {
var element = d.getElementById("lti-${element_id}");
if (element) {
element.submit();
}
}(document));
</script>
</body>
</html>
<h2 class="problem-header">
## Translators: "External resource" means that this learning module is hosted on a platform external to the edX LMS
${display_name} (External resource)
</h2>
% if has_score and weight:
<div class="problem-progress">
% if module_score is not None:
(${"{points} / {total_points} points".format(points=module_score, total_points=weight)})
% else:
(${"{total_points} points possible".format(total_points=weight)})
% endif
</div>
% endif
<div
id="${element_id}"
class="${element_class} lti-consumer-container"
data-ask-to-send-username="${ask_to_send_username}"
data-ask-to-send-email="${ask_to_send_email}"
>
% if launch_url and not hide_launch:
% if launch_target in ['modal', 'new_window']:
<section class="wrapper-lti-link">
% if description:
<div class="lti-description">${description}</div>
% endif
<p class="lti-link external">
% if launch_target == 'modal':
<button
class="btn btn-pl-primary btn-base btn-lti-modal"
data-target="#${element_id + '-lti-modal'}"
>
${button_text or 'View resource in a modal window'} <i class="icon fa fa-external-link"></i>
</button>
% else:
<button
class="btn btn-pl-primary btn-base btn-lti-new-window"
data-target="${form_url}"
>
${button_text or 'View resource in a new window'} <i class="icon fa fa-external-link"></i>
</button>
% endif
</p>
</section>
% endif
% if launch_target == 'modal':
<section
id="${element_id}-lti-modal"
class="modal lti-modal"
aria-hidden="true"
style="width:${modal_width}px; height:${modal_height}px;"
>
<div class="inner-wrapper" role="dialog" aria-labelledby="lti-modal-title">
<button class="close-modal" tabindex="1">
<i class="icon fa fa-remove"></i>
<span class="sr">Close</span>
</button>
## The result of the LTI launch form submit will be rendered here.
<%include file="templates/html/lti_iframe.html"/>
</div>
</section>
% endif
% if launch_target == 'iframe':
<div style="height:${inline_height}px;">
## The result of the LTI launch form submit will be rendered here.
<%include file="templates/html/lti_iframe.html"/>
</div>
% endif
% elif not hide_launch:
<h3 class="error_message">
Please provide launch_url. Click "Edit", and fill in the required fields.
</h3>
% endif
% if has_score and comment:
<h4 class="problem-feedback-label">Feedback on your work from the grader:</h4>
<div class="problem-feedback">
## sanitized with bleach in view
${comment}
</div>
% endif
</div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment