diff --git a/.gitignore b/.gitignore index 8ab4137cc4597cea6ecc55b08b47ab95f4eb6f37..0faecabf8ff1d55b19faef0bbe6bdc06845240eb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ codekit-config.json ### Testing artifacts .coverage var/ + +# virtualenvironment +venv/ \ No newline at end of file diff --git a/README.rst b/README.rst index d8f39068d807f4f8ffddbec5351dc0680ff07fd9..c4371d1b8c658f37c5cce90e390a1e750de690b6 100644 --- a/README.rst +++ b/README.rst @@ -15,6 +15,31 @@ root folder: $ pip install -r requirements.txt +Installing in Docker Devstack +----------------------------- + +Assuming that your ``devstack`` repo lives at ``~/code/devstack`` +and that ``edx-platform`` lives right alongside that directory, you'll want +to checkout ``xblock-lti-consumer`` and have it live in ``~/code/src/xblock-lti-consumer``. +This will make it so that you can access it inside an LMS container shell +and easily make modifications for local testing. + +Run ``make lms-shell`` from your ``devstack`` directory to enter a running LMS container. +Once in there, you can do the following to have your devstack pointing at a local development +version of ``xblock-lti-consumer``: + +.. code:: bash + + $ pushd /edx/src/xblock-lti-consumer + $ virtualenv venv/ + $ source venv/bin/activate + $ make install + $ make test # optional, if you want to see that everything works + $ deactivate + $ pushd # should take you back to /edx/app/edxapp/edx-platform + $ pip uninstall -y lti_consumer_xblock + $ pip install -e /edx/src/xblock-lti-consumer + Enabling in Studio ------------------ @@ -27,6 +52,30 @@ advanced settings. ``"lti_consumer"`` to the policy value list. 3. Click the "Save changes" button. +Testing Against an LTI Provider +------------------------------- + +http://lti.tools/saltire/ provides a "Test Tool Provider" service that allows +you to see messages sent by an LTI consumer. + +We have some useful documentation on how to set this up here: +http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/exercises_tools/lti_component.html#lti-authentication-information + +1. In Studio Advanced settings, set the value of the "LTI Passports" field to "test:test:secret" - + this will set the oauth client key and secret used to send a message to the test LTI provider. +2. Create an LTI Consumer problem in a course in studio (after enabling it in "advanced_modules" + as seen above). Make a unit, select "Advanced", then "LTI Consumer". +3. Click edit and fill in the following fields: + ``LTI ID``: "test" + ``LTI URL``: "http://lti.tools/saltire/tp" +4. Click save. The unit should refresh and you should see "Passed" in the "Verification" field of + the message tab in the LTI Tool Provider emulator. +5. Click the "Publish" button. +6. View the unit in your local LMS. If you get an ``ImportError: No module named lti_consumer``, you + should ``docker-compose restart lms`` (since we previously uninstalled the lti_consumer to get the + tests for this repo running inside an LMS container). From here, you can see the contents of the + messages that we are sending as an LTI Consumer in the "Message Parameters" part of the "Message" tab. + Workbench installation and settings ----------------------------------- diff --git a/lti_consumer/lti.py b/lti_consumer/lti.py index 01b379e5aa03dc4ec200b09c6bfedda7a4b3bcb1..367bb01282244fa73b97dcffc73246485d262080 100644 --- a/lti_consumer/lti.py +++ b/lti_consumer/lti.py @@ -9,6 +9,8 @@ import logging import urllib import json +from six import text_type + from .exceptions import LtiError from .oauth import get_oauth_request_signature, verify_oauth_body_signature @@ -116,19 +118,22 @@ class LtiConsumer(object): # 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, + text_type('user_id'): self.xblock.user_id, + text_type('oauth_callback'): text_type('about:blank'), + text_type('launch_presentation_return_url'): '', + text_type('lti_message_type'): text_type('basic-lti-launch-request'), + text_type('lti_version'): text_type('LTI-1p0'), + text_type('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, + text_type('resource_link_id'): self.xblock.resource_link_id, + text_type('lis_result_sourcedid'): self.xblock.lis_result_sourcedid, + + text_type('context_id'): self.xblock.context_id, + text_type('custom_component_display_name'): self.xblock.display_name, - u'context_id': self.xblock.context_id, - u'custom_component_display_name': self.xblock.display_name, + text_type('context_title'): self.xblock.course.display_name_with_default, + text_type('context_label'): self.xblock.course.display_org_with_default, } if self.xblock.due: @@ -138,7 +143,7 @@ class LtiConsumer(object): if self.xblock.has_score: lti_parameters.update({ - u'lis_outcome_service_url': self.xblock.outcome_service_url + text_type('lis_outcome_service_url'): self.xblock.outcome_service_url }) self.xblock.user_email = "" diff --git a/lti_consumer/tests/unit/test_lti.py b/lti_consumer/tests/unit/test_lti.py index 48ab03f5f505ffcae71d777503b732f6fe71b07e..16eb4385e035d27f87d3dcd5d19d9c8bacdb2561 100644 --- a/lti_consumer/tests/unit/test_lti.py +++ b/lti_consumer/tests/unit/test_lti.py @@ -7,6 +7,7 @@ import unittest from datetime import timedelta from mock import Mock, PropertyMock, patch +from six import text_type from django.utils import timezone @@ -153,30 +154,32 @@ class TestLtiConsumer(TestLtiConsumerXBlock): self.lti_consumer.xblock.graceperiod = timedelta(days=1) expected_lti_parameters = { - u'user_id': self.lti_consumer.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.lti_consumer.xblock.role, - u'resource_link_id': self.lti_consumer.xblock.resource_link_id, - u'lis_result_sourcedid': self.lti_consumer.xblock.lis_result_sourcedid, - u'context_id': self.lti_consumer.xblock.context_id, - u'lis_outcome_service_url': self.lti_consumer.xblock.outcome_service_url, - u'custom_component_display_name': self.lti_consumer.xblock.display_name, - u'custom_component_due_date': self.lti_consumer.xblock.due.strftime('%Y-%m-%d %H:%M:%S'), - u'custom_component_graceperiod': str(self.lti_consumer.xblock.graceperiod.total_seconds()), + text_type('user_id'): self.lti_consumer.xblock.user_id, + text_type('oauth_callback'): 'about:blank', + text_type('launch_presentation_return_url'): '', + text_type('lti_message_type'): 'basic-lti-launch-request', + text_type('lti_version'): 'LTI-1p0', + text_type('roles'): self.lti_consumer.xblock.role, + text_type('resource_link_id'): self.lti_consumer.xblock.resource_link_id, + text_type('lis_result_sourcedid'): self.lti_consumer.xblock.lis_result_sourcedid, + text_type('context_id'): self.lti_consumer.xblock.context_id, + text_type('lis_outcome_service_url'): self.lti_consumer.xblock.outcome_service_url, + text_type('custom_component_display_name'): self.lti_consumer.xblock.display_name, + text_type('custom_component_due_date'): self.lti_consumer.xblock.due.strftime('%Y-%m-%d %H:%M:%S'), + text_type('custom_component_graceperiod'): str(self.lti_consumer.xblock.graceperiod.total_seconds()), 'lis_person_sourcedid': 'edx', 'lis_person_contact_email_primary': 'edx@example.com', 'launch_presentation_locale': 'en', - u'custom_param_1': 'custom1', - u'custom_param_2': 'custom2', - u'oauth_nonce': 'fake_nonce', + text_type('custom_param_1'): 'custom1', + text_type('custom_param_2'): 'custom2', + text_type('oauth_nonce'): 'fake_nonce', 'oauth_timestamp': 'fake_timestamp', 'oauth_version': 'fake_version', 'oauth_signature_method': 'fake_method', 'oauth_consumer_key': 'fake_consumer_key', - 'oauth_signature': u'fake_signature' + 'oauth_signature': 'fake_signature', + text_type('context_label'): self.lti_consumer.xblock.course.display_org_with_default, + text_type('context_title'): self.lti_consumer.xblock.course.display_name_with_default, } self.lti_consumer.xblock.has_score = True self.lti_consumer.xblock.ask_to_send_username = True diff --git a/requirements.txt b/requirements.txt index f9ec186b1faa196dfd50f4613da990bd297a1c2e..d098ce2b1d5442c1c08bc3d0be071459c5f1c7f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ lxml bleach oauthlib mako +lazy git+https://github.com/edx/XBlock.git#egg=XBlock git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0 -e . diff --git a/setup.py b/setup.py index 25ab8271a30e3833c9e758a3e11d62a8b9307e03..4a8c5e57a7290b46d11c797be6540b16c0183f1b 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def package_data(pkg, roots): setup( name='lti_consumer-xblock', - version='1.1.7', + version='1.1.8', description='This XBlock implements the consumer side of the LTI specification.', packages=[ 'lti_consumer', diff --git a/test_requirements.txt b/test_requirements.txt index fc069cb5d1dba80043a34eb3ef139b3d955aa59c..a985a8b0eb3feedbc16abf31de9e77da49ba9189 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,6 +3,7 @@ django-nose==1.4.4 astroid==1.3.8 # Pinning to avoid backwards incompatibility issue with pylint/pylint-django coveralls +mock pep8 git+https://github.com/edx/django-pyfs.git@1.0.3#egg=django-pyfs==1.0.3 git+https://github.com/edx/edx-lint.git@v0.3.2#egg=edx_lint==0.3.2