diff --git a/.travis.yml b/.travis.yml index 87b99925874ef9a5fcb8504019eabacd44cc0232..6aeb63e88aff86e38fabba78b5503c041e912d42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,21 @@ language: python -python: "2.7" + +python: + - "2.7" + - "3.5" + +sudo: false + install: - "pip install six" - "make install" -sudo: false + script: - make quality - make test + branches: only: - master + after_success: coveralls diff --git a/lti_consumer/exceptions.py b/lti_consumer/exceptions.py index 16f3383ffd742e15d72fbe54f54f41fb2200cb1f..e80b55f55642ed82256d5f8ff78d88faa1001926 100644 --- a/lti_consumer/exceptions.py +++ b/lti_consumer/exceptions.py @@ -7,4 +7,3 @@ class LtiError(Exception): """ General error class for LTI XBlock. """ - pass diff --git a/lti_consumer/lti.py b/lti_consumer/lti.py index ea0b591a109b211070db5a43e906bc4831941b04..7fac705622a420135972e40cf64bcd2318e83682 100644 --- a/lti_consumer/lti.py +++ b/lti_consumer/lti.py @@ -5,16 +5,18 @@ For more details see: https://www.imsglobal.org/activity/learning-tools-interoperability """ -import logging -import urllib +from __future__ import absolute_import, unicode_literals + import json +import logging +import six.moves.urllib.error +import six.moves.urllib.parse from six import text_type from .exceptions import LtiError from .oauth import get_oauth_request_signature, verify_oauth_body_signature - log = logging.getLogger(__name__) @@ -83,14 +85,14 @@ def parse_result_json(json_str): log.error("[LTI] %s", msg) raise LtiError(msg) except (TypeError, ValueError) as err: - msg = "Could not convert resultScore to float: {}".format(err.message) + msg = "Could not convert resultScore to float: {}".format(str(err)) log.error("[LTI] %s", msg) raise LtiError(msg) return score, json_obj.get('comment', "") -class LtiConsumer(object): +class LtiConsumer(object): # pylint: disable=bad-option-value, useless-object-inheritance """ Limited implementation of the LTI 1.1/2.0 specification. @@ -202,7 +204,9 @@ class LtiConsumer(object): # 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') + oauth_signature[u'oauth_signature'] = six.moves.urllib.parse.unquote( + oauth_signature[u'oauth_signature'] + ) # Add LTI parameters to OAuth parameters for sending in form. lti_parameters.update(oauth_signature) @@ -290,15 +294,15 @@ class LtiConsumer(object): 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", + error_msg = "For LTI 2.0 result service, Content-Type must be {}. Got {}".format( LtiConsumer.CONTENT_TYPE_RESULT_JSON, content_type ) + raise LtiError(error_msg) __, 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) + log.error("[LTI]: v2.0 result service -- OAuth body verification failed: %s", str(err)) + raise LtiError(str(err)) diff --git a/lti_consumer/lti_consumer.py b/lti_consumer/lti_consumer.py index c7cec87198583045b130aae16fe0776182e37feb..9afcff06e812dd48a95ef2fbbe8236fc68045905 100644 --- a/lti_consumer/lti_consumer.py +++ b/lti_consumer/lti_consumer.py @@ -50,29 +50,29 @@ What is supported: GET / PUT / DELETE HTTP methods respectively """ +from __future__ import absolute_import, unicode_literals + import logging -import bleach import re -from importlib import import_module -import json -import urllib - from collections import namedtuple -from webob import Response +from importlib import import_module +import six.moves.urllib.error +import six.moves.urllib.parse +import six +import bleach from django.utils import timezone - -from xblock.core import String, Scope, List, XBlock +from webob import Response +from xblock.core import List, Scope, String, XBlock from xblock.fields import Boolean, Float, Integer from xblock.fragment import Fragment from xblock.validation import ValidationMessage - from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import StudioEditableXBlockMixin from .exceptions import LtiError -from .oauth import log_authorization_header from .lti import LtiConsumer +from .oauth import log_authorization_header from .outcomes import OutcomeService from .utils import _ @@ -155,7 +155,7 @@ def parse_handler_suffix(suffix): LaunchTargetOption = namedtuple('LaunchTargetOption', ['display_name', 'value']) -class LaunchTarget(object): +class LaunchTarget(object): # pylint: disable=bad-option-value, useless-object-inheritance """ Constants for launch_target field options """ @@ -476,7 +476,9 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): def validate_field_data(self, validation, data): if not isinstance(data.custom_parameters, list): _ = self.runtime.service(self, "i18n").ugettext - validation.add(ValidationMessage(ValidationMessage.ERROR, unicode(_("Custom Parameters must be a list")))) + validation.add(ValidationMessage(ValidationMessage.ERROR, six.text_type( + _("Custom Parameters must be a list") + ))) def get_settings(self): """ @@ -547,7 +549,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): context_id is an opaque identifier that uniquely identifies the context (e.g., a course) that contains the link being launched. """ - return unicode(self.course_id) # pylint: disable=no-member + return six.text_type(self.course_id) # pylint: disable=no-member @property def role(self): @@ -589,7 +591,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): user_id = self.runtime.anonymous_student_id if user_id is None: raise LtiError(self.ugettext("Could not get user id for current request")) - return unicode(urllib.quote(user_id)) + return six.text_type(six.moves.urllib.parse.quote(user_id)) @property def resource_link_id(self): @@ -623,7 +625,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): i4x-2-3-lti-31de800015cf4afb973356dbe81496df this part of resource_link_id: makes resource_link_id to be unique among courses inside same system. """ - return unicode(urllib.quote( + return six.text_type(six.moves.urllib.parse.quote( "{}-{}".format(self.runtime.hostname, self.location.html_id()) # pylint: disable=no-member )) @@ -638,7 +640,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): This field is generally optional, but is required for grading. """ return "{context}:{resource_link}:{user_id}".format( - context=urllib.quote(self.context_id), + context=six.moves.urllib.parse.quote(self.context_id), resource_link=self.resource_link_id, user_id=self.user_id ) @@ -701,7 +703,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): if param_name not in LTI_PARAMETERS: param_name = 'custom_' + param_name - custom_parameters[unicode(param_name)] = unicode(param_value) + custom_parameters[six.text_type(param_name)] = six.text_type(param_value) return custom_parameters @property @@ -849,7 +851,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): return Response(status=404) return Response( - json.dumps(response_body), + json_body=response_body, content_type=LtiConsumer.CONTENT_TYPE_RESULT_JSON, ) diff --git a/lti_consumer/oauth.py b/lti_consumer/oauth.py index c27e54ba930501357740fffe1e4cda966dae7294..7a01ebf0892ba69578f20cc4d0cbcdda81726854 100644 --- a/lti_consumer/oauth.py +++ b/lti_consumer/oauth.py @@ -2,20 +2,23 @@ Utility functions for working with OAuth signatures. """ -import logging -import hashlib +from __future__ import absolute_import, unicode_literals + import base64 -import urllib +import hashlib +import logging +import six.moves.urllib.error +import six.moves.urllib.parse +import six from oauthlib import oauth1 from .exceptions import LtiError - log = logging.getLogger(__name__) -class SignedRequest(object): +class SignedRequest(object): # pylint: disable=bad-option-value, useless-object-inheritance """ Encapsulates request attributes needed when working with the `oauthlib.oauth1` API @@ -45,14 +48,14 @@ def get_oauth_request_signature(key, secret, url, headers, body): Returns: str: Authorization header for the OAuth signed request """ - client = oauth1.Client(client_key=unicode(key), client_secret=unicode(secret)) + client = oauth1.Client(client_key=six.text_type(key), client_secret=six.text_type(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()), + _, headers, _ = client.sign( + six.text_type(url.strip()), http_method=u'POST', body=body, headers=headers @@ -83,7 +86,7 @@ def verify_oauth_body_signature(request, lti_provider_secret, service_url): """ headers = { - 'Authorization': unicode(request.headers.get('Authorization')), + 'Authorization': six.text_type(request.headers.get('Authorization')), 'Content-Type': request.content_type, } @@ -94,18 +97,18 @@ def verify_oauth_body_signature(request, lti_provider_secret, service_url): 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(), + uri=six.text_type(six.moves.urllib.parse.unquote(service_url)), + http_method=six.text_type(request.method), + params=list(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(), + uri=six.text_type(six.moves.urllib.parse.unquote(request.url)), + http_method=six.text_type(request.method), + params=list(oauth_headers.items()), signature=oauth_signature ) - if oauth_body_hash != oauth_headers.get('oauth_body_hash'): + if oauth_body_hash.decode('utf-8') != oauth_headers.get('oauth_body_hash'): log.error( "OAuth body hash verification failed, provided: %s, " "calculated: %s, for url: %s, body is: %s", @@ -123,7 +126,7 @@ def verify_oauth_body_signature(request, lti_provider_secret, service_url): "headers:%s url:%s method:%s", oauth_headers, service_url, - unicode(request.method) + six.text_type(request.method) ) raise LtiError("OAuth signature verification has failed.") @@ -145,18 +148,18 @@ def log_authorization_header(request, client_key, client_secret): """ sha1 = hashlib.sha1() sha1.update(request.body) - oauth_body_hash = unicode(base64.b64encode(sha1.digest())) # pylint: disable=too-many-function-args + oauth_body_hash = six.text_type(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)), + uri=six.text_type(six.moves.urllib.parse.unquote(request.url)), headers=request.headers, body=u"", decoded_body=u"", oauth_params=params, - http_method=unicode(request.method), + http_method=six.text_type(request.method), ) sig = client.get_oauth_signature(mock_request) mock_request.oauth_params.append((u'oauth_signature', sig)) diff --git a/lti_consumer/outcomes.py b/lti_consumer/outcomes.py index 0a63003301322b32a05d3f61cc696a1a5ff46b3b..9f355d0f81cc62a4a52c21907fbaf4b45f2be3fd 100644 --- a/lti_consumer/outcomes.py +++ b/lti_consumer/outcomes.py @@ -5,18 +5,20 @@ For more details see: https://www.imsglobal.org/specs/ltiomv1p0 """ -import logging -import urllib +from __future__ import absolute_import, unicode_literals -from lxml import etree +import logging from xml.sax.saxutils import escape +import six.moves.urllib.error +import six.moves.urllib.parse +from lxml import etree from xblockutils.resources import ResourceLoader + from .exceptions import LtiError from .oauth import verify_oauth_body_signature - log = logging.getLogger(__name__) @@ -40,13 +42,13 @@ def parse_grade_xml_body(body): """ lti_spec_namespace = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0" namespaces = {'def': lti_spec_namespace} - data = body.strip() + 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') + raise LtiError(str(ex) or 'Body is not valid XML') try: imsx_message_identifier = root.xpath("//def:imsx_messageIdentifier", namespaces=namespaces)[0].text or '' @@ -81,7 +83,7 @@ def parse_grade_xml_body(body): return imsx_message_identifier, sourced_id, score, action -class OutcomeService(object): +class OutcomeService(object): # pylint: disable=bad-option-value, useless-object-inheritance """ Service for handling LTI Outcome Management Service requests. @@ -166,8 +168,8 @@ class OutcomeService(object): 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) + error_message = "Request body XML parsing error: {} {}".format(str(ex), body) + log.debug("[LTI]: %s", error_message) failure_values['imsx_description'] = error_message return response_xml_template.format(**failure_values) @@ -177,12 +179,12 @@ class OutcomeService(object): 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) + error_message = "OAuth verification error: " + escape(str(ex)) failure_values['imsx_description'] = error_message - log.debug("[LTI]: " + error_message) + log.debug("[LTI]: %s", error_message) return response_xml_template.format(**failure_values) - real_user = self.xblock.runtime.get_real_user(urllib.unquote(sourced_id.split(':')[-1])) + real_user = self.xblock.runtime.get_real_user(six.moves.urllib.parse.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." @@ -197,9 +199,9 @@ class OutcomeService(object): 'imsx_messageIdentifier': escape(imsx_message_identifier), 'response': '<replaceResultResponse/>' } - log.debug("[LTI]: Grade is saved.") + log.debug(u"[LTI]: Grade is saved.") return response_xml_template.format(**values) unsupported_values['imsx_messageIdentifier'] = escape(imsx_message_identifier) - log.debug("[LTI]: Incorrect action.") + log.debug(u"[LTI]: Incorrect action.") return response_xml_template.format(**unsupported_values) diff --git a/lti_consumer/tests/unit/test_lti.py b/lti_consumer/tests/unit/test_lti.py index 60d721af7e8a4c09290fe7bd7ba675cfba3e4d54..f643253d58251bd7ba1bf169674782b4f9893d35 100644 --- a/lti_consumer/tests/unit/test_lti.py +++ b/lti_consumer/tests/unit/test_lti.py @@ -3,20 +3,21 @@ Unit tests for lti_consumer.lti module """ -import unittest +from __future__ import absolute_import, unicode_literals +import unittest from datetime import timedelta -from mock import Mock, PropertyMock, patch -from six import text_type from django.utils import timezone +from mock import Mock, PropertyMock, patch +from six import text_type +import six -from lti_consumer.tests.unit.test_utils import make_request, patch_signed_parameters -from lti_consumer.tests.unit.test_lti_consumer import TestLtiConsumerXBlock - -from lti_consumer.lti import parse_result_json, LtiConsumer from lti_consumer.exceptions import LtiError - +from lti_consumer.lti import LtiConsumer, parse_result_json +from lti_consumer.tests.unit.test_lti_consumer import TestLtiConsumerXBlock +from lti_consumer.tests.unit.test_utils import (make_request, + patch_signed_parameters) INVALID_JSON_INPUTS = [ ([ @@ -107,7 +108,7 @@ class TestParseResultJson(unittest.TestCase): """ for error_inputs, error_message in INVALID_JSON_INPUTS: for error_input in error_inputs: - with self.assertRaisesRegexp(LtiError, error_message): + with six.assertRaisesRegex(self, LtiError, error_message): parse_result_json(error_input) def test_valid_json(self): @@ -194,14 +195,13 @@ class TestLtiConsumer(TestLtiConsumerXBlock): self._update_xblock_for_signed_parameters() self.xblock.enable_processors = True - with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value={ - 'parameter_processors': [ - 'lti_consumer.tests.unit.test_utils:dummy_processor', - ], - }): + mock_value = { + 'parameter_processors': ['lti_consumer.tests.unit.test_utils:dummy_processor'] + } + with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value=mock_value): params = self.lti_consumer.get_signed_lti_parameters() - assert '' == params['custom_author_country'] - assert 'author@example.com' == params['custom_author_email'] + assert params['custom_author_country'] == u'' + assert params['custom_author_email'] == u'author@example.com' assert not mock_log.exception.called @patch_signed_parameters @@ -210,14 +210,13 @@ class TestLtiConsumer(TestLtiConsumerXBlock): self._update_xblock_for_signed_parameters() self.xblock.enable_processors = True - with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value={ - 'parameter_processors': [ - 'lti_consumer.tests.unit.test_utils:defaulting_processor', - ], - }): + mock_value = { + 'parameter_processors': ['lti_consumer.tests.unit.test_utils:defaulting_processor'] + } + with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value=mock_value): params = self.lti_consumer.get_signed_lti_parameters() - assert '' == params['custom_country'] - assert 'Lex' == params['custom_name'] + assert params['custom_country'] == u'' + assert params['custom_name'] == u'Lex' assert not mock_log.exception.called @patch_signed_parameters @@ -226,13 +225,12 @@ class TestLtiConsumer(TestLtiConsumerXBlock): self._update_xblock_for_signed_parameters() self.xblock.enable_processors = True - with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value={ - 'parameter_processors': [ - 'lti_consumer.tests.unit.test_utils:faulty_processor', - ], - }): + mock_value = { + 'parameter_processors': ['lti_consumer.tests.unit.test_utils:faulty_processor'] + } + with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value=mock_value): params = self.lti_consumer.get_signed_lti_parameters() - assert 'Lex' == params['custom_name'] + assert params['custom_name'] == u'Lex' assert mock_log.exception.called def test_get_result(self): diff --git a/lti_consumer/tests/unit/test_lti_consumer.py b/lti_consumer/tests/unit/test_lti_consumer.py index 0560e8c82d88eb071e70587d3b8e5809d35b93b0..6bae920cf28b4b8233b9466da4a2ae15b068a807 100644 --- a/lti_consumer/tests/unit/test_lti_consumer.py +++ b/lti_consumer/tests/unit/test_lti_consumer.py @@ -2,19 +2,21 @@ Unit tests for LtiConsumerXBlock """ +from __future__ import absolute_import + from datetime import timedelta -import ddt -from mock import Mock, PropertyMock, patch +import ddt +import six from django.test.testcases import TestCase from django.utils import timezone +from mock import Mock, PropertyMock, patch -from lti_consumer.tests.unit.test_utils import FAKE_USER_ID, make_xblock, make_request - -from lti_consumer.lti_consumer import LtiConsumerXBlock, parse_handler_suffix from lti_consumer.exceptions import LtiError +from lti_consumer.lti_consumer import LtiConsumerXBlock, parse_handler_suffix from lti_consumer.tests.unit import test_utils - +from lti_consumer.tests.unit.test_utils import (FAKE_USER_ID, make_request, + make_xblock) HTML_PROBLEM_PROGRESS = '<div class="problem-progress">' HTML_ERROR_MESSAGE = '<h3 class="error_message">' @@ -86,7 +88,7 @@ class TestProperties(TestLtiConsumerXBlock): """ Test `context_id` returns unicode course id """ - self.assertEqual(self.xblock.context_id, unicode(self.xblock.course_id)) # pylint: disable=no-member + self.assertEqual(self.xblock.context_id, six.text_type(self.xblock.course_id)) # pylint: disable=no-member def test_validate(self): """ @@ -165,7 +167,7 @@ class TestProperties(TestLtiConsumerXBlock): type(mock_course).lti_passports = PropertyMock(return_value=["{}{}{}".format(provider, key, secret)]) with self.assertRaises(LtiError): - __, __ = self.xblock.lti_provider_key_secret + _, _ = self.xblock.lti_provider_key_secret def test_user_id(self): """ @@ -285,18 +287,16 @@ class TestProperties(TestLtiConsumerXBlock): self.xblock.due = now + timedelta(days=1) self.assertFalse(self.xblock.is_past_due) - @patch('lti_consumer.lti_consumer.timezone.now') - def test_is_past_due_timezone_now_called(self, mock_timezone_now): + def test_is_past_due_timezone_now_called(self): """ Test `is_past_due` calls django.utils.timezone.now to get current datetime """ now = timezone.now() self.xblock.graceperiod = None self.xblock.due = now - mock_timezone_now.return_value = now - - __ = self.xblock.is_past_due - self.assertTrue(mock_timezone_now.called) + with patch('lti_consumer.lti_consumer.timezone.now', wraps=timezone.now) as mock_timezone_now: + __ = self.xblock.is_past_due + self.assertTrue(mock_timezone_now.called) class TestEditableFields(TestLtiConsumerXBlock): @@ -831,28 +831,25 @@ class TestProcessorSettings(TestLtiConsumerXBlock): """ Unit tests for the adding custom LTI parameters. """ + settings = { + 'parameter_processors': ['lti_consumer.tests.unit.test_utils:dummy_processor'] + } + def test_no_processors_by_default(self): processors = list(self.xblock.get_parameter_processors()) assert not processors, 'The processor list should empty by default.' def test_enable_processor(self): self.xblock.enable_processors = True - with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value={ - 'parameter_processors': [ - 'lti_consumer.tests.unit.test_utils:dummy_processor', - ], - }): + with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value=self.settings): processors = list(self.xblock.get_parameter_processors()) assert len(processors) == 1, 'One processor should be enabled' + # pylint: disable=bad-option-value, comparison-with-callable assert processors[0] == test_utils.dummy_processor, 'Should load the correct function' def test_disabled_processors(self): self.xblock.enable_processors = False - with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value={ - 'parameter_processors': [ - 'lti_consumer.tests.unit.test_utils:dummy_processor', - ], - }): + with patch('lti_consumer.lti_consumer.LtiConsumerXBlock.get_settings', return_value=self.settings): processors = list(self.xblock.get_parameter_processors()) assert not processors, 'No processor should be enabled' diff --git a/lti_consumer/tests/unit/test_oauth.py b/lti_consumer/tests/unit/test_oauth.py index 57e98cbcd6853b0fdacaff9f6d0c28617c5a0465..3186c384d3daa5cec7b6d517d17fe98846826c0d 100644 --- a/lti_consumer/tests/unit/test_oauth.py +++ b/lti_consumer/tests/unit/test_oauth.py @@ -2,19 +2,17 @@ Unit tests for lti_consumer.oauth module """ +from __future__ import absolute_import + import unittest from mock import Mock, patch -from lti_consumer.tests.unit.test_utils import make_request - from lti_consumer.exceptions import LtiError -from lti_consumer.oauth import ( - get_oauth_request_signature, - verify_oauth_body_signature, - log_authorization_header, -) - +from lti_consumer.oauth import (get_oauth_request_signature, + log_authorization_header, + verify_oauth_body_signature) +from lti_consumer.tests.unit.test_utils import make_request OAUTH_PARAMS = [ (u'oauth_nonce', u'80966668944732164491378916897'), diff --git a/lti_consumer/tests/unit/test_outcomes.py b/lti_consumer/tests/unit/test_outcomes.py index da01b3b9966344543d3695077a1d3e5684810562..1ebb538ae2b419a7a33ab3fd52ff219da93d4509 100644 --- a/lti_consumer/tests/unit/test_outcomes.py +++ b/lti_consumer/tests/unit/test_outcomes.py @@ -3,18 +3,18 @@ Unit tests for lti_consumer.outcomes module """ -import unittest -import textwrap +from __future__ import absolute_import, unicode_literals +import textwrap +import unittest from copy import copy -from mock import Mock, PropertyMock, patch -from lti_consumer.tests.unit.test_utils import make_request -from lti_consumer.tests.unit.test_lti_consumer import TestLtiConsumerXBlock +from mock import Mock, PropertyMock, patch -from lti_consumer.outcomes import parse_grade_xml_body, OutcomeService from lti_consumer.exceptions import LtiError - +from lti_consumer.outcomes import OutcomeService, parse_grade_xml_body +from lti_consumer.tests.unit.test_lti_consumer import TestLtiConsumerXBlock +from lti_consumer.tests.unit.test_utils import make_request REQUEST_BODY_TEMPLATE_VALID = textwrap.dedent(""" <?xml version="1.0" encoding="UTF-8"?> @@ -211,7 +211,7 @@ class TestParseGradeXmlBody(unittest.TestCase): Test missing <imsx_messageIdentifier> raises LtiError """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body( + _, _, _, _ = parse_grade_xml_body( REQUEST_BODY_TEMPLATE_MISSING_MSG_ID.format(**REQUEST_TEMPLATE_DEFAULTS) ) @@ -220,7 +220,7 @@ class TestParseGradeXmlBody(unittest.TestCase): Test missing <sourcedId> raises LtiError """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body( + _, _, _, _ = parse_grade_xml_body( REQUEST_BODY_TEMPLATE_MISSING_SOURCED_ID.format(**REQUEST_TEMPLATE_DEFAULTS) ) @@ -229,7 +229,7 @@ class TestParseGradeXmlBody(unittest.TestCase): Test missing <imsx_POXBody> raises LtiError """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body( + _, _, _, _ = parse_grade_xml_body( REQUEST_BODY_TEMPLATE_MISSING_BODY.format(**REQUEST_TEMPLATE_DEFAULTS) ) @@ -238,7 +238,7 @@ class TestParseGradeXmlBody(unittest.TestCase): Test missing <replaceResultRequest> raises LtiError """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body( + _, _, _, _ = parse_grade_xml_body( REQUEST_BODY_TEMPLATE_MISSING_ACTION.format(**REQUEST_TEMPLATE_DEFAULTS) ) @@ -247,7 +247,7 @@ class TestParseGradeXmlBody(unittest.TestCase): Test missing score <textString> raises LtiError """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body( + _, _, _, _ = parse_grade_xml_body( REQUEST_BODY_TEMPLATE_MISSING_SCORE.format(**REQUEST_TEMPLATE_DEFAULTS) ) @@ -259,11 +259,11 @@ class TestParseGradeXmlBody(unittest.TestCase): with self.assertRaises(LtiError): data['score'] = 10 - __, __, __, __ = parse_grade_xml_body(REQUEST_BODY_TEMPLATE_VALID.format(**data)) + _, _, _, _ = parse_grade_xml_body(REQUEST_BODY_TEMPLATE_VALID.format(**data)) with self.assertRaises(LtiError): data['score'] = -10 - __, __, __, __ = parse_grade_xml_body(REQUEST_BODY_TEMPLATE_VALID.format(**data)) + _, _, _, _ = parse_grade_xml_body(REQUEST_BODY_TEMPLATE_VALID.format(**data)) def test_invalid_score(self): """ @@ -273,21 +273,21 @@ class TestParseGradeXmlBody(unittest.TestCase): data['score'] = '1,0' with self.assertRaises(Exception): - __, __, __, __ = parse_grade_xml_body(REQUEST_BODY_TEMPLATE_VALID.format(**data)) + _, _, _, _ = parse_grade_xml_body(REQUEST_BODY_TEMPLATE_VALID.format(**data)) def test_empty_xml(self): """ Test empty xml raises exception """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body('') + _, _, _, _ = parse_grade_xml_body('') def test_invalid_xml(self): """ Test invalid xml raises exception """ with self.assertRaises(LtiError): - __, __, __, __ = parse_grade_xml_body('<xml>') + _, _, _, _ = parse_grade_xml_body('<xml>') def test_string_with_unicode_chars(self): """ diff --git a/lti_consumer/tests/unit/test_utils.py b/lti_consumer/tests/unit/test_utils.py index 9c32789adb9beb3c5f5d6a0d10f7f0c47a90257e..e328aeabf913791e9f46f4252bf997969516b321 100644 --- a/lti_consumer/tests/unit/test_utils.py +++ b/lti_consumer/tests/unit/test_utils.py @@ -2,14 +2,14 @@ Utility functions used within unit tests """ -from webob import Request -from mock import patch, Mock, PropertyMock - -from xblock.fields import ScopeIds -from xblock.runtime import KvsFieldData, DictKeyValueStore +from __future__ import absolute_import +import six +from mock import Mock, PropertyMock, patch +from webob import Request from workbench.runtime import WorkbenchRuntime - +from xblock.fields import ScopeIds +from xblock.runtime import DictKeyValueStore, KvsFieldData FAKE_USER_ID = 'fake_user_id' @@ -31,7 +31,7 @@ def make_xblock(xblock_name, xblock_cls, attributes): hostname='localhost', ) xblock.course_id = 'course-v1:edX+DemoX+Demo_Course' - for key, value in attributes.iteritems(): + for key, value in six.iteritems(attributes): setattr(xblock, key, value) return xblock @@ -100,7 +100,6 @@ def defaulting_processor(_xblock): """ A dummy LTI parameter processor with default params. """ - pass defaulting_processor.lti_xblock_default_params = { diff --git a/openedx.yaml b/openedx.yaml index 06ce8055446729a7fb000b91baa052a2b899dbe1..fa679fd566f715daacb1950104e17eea5fd05aa0 100644 --- a/openedx.yaml +++ b/openedx.yaml @@ -4,7 +4,7 @@ nick: lti oeps: oep-2: true - oep-7: false + oep-7: true oep-18: false owner: scottrish track-pulls: true diff --git a/requirements.txt b/requirements.txt index d098ce2b1d5442c1c08bc3d0be071459c5f1c7f0..555b3eb2c3c6f97179a25a9f465d4fee1d3cb628 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ lxml bleach +django==1.11.25 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 git+https://github.com/edx/XBlock.git#egg=XBlock +-e git+https://github.com/edx/xblock-utils.git#egg=xblock-utils -e . diff --git a/setup.py b/setup.py index 8e4be4194af6c9b8f697dea547b4e86ab158d0af..8ed60ef2a9e67310b9d83ca85006090b5cea6631 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ """Setup for lti_consumer XBlock.""" +from __future__ import absolute_import + import os + from setuptools import setup @@ -22,7 +25,7 @@ def package_data(pkg, roots): setup( name='lti_consumer-xblock', - version='1.2.0', + version='1.2.1', description='This XBlock implements the consumer side of the LTI specification.', packages=[ 'lti_consumer', diff --git a/test.py b/test.py index 56599543313b4f573f2009051131f653312f7e2d..389fd40e5672be31798edf5a823d1c2a3b57915d 100644 --- a/test.py +++ b/test.py @@ -3,6 +3,8 @@ """ Run tests for the LTI Consumer XBlock """ +from __future__ import absolute_import + import os import sys diff --git a/test_requirements.txt b/test_requirements.txt index c7bb14178c9b6c64c46d15d03d7fd1184377bea6..ea45f3f80d92298005181abc31a3fafbc5ba9c20 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,11 +1,10 @@ -r requirements.txt -django-nose==1.4.4 -astroid==1.3.8 # Pinning to avoid backwards incompatibility issue with pylint/pylint-django +django-nose==1.4.6 ddt 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 -git+https://github.com/edx/xblock-sdk.git#egg=xblock-sdk +git+https://github.com/edx/django-pyfs.git@2.0#egg=django-pyfs==2.0 +git+https://github.com/edx/edx-lint.git@1.4.0#egg=edx_lint==1.4.0 +-e git+https://github.com/edx/xblock-sdk.git#egg=xblock-sdk