diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index deef87a403..724dc439d9 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -52,8 +52,7 @@ NOTE_COMPONENT_TYPES = ['notes'] ADVANCED_COMPONENT_TYPES = [ 'annotatable', 'word_cloud', - 'graphical_slider_tool', - 'lti', + 'graphical_slider_tool' ] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 45ad7a7424..ccbb7fb5bb 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -81,6 +81,7 @@ def preview_component(request, location): component, 'xmodule_edit.html' ) + return render_to_response('component.html', { 'preview': get_preview_html(request, component, 0), 'editor': component.runtime.render(component, None, 'studio_view').content, @@ -103,6 +104,7 @@ def preview_module_system(request, preview_id, descriptor): return lms_field_data(descriptor._field_data, student_data) course_id = get_course_for_item(descriptor.location).location.course_id + return ModuleSystem( ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), # TODO (cpennington): Do we want to track how instructors are using the preview problems? @@ -116,8 +118,6 @@ def preview_module_system(request, preview_id, descriptor): xblock_field_data=preview_field_data, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), mixins=settings.XBLOCK_MIXINS, - course_id=course_id, - anonymous_student_id='student' ) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 6a24bf8f27..704de15ea7 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -56,7 +56,6 @@ setup( "hidden = xmodule.hidden_module:HiddenDescriptor", "raw = xmodule.raw_module:RawDescriptor", "crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor", - "lti = xmodule.lti_module:LTIModuleDescriptor" ], 'console_scripts': [ 'xmodule_assets = xmodule.static_content:main', diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 658a095d14..aca804d5e2 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -153,7 +153,6 @@ class TextbookList(List): class CourseFields(object): - lti_passports = List(help="LTI tools passports as id:client_key:client_secret", scope=Scope.settings) textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content) wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content) diff --git a/common/lib/xmodule/xmodule/css/lti/lti.scss b/common/lib/xmodule/xmodule/css/lti/lti.scss deleted file mode 100644 index 97a8f62d54..0000000000 --- a/common/lib/xmodule/xmodule/css/lti/lti.scss +++ /dev/null @@ -1,30 +0,0 @@ -div.lti { - // align center - margin: 0 auto; - - h3.error_message { - display: block; - } - - form.ltiLaunchForm { - display: none; - } - - iframe.ltiLaunchFrame { - width: 100%; - height: 800px; - display: none; - border: 0px; - overflow-x: hidden; - } - - &.rendered { - iframe.ltiLaunchFrame { - display: block; - } - - h3.error_message { - display: none; - } - } -} diff --git a/common/lib/xmodule/xmodule/js/fixtures/lti.html b/common/lib/xmodule/xmodule/js/fixtures/lti.html deleted file mode 100644 index e5e7ab3f3f..0000000000 --- a/common/lib/xmodule/xmodule/js/fixtures/lti.html +++ /dev/null @@ -1,40 +0,0 @@ -
- -
- - - - - - - - - - - - - - - - -
- -

- Please provide launch_url. Click "Edit", and fill in the - required fields. -

- - - -
diff --git a/common/lib/xmodule/xmodule/js/spec/lti/constructor.js b/common/lib/xmodule/xmodule/js/spec/lti/constructor.js deleted file mode 100644 index 0a73496bed..0000000000 --- a/common/lib/xmodule/xmodule/js/spec/lti/constructor.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * File: constructor.js - * - * Purpose: Jasmine tests for LTI module (front-end part). - * - * - * The front-end part of the LTI module is really simple. If an action - * is set for the hidden LTI form, then it is submited, and the results are - * redirected to an iframe. - * - * We will test that the form is only submited when the action is set (i.e. - * not empty). - * - * Other aspects of LTI module will be covered by Python unit tests and - * acceptance tests. - * - */ - -/* - * "Hence that general is skilful in attack whose opponent does not know what - * to defend; and he is skilful in defense whose opponent does not know what - * to attack." - * - * ~ Sun Tzu - */ - -(function () { - describe('LTI', function () { - describe('constructor', function () { - describe('before settings were filled in', function () { - var element, errorMessage, frame; - - // This function will be executed before each of the it() specs - // in this suite. - beforeEach(function () { - loadFixtures('lti.html'); - - element = $('#lti_id'); - errorMessage = element.find('.error_message'); - form = element.find('.ltiLaunchForm'); - frame = element.find('.ltiLaunchFrame'); - - spyOnEvent(form, 'submit'); - - LTI(element); - }); - - it( - 'when URL setting is filled form is not submited', - function () { - - expect('submit').not.toHaveBeenTriggeredOn(form); - }); - }); - - describe('After the settings were filled in', function () { - var element, errorMessage, frame; - - // This function will be executed before each of the it() specs - // in this suite. - beforeEach(function () { - loadFixtures('lti.html'); - - element = $('#lti_id'); - errorMessage = element.find('.error_message'); - form = element.find('.ltiLaunchForm'); - frame = element.find('.ltiLaunchFrame'); - - spyOnEvent(form, 'submit'); - - // The user "fills in" the necessary settings, and the - // form will get an action URL. - form.attr('action', 'http://www.example.com/'); - - LTI(element); - }); - - it('when URL setting is filled form is submited', function () { - expect('submit').toHaveBeenTriggeredOn(form); - }); - }); - }); - }); -}()); diff --git a/common/lib/xmodule/xmodule/js/src/lti/lti.js b/common/lib/xmodule/xmodule/js/src/lti/lti.js deleted file mode 100644 index e5b6885e1b..0000000000 --- a/common/lib/xmodule/xmodule/js/src/lti/lti.js +++ /dev/null @@ -1,26 +0,0 @@ -window.LTI = (function () { - // Function initialize(element) - // - // Initialize the LTI iframe. - function initialize(element) { - var form; - - // In cms (Studio) the element is already a jQuery object. In lms it is - // a DOM object. - // - // To make sure that there is no error, we pass it through the $() - // function. This will make it a jQuery object if it isn't already so. - element = $(element); - - form = element.find('.ltiLaunchForm'); - - // If the Form's action attribute is set (i.e. we can perform a normal - // submit), then we submit the form and make the frame shown. - if (form.attr('action')) { - form.submit(); - element.find('.lti').addClass('rendered') - } - } - - return initialize; -}()); diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py deleted file mode 100644 index bc07cea97e..0000000000 --- a/common/lib/xmodule/xmodule/lti_module.py +++ /dev/null @@ -1,249 +0,0 @@ -""" -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 LTIError(Exception): - pass - - -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. - - 3. 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. - - Oauth data which is generated after signing is usual:: - - oauth_callback - oauth_nonce - oauth_consumer_key - oauth_signature_method - oauth_timestamp - oauth_version - - - 4. All that data is passed to form and sent to LTI provider server by browser via - autosubmit via javascript. - - Form example:: - -
- - - - - - - - - - - - - - - - - - - - -
- - 5. 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.lti_passports: - try: - lti_id, key, secret = lti_passport.split(':') - except ValueError: - raise LTIError('Could not parse LTI passport: {0!r}. \ - 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('=', 1) - except ValueError: - raise LTIError('Could not parse custom parameter: {0!r}. \ - Should be "x=y" string.'.format(custom_parameter)) - - # LTI specs: 'custom_' should be prepended before each custom parameter - custom_parameters[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): - """ - LTIModuleDescriptor provides no export/import to xml. - """ - module_class = LTIModule diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index c8228c5e3e..4ad801aef8 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -27,7 +27,7 @@ class XModuleCourseFactory(Factory): store = editable_modulestore('direct') # Write the data to the mongo datastore - new_course = store.create_xmodule(location, metadata=kwargs.get('metadata', None)) + new_course = store.create_xmodule(location) # This metadata code was copied from cms/djangoapps/contentstore/views.py if display_name is not None: diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index b7e5ea8435..fefa668a56 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -40,7 +40,7 @@ open_ended_grading_interface = { } -def get_test_system(course_id=''): +def get_test_system(): """ Construct a test ModuleSystem instance. @@ -66,8 +66,7 @@ def get_test_system(course_id=''): node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), xblock_field_data=lambda descriptor: descriptor._field_data, anonymous_student_id='student', - open_ended_grading_interface=open_ended_grading_interface, - course_id=course_id, + open_ended_grading_interface=open_ended_grading_interface ) diff --git a/docs/developers/source/xmodule.rst b/docs/developers/source/xmodule.rst index 77ee2ea684..008a1303d2 100644 --- a/docs/developers/source/xmodule.rst +++ b/docs/developers/source/xmodule.rst @@ -95,14 +95,6 @@ Html :members: :show-inheritance: - -LTI -=== - -.. automodule:: xmodule.lti_module - :members: - :show-inheritance: - Mako ==== diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature deleted file mode 100644 index abdcfdb704..0000000000 --- a/lms/djangoapps/courseware/features/lti.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: LTI component - As a student, I want to view LTI component in LMS. - - Scenario: LTI component in LMS is not rendered - Given the course has correct LTI credentials - And the course has an LTI component with incorrect fields - Then I view the LTI and it is not rendered - - Scenario: LTI component in LMS is rendered - Given the course has correct LTI credentials - And the course has an LTI component filled with correct fields - Then I view the LTI and it is rendered - - Scenario: LTI component in LMS is rendered incorrectly - Given the course has incorrect LTI credentials - And the course has an LTI component filled with correct fields - Then I view the LTI but incorrect_signature warning is rendered \ No newline at end of file diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py deleted file mode 100644 index 0e91d5ed02..0000000000 --- a/lms/djangoapps/courseware/features/lti.py +++ /dev/null @@ -1,188 +0,0 @@ -#pylint: disable=C0111 - -from django.contrib.auth.models import User -from lettuce import world, step -from lettuce.django import django_url -from common import course_id - -from student.models import CourseEnrollment - - -@step('I view the LTI and it is not rendered$') -def lti_is_not_rendered(_step): - # lti div has no class rendered - assert world.is_css_not_present('div.lti.rendered') - - # error is shown - assert world.css_visible('.error_message') - - # iframe is not visible - assert not world.css_visible('iframe') - - #inside iframe test content is not presented - with world.browser.get_iframe('ltiLaunchFrame') as iframe: - # iframe does not contain functions from terrain/ui_helpers.py - assert iframe.is_element_not_present_by_css('.result', wait_time=5) - - -@step('I view the LTI and it is rendered$') -def lti_is_rendered(_step): - # lti div has class rendered - assert world.is_css_present('div.lti.rendered') - - # error is hidden - assert not world.css_visible('.error_message') - - # iframe is visible - assert world.css_visible('iframe') - - #inside iframe test content is presented - with world.browser.get_iframe('ltiLaunchFrame') as iframe: - # iframe does not contain functions from terrain/ui_helpers.py - assert iframe.is_element_present_by_css('.result', wait_time=5) - assert ("This is LTI tool. Success." == world.retry_on_exception( - lambda: iframe.find_by_css('.result')[0].text, - max_attempts=5 - )) - - -@step('I view the LTI but incorrect_signature warning is rendered$') -def incorrect_lti_is_rendered(_step): - # lti div has class rendered - assert world.is_css_present('div.lti.rendered') - - # error is hidden - assert not world.css_visible('.error_message') - - # iframe is visible - assert world.css_visible('iframe') - - #inside iframe test content is presented - with world.browser.get_iframe('ltiLaunchFrame') as iframe: - # iframe does not contain functions from terrain/ui_helpers.py - assert iframe.is_element_present_by_css('.result', wait_time=5) - assert ("Wrong LTI signature" == world.retry_on_exception( - lambda: iframe.find_by_css('.result')[0].text, - max_attempts=5 - )) - - -@step('the course has correct LTI credentials$') -def set_correct_lti_passport(_step): - coursenum = 'test_course' - metadata = { - 'lti_passports': ["correct_lti_id:{}:{}".format( - world.lti_server.oauth_settings['client_key'], - world.lti_server.oauth_settings['client_secret'] - )] - } - i_am_registered_for_the_course(coursenum, metadata) - - -@step('the course has incorrect LTI credentials$') -def set_incorrect_lti_passport(_step): - coursenum = 'test_course' - metadata = { - 'lti_passports': ["test_lti_id:{}:{}".format( - world.lti_server.oauth_settings['client_key'], - "incorrect_lti_secret_key" - )] - } - i_am_registered_for_the_course(coursenum, metadata) - - -@step('the course has an LTI component filled with correct fields$') -def add_correct_lti_to_course(_step): - category = 'lti' - world.ItemFactory.create( - # parent_location=section_location(course), - parent_location=world.scenario_dict['SEQUENTIAL'].location, - category=category, - display_name='LTI', - metadata={ - 'lti_id': 'correct_lti_id', - 'launch_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint'] - } - ) - course = world.scenario_dict["COURSE"] - chapter_name = world.scenario_dict['SECTION'].display_name.replace( - " ", "_") - section_name = chapter_name - path = "/courses/{org}/{num}/{name}/courseware/{chapter}/{section}".format( - org=course.org, - num=course.number, - name=course.display_name.replace(' ', '_'), - chapter=chapter_name, - section=section_name) - url = django_url(path) - - world.browser.visit(url) - - -@step('the course has an LTI component with incorrect fields$') -def add_incorrect_lti_to_course(_step): - category = 'lti' - world.ItemFactory.create( - parent_location=world.scenario_dict['SEQUENTIAL'].location, - category=category, - display_name='LTI', - metadata={ - 'lti_id': 'incorrect_lti_id', - 'lti_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint'] - } - ) - course = world.scenario_dict["COURSE"] - chapter_name = world.scenario_dict['SECTION'].display_name.replace( - " ", "_") - section_name = chapter_name - path = "/courses/{org}/{num}/{name}/courseware/{chapter}/{section}".format( - org=course.org, - num=course.number, - name=course.display_name.replace(' ', '_'), - chapter=chapter_name, - section=section_name) - url = django_url(path) - - world.browser.visit(url) - - -def create_course(course, metadata): - - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - world.scenario_dict['COURSE'] = world.CourseFactory.create( - org='edx', - number=course, - display_name='Test Course', - metadata=metadata - ) - - # Add a section to the course to contain problems - world.scenario_dict['SECTION'] = world.ItemFactory.create( - parent_location=world.scenario_dict['COURSE'].location, - display_name='Test Section' - ) - world.scenario_dict['SEQUENTIAL'] = world.ItemFactory.create( - parent_location=world.scenario_dict['SECTION'].location, - category='sequential', - display_name='Test Section') - - -def i_am_registered_for_the_course(course, metadata): - # Create the course - create_course(course, metadata) - - # Create the user - world.create_user('robot', 'test') - usr = User.objects.get(username='robot') - - # If the user is not already enrolled, enroll the user. - CourseEnrollment.enroll(usr, course_id(course)) - - world.log_in(username='robot', password='test') diff --git a/lms/djangoapps/courseware/features/lti_setup.py b/lms/djangoapps/courseware/features/lti_setup.py deleted file mode 100644 index 0a6c4590dd..0000000000 --- a/lms/djangoapps/courseware/features/lti_setup.py +++ /dev/null @@ -1,50 +0,0 @@ -#pylint: disable=C0111 -#pylint: disable=W0621 - -from courseware.mock_lti_server.mock_lti_server import MockLTIServer -from lettuce import before, after, world -from django.conf import settings -import threading - -from logging import getLogger -logger = getLogger(__name__) - - -@before.all -def setup_mock_lti_server(): - - server_host = '127.0.0.1' - - # Add +1 to XQUEUE random port number - server_port = settings.XQUEUE_PORT + 1 - - address = (server_host, server_port) - - # Create the mock server instance - server = MockLTIServer(address) - logger.debug("LTI server started at {} port".format(str(server_port))) - # Start the server running in a separate daemon thread - # Because the thread is a daemon, it will terminate - # when the main thread terminates. - server_thread = threading.Thread(target=server.serve_forever) - server_thread.daemon = True - server_thread.start() - - server.oauth_settings = { - 'client_key': 'test_client_key', - 'client_secret': 'test_client_secret', - 'lti_base': 'http://{}:{}/'.format(server_host, server_port), - 'lti_endpoint': 'correct_lti_endpoint' - } - - # Store the server instance in lettuce's world - # so that other steps can access it - # (and we can shut it down later) - world.lti_server = server - - -@after.all -def teardown_mock_lti_server(total): - - # Stop the LTI server and free up the port - world.lti_server.shutdown() diff --git a/lms/djangoapps/courseware/mock_lti_server/__init__.py b/lms/djangoapps/courseware/mock_lti_server/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py b/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py deleted file mode 100644 index ba9cea84d6..0000000000 --- a/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py +++ /dev/null @@ -1,167 +0,0 @@ -from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler -import urlparse -from requests.packages.oauthlib.oauth1.rfc5849 import signature -import mock -from logging import getLogger -logger = getLogger(__name__) - - -class MockLTIRequestHandler(BaseHTTPRequestHandler): - ''' - A handler for LTI POST requests. - ''' - - protocol = "HTTP/1.0" - - def do_HEAD(self): - self._send_head() - - def do_POST(self): - ''' - Handle a POST request from the client and sends response back. - ''' - self._send_head() - - post_dict = self._post_dict() # Retrieve the POST data - - logger.debug("LTI provider received POST request {} to path {}".format( - str(post_dict), - self.path) - ) # Log the request - - # Respond only to requests with correct lti endpoint: - if self._is_correct_lti_request(): - correct_keys = [ - 'user_id', - 'role', - 'oauth_nonce', - 'oauth_timestamp', - 'oauth_consumer_key', - 'lti_version', - 'oauth_signature_method', - 'oauth_version', - 'oauth_signature', - 'lti_message_type', - 'oauth_callback', - 'lis_outcome_service_url', - 'lis_result_sourcedid', - 'launch_presentation_return_url' - ] - - if sorted(correct_keys) != sorted(post_dict.keys()): - status_message = "Incorrect LTI header" - else: - params = {k: v for k, v in post_dict.items() if k != 'oauth_signature'} - if self.server.check_oauth_signature(params, post_dict['oauth_signature']): - status_message = "This is LTI tool. Success." - else: - status_message = "Wrong LTI signature" - else: - status_message = "Invalid request URL" - - self._send_response(status_message) - - def _send_head(self): - ''' - Send the response code and MIME headers - ''' - if self._is_correct_lti_request(): - self.send_response(200) - else: - self.send_response(500) - - self.send_header('Content-type', 'text/html') - self.end_headers() - - def _post_dict(self): - ''' - Retrieve the POST parameters from the client as a dictionary - ''' - try: - length = int(self.headers.getheader('content-length')) - post_dict = urlparse.parse_qs(self.rfile.read(length), keep_blank_values=True) - # The POST dict will contain a list of values for each key. - # None of our parameters are lists, however, so we map [val] --> val. - # If the list contains multiple entries, we pick the first one - post_dict = {key: val[0] for key, val in post_dict.items()} - except: - # We return an empty dict here, on the assumption - # that when we later check that the request has - # the correct fields, it won't find them, - # and will therefore send an error response - return {} - return post_dict - - def _send_response(self, message): - ''' - Send message back to the client - ''' - response_str = """TEST TITLE - -

IFrame loaded

\ -

Server response is:

\ -

{}

- """.format(message) - - # Log the response - logger.debug("LTI: sent response {}".format(response_str)) - - self.wfile.write(response_str) - - def _is_correct_lti_request(self): - '''If url to LTI tool is correct.''' - return self.server.oauth_settings['lti_endpoint'] in self.path - - -class MockLTIServer(HTTPServer): - ''' - A mock LTI provider server that responds - to POST requests to localhost. - ''' - - def __init__(self, address): - ''' - Initialize the mock XQueue server instance. - - *address* is the (host, host's port to listen to) tuple. - ''' - handler = MockLTIRequestHandler - HTTPServer.__init__(self, address, handler) - - def shutdown(self): - ''' - Stop the server and free up the port - ''' - # First call superclass shutdown() - HTTPServer.shutdown(self) - # We also need to manually close the socket - self.socket.close() - - def check_oauth_signature(self, params, client_signature): - ''' - Checks oauth signature from client. - - `params` are params from post request except signature, - `client_signature` is signature from request. - - Builds mocked request and verifies hmac-sha1 signing:: - 1. builds string to sign from `params`, `url` and `http_method`. - 2. signs it with `client_secret` which comes from server settings. - 3. obtains signature after sign and then compares it with request.signature - (request signature comes form client in request) - - Returns `True` if signatures are correct, otherwise `False`. - - ''' - client_secret = unicode(self.oauth_settings['client_secret']) - url = self.oauth_settings['lti_base'] + self.oauth_settings['lti_endpoint'] - - request = mock.Mock() - - request.params = [(unicode(k), unicode(v)) for k, v in params.items()] - request.uri = unicode(url) - request.http_method = u'POST' - request.signature = unicode(client_signature) - - return signature.verify_hmac_sha1(request, client_secret) - diff --git a/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py b/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py deleted file mode 100644 index 99650d5faa..0000000000 --- a/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Test for Mock_LTI_Server -""" -import unittest -import threading -import urllib -from mock_lti_server import MockLTIServer - -from nose.plugins.skip import SkipTest - - -class MockLTIServerTest(unittest.TestCase): - ''' - A mock version of the LTI provider server that listens on a local - port and responds with pre-defined grade messages. - - Used for lettuce BDD tests in lms/courseware/features/lti.feature - ''' - - def setUp(self): - - # This is a test of the test setup, - # so it does not need to run as part of the unit test suite - # You can re-enable it by commenting out the line below - # raise SkipTest - - # Create the server - server_port = 8034 - server_host = '127.0.0.1' - address = (server_host, server_port) - self.server = MockLTIServer(address) - self.server.oauth_settings = { - 'client_key': 'test_client_key', - 'client_secret': 'test_client_secret', - 'lti_base': 'http://{}:{}/'.format(server_host, server_port), - 'lti_endpoint': 'correct_lti_endpoint' - } - # Start the server in a separate daemon thread - server_thread = threading.Thread(target=self.server.serve_forever) - server_thread.daemon = True - server_thread.start() - - def tearDown(self): - - # Stop the server, freeing up the port - self.server.shutdown() - - def test_request(self): - """ - Tests that LTI server processes request with right program - path, and responses with incorrect signature. - """ - request = { - 'user_id': 'default_user_id', - 'role': 'student', - 'oauth_nonce': '', - 'oauth_timestamp': '', - 'oauth_consumer_key': 'client_key', - 'lti_version': 'LTI-1p0', - 'oauth_signature_method': 'HMAC-SHA1', - 'oauth_version': '1.0', - 'oauth_signature': '', - 'lti_message_type': 'basic-lti-launch-request', - 'oauth_callback': 'about:blank', - 'launch_presentation_return_url': '', - 'lis_outcome_service_url': '', - 'lis_result_sourcedid': '' - } - - response_handle = urllib.urlopen( - self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint'], - urllib.urlencode(request) - ) - response = response_handle.read() - self.assertTrue('Wrong LTI signature' in response) diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index 0a4d3508b8..88129cc8d1 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -86,7 +86,7 @@ class BaseTestXmodule(ModuleStoreTestCase): data=self.DATA ) - self.runtime = get_test_system(course_id=self.course.id) + self.runtime = get_test_system() # Allow us to assert that the template was called in the same way from # different code paths while maintaining the type returned by render_template self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items())) diff --git a/lms/djangoapps/courseware/tests/test_lti.py b/lms/djangoapps/courseware/tests/test_lti.py deleted file mode 100644 index d2b4ea6867..0000000000 --- a/lms/djangoapps/courseware/tests/test_lti.py +++ /dev/null @@ -1,79 +0,0 @@ -"""LTI integration tests""" - -import requests -from . import BaseTestXmodule -from collections import OrderedDict -import mock - - -class TestLTI(BaseTestXmodule): - """ - Integration test for lti xmodule. - - It checks overall code, by assuring that context that goes to template is correct. - As part of that, checks oauth signature generation by mocking signing function of `requests` library. - """ - CATEGORY = "lti" - - def setUp(self): - """ - Mock oauth1 signing of requests library for testing. - """ - super(TestLTI, self).setUp() - mocked_nonce = u'135685044251684026041377608307' - mocked_timestamp = u'1234567890' - 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_nonce, - u'oauth_timestamp': mocked_timestamp, - u'oauth_consumer_key': u'', - u'oauth_signature_method': u'HMAC-SHA1', - u'oauth_version': u'1.0', - 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 here: - __, headers, __ = saved_sign(self, *args, **kwargs) - # we should replace nonce, timestamp and signed_signature in headers: - old = headers[u'Authorization'] - old_parsed = OrderedDict([param.strip().replace('"', '').split('=') for param in old.split(',')]) - old_parsed[u'OAuth oauth_nonce'] = mocked_nonce - 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 - - patcher = mock.patch.object(requests.auth.Client, "sign", mocked_sign) - patcher.start() - self.addCleanup(patcher.stop) - - def test_lti_constructor(self): - """ - Makes sure that all parameters extracted. - """ - 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(), - 'launch_url': '', # default value - } - self.assertDictEqual(generated_context, expected_context) diff --git a/lms/templates/lti.html b/lms/templates/lti.html deleted file mode 100644 index 3d97c8d808..0000000000 --- a/lms/templates/lti.html +++ /dev/null @@ -1,34 +0,0 @@ -
- - ## This form will be hidden. Once available on the client, the LTI - ## module JavaScript will trigget a "submit" on the form, and the - ## result will be rendered to the below iFrame. -
- - % for param_name, param_value in input_fields.items(): - - %endfor - - -
- -

- Please provide launch_url. Click "Edit", and fill in the - required fields. -

- - ## The result of the form submit will be rendered here. - - -