diff --git a/Makefile b/Makefile index 0ae86fef01e3f4135f85dce5afa99b1ece416af6..cfebe781c92790a43bf18933ba73d8d7a0e16476 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ all: install compile-sass quality test install-test: pip install -q -r requirements/test.txt +install-dev: + pip install -q -r requirements/dev.txt + install: install-test compile-sass: ## Compile the Sass assets @@ -31,6 +34,7 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy pip install -q -r requirements/pip_tools.txt pip-compile --upgrade -o requirements/pip_tools.txt requirements/pip_tools.in pip-compile --upgrade -o requirements/base.txt requirements/base.in + pip-compile --upgrade -o requirements/dev.txt requirements/dev.in pip-compile --upgrade -o requirements/test.txt requirements/test.in pip-compile --upgrade -o requirements/tox.txt requirements/tox.in pip-compile --upgrade -o requirements/travis.txt requirements/travis.in @@ -38,3 +42,33 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy grep -e "^django==" requirements/test.txt > requirements/django.txt sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp mv requirements/test.tmp requirements/test.txt + + +## Localization targets + +WORKING_DIR := lti_consumer +EXTRACT_DIR := $(WORKING_DIR)/translations/en/LC_MESSAGES +EXTRACTED_DJANGO := $(EXTRACT_DIR)/django-partial.po +EXTRACTED_DJANGOJS := $(EXTRACT_DIR)/djangojs-partial.po +EXTRACTED_TEXT := $(EXTRACT_DIR)/text.po + +extract_translations: ## extract strings to be translated, outputting .po files + cd $(WORKING_DIR) && i18n_tool extract + mv $(EXTRACTED_DJANGO) $(EXTRACTED_TEXT) + tail -n +20 $(EXTRACTED_DJANGOJS) >> $(EXTRACTED_TEXT) + rm $(EXTRACTED_DJANGOJS) + sed -i'' -e 's/nplurals=INTEGER/nplurals=2/' $(EXTRACTED_TEXT) + sed -i'' -e 's/plural=EXPRESSION/plural=\(n != 1\)/' $(EXTRACTED_TEXT) + +compile_translations: ## compile translation files, outputting .mo files for each supported language + cd $(WORKING_DIR) && i18n_tool generate + +detect_changed_source_translations: + cd $(WORKING_DIR) && i18n_tool changed + +dummy_translations: ## generate dummy translation (.po) files + cd $(WORKING_DIR) && i18n_tool dummy + +build_dummy_translations: dummy_translations compile_translations ## generate and compile dummy translation files + +validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations \ No newline at end of file diff --git a/lti_consumer/api.py b/lti_consumer/api.py index df1e38b84e5c1b907f145c70c3e2015374f26b93..5490f35c1d9c4977edd4c3301104978c3ef48d3c 100644 --- a/lti_consumer/api.py +++ b/lti_consumer/api.py @@ -80,8 +80,11 @@ def get_lti_1p3_launch_info(config_id=None, block=None): lti_config = _get_lti_config(config_id, block) lti_consumer = lti_config.get_lti_consumer() - # Check if deep Linking is available, if so, retrieve it's launch url + # Check if deep Linking is available, if so, add some extra context: + # Deep linking launch URL, and if deep linking is already configured deep_linking_launch_url = None + deep_linking_content_items = [] + if lti_consumer.dl is not None: deep_linking_launch_url = lti_consumer.prepare_preflight_url( callback_url=get_lms_lti_launch_link(), @@ -89,6 +92,14 @@ def get_lti_1p3_launch_info(config_id=None, block=None): lti_hint="deep_linking_launch" ) + # Retrieve LTI Content Items (if any was set up) + dl_content_items = LtiDlContentItem.objects.filter( + lti_configuration=lti_config + ) + # Add content item attributes to context + if dl_content_items.exists(): + deep_linking_content_items = [item.attributes for item in dl_content_items] + # Return LTI launch information for end user configuration return { 'client_id': lti_config.lti_1p3_client_id, @@ -97,6 +108,7 @@ def get_lti_1p3_launch_info(config_id=None, block=None): 'oidc_callback': get_lms_lti_launch_link(), 'token_url': get_lms_lti_access_token_link(lti_config.location), 'deep_linking_launch_url': deep_linking_launch_url, + 'deep_linking_content_items': deep_linking_content_items, } diff --git a/lti_consumer/locale b/lti_consumer/locale new file mode 120000 index 0000000000000000000000000000000000000000..cef3e3fa5dbda59d171c3fe69b4a908ca3576b56 --- /dev/null +++ b/lti_consumer/locale @@ -0,0 +1 @@ +translations \ No newline at end of file diff --git a/lti_consumer/lti_xblock.py b/lti_consumer/lti_xblock.py index 22d31bb9e731a5d0698b86de04d11cc1d11d8595..58a9432db94c93c69868b418c718358359b3d362 100644 --- a/lti_consumer/lti_xblock.py +++ b/lti_consumer/lti_xblock.py @@ -674,8 +674,9 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): raise ValueError key = ':'.join(key) except ValueError as err: - msg = 'Could not parse LTI passport: {lti_passport!r}. Should be "id:key:secret" string.' - msg = self.ugettext(msg).format(lti_passport=lti_passport) + msg = self.ugettext( + 'Could not parse LTI passport: {lti_passport!r}. Should be "id:key:secret" string.' + ).format(lti_passport=lti_passport) raise LtiError(msg) from err if lti_id == self.lti_id.strip(): @@ -810,8 +811,9 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): param_name, param_value = [p.strip() for p in custom_parameter.split('=', 1)] except ValueError as err: _ = self.runtime.service(self, "i18n").ugettext - msg = 'Could not parse custom parameter: {custom_parameter!r}. Should be "x=y" string.' - msg = self.ugettext(msg).format(custom_parameter=custom_parameter) + msg = self.ugettext( + 'Could not parse custom parameter: {custom_parameter!r}. Should be "x=y" string.' + ).format(custom_parameter=custom_parameter) raise LtiError(msg) from err # LTI specs: 'custom_' should be prepended before each custom parameter, as pointed in link above. @@ -924,7 +926,13 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): # Render template fragment = Fragment() loader = ResourceLoader(__name__) - fragment.add_content(loader.render_mako_template('/templates/html/lti_1p3_studio.html', context)) + fragment.add_content( + loader.render_django_template( + '/templates/html/lti_1p3_studio.html', + context, + i18n_service=self.runtime.service(self, 'i18n') + ), + ) fragment.add_css(loader.load_unicode('static/css/student.css')) fragment.add_javascript(loader.load_unicode('static/js/xblock_lti_consumer.js')) fragment.initialize_js('LtiConsumerXBlock') @@ -1101,10 +1109,21 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): template = loader.render_mako_template('/templates/html/lti_1p3_launch.html', context) return Response(template, content_type='text/html') - except Lti1p3Exception: + except Lti1p3Exception as exc: + log.warning( + "Error preparing LTI 1.3 launch for block %r: %s", + str(self.location), # pylint: disable=no-member + exc, + ) template = loader.render_mako_template('/templates/html/lti_1p3_launch_error.html', context) return Response(template, status=400, content_type='text/html') - except AssertionError: + except AssertionError as exc: + log.warning( + "Permission on LTI block %r denied for user %r: %s", + str(self.location), # pylint: disable=no-member + self.external_user_id, + exc, + ) template = loader.render_mako_template('/templates/html/lti_1p3_permission_error.html', context) return Response(template, status=403, content_type='text/html') diff --git a/lti_consumer/plugin/views.py b/lti_consumer/plugin/views.py index e7b7973128176bb8e1c99213c1b5fe5bc9a7cad4..53101b0558cdd58cfc9cefdb8c1d0540d232275f 100644 --- a/lti_consumer/plugin/views.py +++ b/lti_consumer/plugin/views.py @@ -1,8 +1,10 @@ """ LTI consumer plugin passthrough views """ +import logging + from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.http import HttpResponse, JsonResponse +from django.http import JsonResponse, Http404 from django.db import transaction from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -22,7 +24,7 @@ from lti_consumer.models import ( LtiDlContentItem, ) -from lti_consumer.lti_1p3.exceptions import Lti1p3Exception +from lti_consumer.lti_1p3.exceptions import Lti1p3Exception, LtiDeepLinkingContentTypeNotSupported from lti_consumer.lti_1p3.extensions.rest_framework.constants import LTI_DL_CONTENT_TYPE_SERIALIZER_MAP from lti_consumer.lti_1p3.extensions.rest_framework.serializers import ( LtiAgsLineItemSerializer, @@ -48,6 +50,10 @@ from lti_consumer.plugin.compat import ( user_course_access, user_has_access, ) +from lti_consumer.utils import _ + + +log = logging.getLogger(__name__) def user_has_staff_access(user, course_key): @@ -111,8 +117,9 @@ def public_keyset_endpoint(request, usage_id=None): response = JsonResponse(lti_config.lti_1p3_public_jwk) response['Content-Disposition'] = 'attachment; filename=keyset.json' return response - except (LtiError, InvalidKeyError, ObjectDoesNotExist): - return HttpResponse(status=404) + except (LtiError, InvalidKeyError, ObjectDoesNotExist) as exc: + log.info("Error while retrieving keyset for usage_id %r: %s", usage_id, exc) + raise Http404 from exc @require_http_methods(["GET", "POST"]) @@ -135,8 +142,9 @@ def launch_gate_endpoint(request, suffix): handler='lti_1p3_launch_callback', suffix=suffix ) - except Exception: # pylint: disable=broad-except - return HttpResponse(status=404) + except Exception as exc: + log.warning("Error preparing LTI 1.3 launch for hint %r: %s", usage_key_str, exc) + raise Http404 from exc @csrf_exempt @@ -154,8 +162,9 @@ def access_token_endpoint(request, usage_id=None): usage_id=str(usage_key), handler='lti_1p3_access_token' ) - except Exception: # pylint: disable=broad-except - return HttpResponse(status=404) + except Exception as exc: + log.warning("Error retrieving an access token for usage_id %r: %s", usage_id, exc) + raise Http404 from exc # Post from external tool that doesn't @@ -194,10 +203,11 @@ def deep_linking_response_endpoint(request, lti_config_id=None): LtiDlContentItem.objects.filter(lti_configuration=lti_config).delete() for content_item in content_items: - content_type = content_item.get('type') - # Retrieve serializer (or throw error) + # Retrieve serializer (or raise) + if content_type not in LTI_DL_CONTENT_TYPE_SERIALIZER_MAP.keys(): + raise LtiDeepLinkingContentTypeNotSupported() serializer_cls = LTI_DL_CONTENT_TYPE_SERIALIZER_MAP[content_type] # Validate content item data @@ -211,18 +221,41 @@ def deep_linking_response_endpoint(request, lti_config_id=None): attributes=serializer.validated_data, ) - # TODO: Redirect the user to the launch endpoint, and present content - # selected in Deep Linking flow. Can only be completed once content - # presentation is implemented. For now, return ok status page - return HttpResponse(status=200) + # Display sucess page to indicate that LTI DL Content was + # saved successfully and auto-close after a few seconds. + return render(request, 'html/lti-dl/dl_response_saved.html') # If LtiConfiguration doesn't exist, error with 404 status. - except LtiConfiguration.DoesNotExist: - return HttpResponse(status=404) - # Bad JWT message, invalid token, or any message validation issues - except (Lti1p3Exception, KeyError, PermissionDenied): - # TODO: Add template with error message - return HttpResponse(status=403) + except LtiConfiguration.DoesNotExist as exc: + log.info("LtiConfiguration %r does not exist: %s", lti_config_id, exc) + raise Http404 from exc + # If the deep linking content type is not supported + except LtiDeepLinkingContentTypeNotSupported as exc: + log.info("One of the selected LTI Content Types is not supported: %s", exc) + return render( + request, + 'html/lti-dl/dl_response_error.html', + {"error": _("The selected content type is not supported by Open edX.")}, + status=400 + ) + # Bad JWT message, invalid token, or any other message validation issues + except (Lti1p3Exception, PermissionDenied) as exc: + log.warning( + "Permission on LTI Config %r denied for user %r: %s", + lti_config, + request.user, + exc, + ) + return render( + request, + 'html/lti-dl/dl_response_error.html', + { + "error": _("You don't have access to save LTI Content Items."), + "explanation": _("Please check that you have course staff permissions " + "and double check this block's LTI settings."), + }, + status=403 + ) @require_http_methods(['GET']) @@ -233,19 +266,26 @@ def deep_linking_content_endpoint(request, lti_config_id=None): try: # Get LTI Configuration lti_config = LtiConfiguration.objects.get(id=lti_config_id) - except LtiConfiguration.DoesNotExist: - return HttpResponse(status=404) + except LtiConfiguration.DoesNotExist as exc: + log.info("LtiConfiguration %r does not exist: %s", lti_config_id, exc) + raise Http404 from exc # check if user has proper access if not has_block_access(request.user, lti_config.block, lti_config.location.course_key): - return HttpResponse(status=403) + log.warning( + "Permission on LTI Config %r denied for user %r.", + lti_config_id, + request.user, + ) + raise PermissionDenied # Get all LTI-DL contents content_items = LtiDlContentItem.objects.filter(lti_configuration=lti_config) # If no LTI-DL contents found for current configuration, throw 404 error if not content_items.exists(): - return HttpResponse(status=404) + log.info("There's no Deep linking content for LTI configuration %s.", lti_config) + raise Http404 # Render LTI-DL contents return render(request, 'html/lti-dl/render_dl_content.html', { diff --git a/lti_consumer/templates/html/lti-dl/dl_response_error.html b/lti_consumer/templates/html/lti-dl/dl_response_error.html new file mode 100644 index 0000000000000000000000000000000000000000..70acaa3a2a7c31cb1c1d0cc98c1d27f2efe008fb --- /dev/null +++ b/lti_consumer/templates/html/lti-dl/dl_response_error.html @@ -0,0 +1,18 @@ +{% load i18n %} +<!DOCTYPE HTML> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <title>{% trans "LTI Deep Linking failed." %}</title> + </head> + <body> + <p> + <b>{{ error }}</b> + </p> + {% if explanation %} + <p> + {{ explanation }} + </p> + {% endif %} + </body> +</html> diff --git a/lti_consumer/templates/html/lti-dl/dl_response_saved.html b/lti_consumer/templates/html/lti-dl/dl_response_saved.html new file mode 100644 index 0000000000000000000000000000000000000000..5be0f5dc6eb0d90e025c99c8f12362baba2381b5 --- /dev/null +++ b/lti_consumer/templates/html/lti-dl/dl_response_saved.html @@ -0,0 +1,16 @@ +{% load i18n %} +<!DOCTYPE HTML> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <title>{% trans "LTI Deep Linking" %}</title> + </head> + <body> + <p> + <b>{% trans "The LTI Deep Linking content was successfully saved in the LMS." %}</b> + </p> + <p> + {% trans "You can safely close this page now." %} + </p> + </body> +</html> diff --git a/lti_consumer/templates/html/lti_1p3_launch_error.html b/lti_consumer/templates/html/lti_1p3_launch_error.html index ba5dba765f3949bbe1bd180de21167a1677534a8..813591ab5175169408d3d3e0a1ffcfcc3197d40e 100644 --- a/lti_consumer/templates/html/lti_1p3_launch_error.html +++ b/lti_consumer/templates/html/lti_1p3_launch_error.html @@ -1,3 +1,4 @@ +{% load i18n %} <!DOCTYPE HTML> <html> <head> @@ -6,10 +7,10 @@ </head> <body> <p> - <b>There was an error while launching the LTI 1.3 tool.</b> + <b>{% trans "There was an error while launching the LTI 1.3 tool." %}</b> </p> <p> - If you're seeing this on a live course, please contact the course staff. + {% trans "If you're seeing this on a live course, please contact the course staff." %} </p> </body> </html> diff --git a/lti_consumer/templates/html/lti_1p3_permission_error.html b/lti_consumer/templates/html/lti_1p3_permission_error.html index eb74976e68c5f8c3219c45e13d3aae74d7c005fa..184ebeda143853519d37c93171d99ab5c6b477aa 100644 --- a/lti_consumer/templates/html/lti_1p3_permission_error.html +++ b/lti_consumer/templates/html/lti_1p3_permission_error.html @@ -1,3 +1,4 @@ +{% load i18n %} <!DOCTYPE HTML> <html> <head> @@ -6,10 +7,10 @@ </head> <body> <p> - <b>Unauthorized.</b> + <b>{% trans "Unauthorized." %}</b> </p> <p> - Students don't have permissions to perform LTI Deep Linking configuration launches. + {% trans "Students don't have permissions to perform LTI Deep Linking configuration launches." %} </p> </body> </html> diff --git a/lti_consumer/templates/html/lti_1p3_studio.html b/lti_consumer/templates/html/lti_1p3_studio.html index b5303957c1dcf0b45d1d6582836c0bfbed39c42a..820c378d573a9e31147d0c661da01dd931f65ffd 100644 --- a/lti_consumer/templates/html/lti_1p3_studio.html +++ b/lti_consumer/templates/html/lti_1p3_studio.html @@ -1,61 +1,58 @@ -<!DOCTYPE HTML> -<html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> - <title>LTI</title> - </head> - <body> - <p> - <b>LTI 1.3 Launches can only be performed from the LMS.</b> - </p> - - <p> - The current implementation of the LTI 1.3 XBlock does not support - <a href="http://www.imsglobal.org/lti-advantage-faq#what-is-LTI-Advantage"> - LTI Advantage services - </a>. - Therefore, there is no mechanism for LTI Tools to pass back grades. - </p> - - <p> - To set up the LTI integration, you need to register the LMS in the tool with the information provided below. - </p> - - <p> - <b>Client: </b> - ${client_id} - </p> - - <p> - <b>Deployment ID: </b> - ${deployment_id} - </p> - - <p> - <b>Keyset URL: </b> - ${keyset_url} - </p> - - <p> - <b>OAuth Token URL: </b> - ${token_url} - </p> - - <p> - <b>OIDC Callback URL: </b> - ${oidc_callback} - </p> - - % if deep_linking_launch_url: - <hr /> - <p> - You can configure this tool's content using LTI Deep Linking. - To do that, make sure the block is published and click the link below: - </p> - <a href="${deep_linking_launch_url}" target="_blank"> - Deep Linking Launch - Configure tool - <i class="icon fa fa-external-link"></i> - </a> - % endif - </body> -</html> +{% load i18n %} +<p> + <b>{% trans "LTI 1.3 Launches can only be performed from the LMS." %}</b> +</p> + +<p> + {% trans "To set up the LTI integration, you need to register the LMS in the tool with the information provided below." %} +</p> + +<p> + <b>{% trans "Client: " %}</b> + {{ client_id }} +</p> + +<p> + <b>{% trans "Deployment ID: " %}</b> + {{ deployment_id }} +</p> + +<p> + <b>{% trans "Keyset URL: " %}</b> + {{ keyset_url }} +</p> + +<p> + <b>{% trans "OAuth Token URL: " %}</b> + {{ token_url }} +</p> + +<p> + <b>{% trans "OIDC Callback URL: " %}</b> + {{ oidc_callback }} +</p> + +{% if deep_linking_launch_url %} + <hr /> + {% if deep_linking_content_items %} + <p> + <b>{% trans "Deep Linking is configured on this tool." %}</b> + {% trans "The Deep Linking configuration stored is presented below:" %} + </p> + <code> + {{ deep_linking_content_items }} + </code> + + <p> + {% trans "If you run deep linking again, the content above will be replaced." %} + </p> + {% endif %} + <p> + {% trans "You can configure this tool's content using LTI Deep Linking." %} + {% trans "To do that, make sure the block is published and click the link below:" %} + </p> + <a href="{{ deep_linking_launch_url }}" target="_blank"> + {% trans "Deep Linking Launch - Configure tool" %} + <i class="icon fa fa-external-link"></i> + </a> +{% endif %} diff --git a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py index 746626f0a4749bdee5a64fdddc8cde454eb060b6..364b258c6251becf3a99f9a9b22dcbc79e9323c3 100644 --- a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py +++ b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py @@ -136,7 +136,7 @@ class LtiDeepLinkingResponseEndpointTestCase(LtiDeepLinkingTestCase): def test_lti_deep_linking_with_invalid_content_type(self): """ - Test that the endpoint returns 403 when content type is not supported. + Test that the endpoint returns 400 when content type is not supported. """ response = self.client.post( self.url, @@ -146,7 +146,7 @@ class LtiDeepLinkingResponseEndpointTestCase(LtiDeepLinkingTestCase): }]) }, ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 400) def test_lti_deep_linking_valid_request(self): """ diff --git a/lti_consumer/tests/unit/test_api.py b/lti_consumer/tests/unit/test_api.py index 30de24f4f1ff387a5a693a779a96007467523090..4a7b0e4e557c9e0c5caad7b7ee9caebf492fa046 100644 --- a/lti_consumer/tests/unit/test_api.py +++ b/lti_consumer/tests/unit/test_api.py @@ -183,7 +183,8 @@ class TestGetLti1p3LaunchInfo(TestCase): 'token_url': 'https://example.com/api/lti_consumer/v1/token/{}'.format( lti_config.lti_1p3_client_id ), - 'deep_linking_launch_url': 'https://example.com' + 'deep_linking_launch_url': 'https://example.com', + 'deep_linking_content_items': [] } ) diff --git a/lti_consumer/tests/unit/test_lti_xblock.py b/lti_consumer/tests/unit/test_lti_xblock.py index d40d228fc88b0c2e38f8397e2e7cc60a1b251a17..94841d293f09b1124eb4728a14f4648b4a25436a 100644 --- a/lti_consumer/tests/unit/test_lti_xblock.py +++ b/lti_consumer/tests/unit/test_lti_xblock.py @@ -1287,8 +1287,10 @@ class TestLtiConsumer1p3XBlock(TestCase): 'oidc_callback': "mock-oidc_callback", 'token_url': "mock-token_url", } - + # Mock i18n service before fetching author view + self.xblock.runtime.service.return_value = None response = self.xblock.author_view({}) + self.assertIn("mock-client_id", response.content) self.assertIn("mock-keyset_url", response.content) self.assertIn("mock-token_url", response.content) diff --git a/lti_consumer/translations/ar/LC_MESSAGES/text.mo b/lti_consumer/translations/ar/LC_MESSAGES/text.mo index a0ebeb64cb88f844bf66df2caf14218b6524dcf8..9f9a74b4686279e24141c6c03e45915e7739c611 100644 Binary files a/lti_consumer/translations/ar/LC_MESSAGES/text.mo and b/lti_consumer/translations/ar/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/config.yaml b/lti_consumer/translations/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f6e22273614c422a18c3765c209f1ff10005dfc6 --- /dev/null +++ b/lti_consumer/translations/config.yaml @@ -0,0 +1,14 @@ +# Configuration for i18n workflow. + +locales: + - en # English - Source Language + - ar # Arabic + - es_419 # Spanish (Latin America) + - fr # French + - he # Hebrew + - hi # Hindi + - ja_JP # Japanese (Japan) + - ko_KR # Korean (Korea) + - pt_BR # Portuguese (Brazil) + - ru # Russian + - zh_CN # Chinese (China) diff --git a/lti_consumer/translations/en/LC_MESSAGES/text.mo b/lti_consumer/translations/en/LC_MESSAGES/text.mo index 29fe0c3be6558862833ea7fc65a7665f10b3243a..71cbdf3e9d8d54be31066ec4ad8628bc2c1f2845 100644 Binary files a/lti_consumer/translations/en/LC_MESSAGES/text.mo and b/lti_consumer/translations/en/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/en/LC_MESSAGES/text.po b/lti_consumer/translations/en/LC_MESSAGES/text.po index 924650b00d930adf1ccefe78996a039187f395e1..2a944197560dbd16b3594e6b928ead2f724383f1 100644 --- a/lti_consumer/translations/en/LC_MESSAGES/text.po +++ b/lti_consumer/translations/en/LC_MESSAGES/text.po @@ -1,53 +1,150 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy msgid "" msgstr "" -"Project-Id-Version: \n" -"POT-Creation-Date: 2016-03-18 15:09+0500\n" -"PO-Revision-Date: 2016-03-18 15:09+0500\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: en\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-03-02 11:48-0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.7\n" -"X-Poedit-Basepath: ../../..\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Poedit-SearchPath-0: lti_consumer.py\n" -#: lti_consumer.py +#: lti_1p3/extensions/rest_framework/authentication.py:40 +msgid "Missing LTI 1.3 authentication token." +msgstr "" + +#: lti_1p3/extensions/rest_framework/authentication.py:44 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: lti_1p3/extensions/rest_framework/authentication.py:48 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: lti_1p3/extensions/rest_framework/authentication.py:56 +msgid "LTI configuration not found." +msgstr "" + +#: lti_1p3/extensions/rest_framework/authentication.py:65 +msgid "Invalid token signature." +msgstr "" + +#: lti_xblock.py:130 msgid "No valid user id found in endpoint URL" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:236 msgid "Display Name" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:238 msgid "" "Enter the name that students see for this component. Analytics reports may " "also use the display name to identify this component." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:242 msgid "LTI Consumer" msgstr "" -#: lti_consumer.py:256 +#: lti_xblock.py:245 msgid "LTI Application Information" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:247 msgid "" "Enter a description of the third party application. If requesting username " "and/or email, use this text box to inform users why their username and/or " "email will be forwarded to a third party application." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:257 +msgid "LTI Version" +msgstr "" + +#: lti_xblock.py:265 +msgid "" +"Select the LTI version that your tool supports.<br />The XBlock LTI Consumer " +"fully supports LTI 1.1.1 and partially supports LTI 1.3 (only launches, no " +"grade support)." +msgstr "" + +#: lti_xblock.py:271 +msgid "LTI 1.3 Tool Launch URL" +msgstr "" + +#: lti_xblock.py:275 +msgid "" +"Enter the LTI 1.3 Tool Launch URL. <br />This is the URL the LMS will use to " +"launch the LTI Tool." +msgstr "" + +#: lti_xblock.py:280 +msgid "LTI 1.3 OIDC URL" +msgstr "" + +#: lti_xblock.py:284 +msgid "" +"Enter the LTI 1.3 Tool OIDC Authorization url (can also be called login or " +"login initiation URL).<br />This is the URL the LMS will use to start a LTI " +"authorization prior to doing the launch request." +msgstr "" + +#: lti_xblock.py:290 +msgid "LTI 1.3 Tool Public Key" +msgstr "" + +#: lti_xblock.py:295 +msgid "" +"Enter the LTI 1.3 Tool's public key.<br />This is a string that starts with " +"'-----BEGIN PUBLIC KEY-----' and is required so that the LMS can check if " +"the messages and launch requests received have the signature from the tool." +"<br /><b>This is not required when doing LTI 1.3 Launches without LTI " +"Advantage nor Basic Outcomes requests.</b>" +msgstr "" + +#: lti_xblock.py:305 +msgid "LTI 1.3 Block Client ID - DEPRECATED" +msgstr "" + +#: lti_xblock.py:308 +msgid "DEPRECATED - This is now stored in the LtiConfiguration model." +msgstr "" + +#: lti_xblock.py:311 +msgid "LTI 1.3 Block Key - DEPRECATED" +msgstr "" + +#: lti_xblock.py:318 +msgid "Deep linking" +msgstr "" + +#: lti_xblock.py:319 +msgid "Select True if you want to enable LTI Advantage Deep Linking." +msgstr "" + +#: lti_xblock.py:324 +msgid "LTI Advantage Deep Linking Launch URL" +msgstr "" + +#: lti_xblock.py:327 +msgid "Enter the LTI Advantage Deep Linking Launch URL. " +msgstr "" + +#: lti_xblock.py:332 msgid "LTI ID" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:334 #, python-brace-format msgid "" "Enter the LTI ID for the external LTI provider. This value must be the same " @@ -56,11 +153,11 @@ msgid "" "documentation{anchor_close} for more details on this setting." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:346 msgid "LTI URL" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:348 #, python-brace-format msgid "" "Enter the URL of the external tool that this component launches. This " @@ -69,11 +166,11 @@ msgid "" "this setting." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:361 msgid "Custom Parameters" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:363 #, python-brace-format msgid "" "Add the key/value pair for any custom parameters, such as the page your e-" @@ -82,11 +179,11 @@ msgid "" "documentation{anchor_close} for more details on this setting." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:373 msgid "LTI Launch Target" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:375 msgid "" "Select Inline if you want the LTI content to open in an IFrame in the " "current page. Select Modal if you want the LTI content to open in a modal " @@ -95,130 +192,322 @@ msgid "" "Tool is set to False." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:389 msgid "Button Text" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:391 msgid "" "Enter the text on the button used to launch the third party application. " "This setting is only used when Hide External Tool is set to False and LTI " "Launch Target is set to Modal or New Window." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:399 msgid "Inline Height" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:401 msgid "" "Enter the desired pixel height of the iframe which will contain the LTI " "tool. This setting is only used when Hide External Tool is set to False and " "LTI Launch Target is set to Inline." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:409 msgid "Modal Height" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:411 msgid "" "Enter the desired viewport percentage height of the modal overlay which will " "contain the LTI tool. This setting is only used when Hide External Tool is " "set to False and LTI Launch Target is set to Modal." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:419 msgid "Modal Width" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:421 msgid "" "Enter the desired viewport percentage width of the modal overlay which will " "contain the LTI tool. This setting is only used when Hide External Tool is " "set to False and LTI Launch Target is set to Modal." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:429 msgid "Scored" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:430 msgid "" "Select True if this component will receive a numerical score from the " "external LTI system." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:437 msgid "" "Enter the number of points possible for this component. The default value " "is 1.0. This setting is only used when Scored is set to True." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:446 msgid "" "The score kept in the xblock KVS -- duplicate of the published score in " "django DB" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:451 msgid "Comment as returned from grader, LTI2.0 spec" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:456 msgid "Hide External Tool" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:458 msgid "" "Select True if you want to use this component as a placeholder for syncing " "with an external grading system rather than launch an external tool. This " "setting hides the Launch button and any IFrames for this component." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:466 msgid "Accept grades past deadline" msgstr "" -#: lti_consumer.py +#: lti_xblock.py:467 msgid "" "Select True to allow third party systems to post grades past the deadline." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:475 msgid "Request user's username" msgstr "" -#: lti_consumer.py +#. Translators: This is used to request the user's username for a third party service. +#: lti_xblock.py:477 msgid "Select True to request the user's username." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:482 msgid "Request user's email" msgstr "" -#: lti_consumer.py +#. Translators: This is used to request the user's email for a third party service. +#: lti_xblock.py:484 msgid "Select True to request the user's email address." msgstr "" -#: lti_consumer.py -#, python-brace-format +#: lti_xblock.py:489 +msgid "Send extra parameters" +msgstr "" + +#: lti_xblock.py:490 msgid "" -"Could not parse LTI passport: {lti_passport}. Should be \"id:key:secret\" " +"Select True to send the extra parameters, which might contain Personally " +"Identifiable Information. The processors are site-wide, please consult the " +"site administrator if you have any questions." +msgstr "" + +#: lti_xblock.py:550 +msgid "Custom Parameters must be a list" +msgstr "" + +#: lti_xblock.py:678 +msgid "" +"Could not parse LTI passport: {lti_passport!r}. Should be \"id:key:secret\" " "string." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:694 lti_xblock.py:710 msgid "Could not get user id for current request" msgstr "" -#: lti_consumer.py -#, python-brace-format +#: lti_xblock.py:815 msgid "" -"Could not parse custom parameter: {custom_parameter}. Should be \"x=y\" " +"Could not parse custom parameter: {custom_parameter!r}. Should be \"x=y\" " "string." msgstr "" -#: lti_consumer.py +#: lti_xblock.py:1249 msgid "[LTI]: Real user not found against anon_id: {}" msgstr "" + +#: models.py:68 +msgid "Configuration Stored on XBlock fields" +msgstr "" + +#: models.py:69 +msgid "Configuration Stored on this model" +msgstr "" + +#: models.py:93 +msgid "LTI configuration data." +msgstr "" + +#: models.py:100 +msgid "The URL of the external tool that initiates the launch." +msgstr "" + +#: models.py:105 +msgid "Client key provided by the LTI tool provider." +msgstr "" + +#: models.py:111 +msgid "Client secret provided by the LTI tool provider." +msgstr "" + +#: models.py:117 +msgid "Platform's generated Private key. Keep this value secret." +msgstr "" + +#: models.py:123 +msgid "Platform's generated Private key ID" +msgstr "" + +#: models.py:128 +msgid "Platform's generated JWK keyset." +msgstr "" + +#: models.py:134 +msgid "Client ID used by LTI tool" +msgstr "" + +#: models.py:144 +msgid "LTI Configuration stores on XBlock needs a block location set." +msgstr "" + +#: models.py:151 +msgid "Invalid LTI configuration." +msgstr "" + +#: models.py:161 +msgid "Block location not set, it's not possible to retrieve the block." +msgstr "" + +#: plugin/views.py:238 +msgid "You don't have access to save LTI Content Items." +msgstr "" + +#: plugin/views.py:239 +msgid "" +"Please check that you have course staff permissions and double check this " +"block's LTI settings." +msgstr "" + +#: plugin/views.py:249 +msgid "The selected content type is not supported by Open edX." +msgstr "" + +#: templates/html/lti-dl/dl_response_error.html:6 +msgid "LTI Deep Linking failed." +msgstr "" + +#: templates/html/lti-dl/dl_response_saved.html:6 +msgid "LTI Deep Linking" +msgstr "" + +#: templates/html/lti-dl/dl_response_saved.html:10 +msgid "The LTI Deep Linking content was successfully saved in the LMS." +msgstr "" + +#: templates/html/lti-dl/dl_response_saved.html:13 +msgid "You can safely close this page now." +msgstr "" + +#: templates/html/lti_1p3_launch_error.html:10 +msgid "There was an error while launching the LTI 1.3 tool." +msgstr "" + +#: templates/html/lti_1p3_launch_error.html:13 +msgid "" +"If you're seeing this on a live course, please contact the course staff." +msgstr "" + +#: templates/html/lti_1p3_permission_error.html:10 +msgid "Unauthorized." +msgstr "" + +#: templates/html/lti_1p3_permission_error.html:13 +msgid "" +"Students don't have permissions to perform LTI Deep Linking configuration " +"launches." +msgstr "" + +#: templates/html/lti_1p3_studio.html:3 +msgid "LTI 1.3 Launches can only be performed from the LMS." +msgstr "" + +#: templates/html/lti_1p3_studio.html:7 +msgid "" +"To set up the LTI integration, you need to register the LMS in the tool with " +"the information provided below." +msgstr "" + +#: templates/html/lti_1p3_studio.html:11 +msgid "Client: " +msgstr "" + +#: templates/html/lti_1p3_studio.html:16 +msgid "Deployment ID: " +msgstr "" + +#: templates/html/lti_1p3_studio.html:21 +msgid "Keyset URL: " +msgstr "" + +#: templates/html/lti_1p3_studio.html:26 +msgid "OAuth Token URL: " +msgstr "" + +#: templates/html/lti_1p3_studio.html:31 +msgid "OIDC Callback URL: " +msgstr "" + +#: templates/html/lti_1p3_studio.html:39 +msgid "Deep Linking is configured on this tool." +msgstr "" + +#: templates/html/lti_1p3_studio.html:40 +msgid "The Deep Linking configuration stored is presented below:" +msgstr "" + +#: templates/html/lti_1p3_studio.html:47 +msgid "If you run deep linking again, the content above will be replaced." +msgstr "" + +#: templates/html/lti_1p3_studio.html:51 +msgid "You can configure this tool's content using LTI Deep Linking." +msgstr "" + +#: templates/html/lti_1p3_studio.html:52 +msgid "To do that, make sure the block is published and click the link below:" +msgstr "" + +#: templates/html/lti_1p3_studio.html:55 +msgid "Deep Linking Launch - Configure tool" +msgstr "" + +#: static/js/xblock_lti_consumer.js:110 +msgid "" +"Click OK to have your username and e-mail address sent to a 3rd party " +"application.\n" +"\n" +"Click Cancel to return to this page without sending your information." +msgstr "" + +#: static/js/xblock_lti_consumer.js:112 +msgid "" +"Click OK to have your username sent to a 3rd party application.\n" +"\n" +"Click Cancel to return to this page without sending your information." +msgstr "" + +#: static/js/xblock_lti_consumer.js:114 +msgid "" +"Click OK to have your e-mail address sent to a 3rd party application.\n" +"\n" +"Click Cancel to return to this page without sending your information." +msgstr "" diff --git a/lti_consumer/translations/es_419/LC_MESSAGES/text.mo b/lti_consumer/translations/es_419/LC_MESSAGES/text.mo index dee7f292335a9852a85e76daf8278d6349408967..707ef9cb3e526d0d3324ab9f303081517de12a25 100644 Binary files a/lti_consumer/translations/es_419/LC_MESSAGES/text.mo and b/lti_consumer/translations/es_419/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/fr/LC_MESSAGES/text.mo b/lti_consumer/translations/fr/LC_MESSAGES/text.mo index f5dcc675e2789cde7d7f20616c7e4eb33b161216..d8b0e7f66593db85b6c5a89811d0cd542680bd49 100644 Binary files a/lti_consumer/translations/fr/LC_MESSAGES/text.mo and b/lti_consumer/translations/fr/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/fr_CA/LC_MESSAGES/text.mo b/lti_consumer/translations/fr_CA/LC_MESSAGES/text.mo index ec09793ff502ec1d41e67b5c60be2e1840c2655d..d55f999e96f9a848804285b785516157ef38a855 100644 Binary files a/lti_consumer/translations/fr_CA/LC_MESSAGES/text.mo and b/lti_consumer/translations/fr_CA/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/he/LC_MESSAGES/text.mo b/lti_consumer/translations/he/LC_MESSAGES/text.mo index a1b5b9bd3f7f6e554956af8f9f5c5fae50ed83ed..cdd6c76909f32dd55a07c24ed45fce1a23a409e6 100644 Binary files a/lti_consumer/translations/he/LC_MESSAGES/text.mo and b/lti_consumer/translations/he/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/hi/LC_MESSAGES/text.mo b/lti_consumer/translations/hi/LC_MESSAGES/text.mo index 71f6f7536b56be732aac0f2f541632268b13bc15..fc050f01e0b6a5ac1df4999f38c9056543a867d7 100644 Binary files a/lti_consumer/translations/hi/LC_MESSAGES/text.mo and b/lti_consumer/translations/hi/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/ja_JP/LC_MESSAGES/text.mo b/lti_consumer/translations/ja_JP/LC_MESSAGES/text.mo index 10a45b26449637d4d96fe7e1f1273af5264b1da1..9aa9d3661ca98bb7e51e83e6bba8007da06c259a 100644 Binary files a/lti_consumer/translations/ja_JP/LC_MESSAGES/text.mo and b/lti_consumer/translations/ja_JP/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/ko_KR/LC_MESSAGES/text.mo b/lti_consumer/translations/ko_KR/LC_MESSAGES/text.mo index b3018ddf66b0eb144e60f3b6a4299745c48fd865..7577cdb9adf34b65ba397fb85f87026114d841db 100644 Binary files a/lti_consumer/translations/ko_KR/LC_MESSAGES/text.mo and b/lti_consumer/translations/ko_KR/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/pt_BR/LC_MESSAGES/text.mo b/lti_consumer/translations/pt_BR/LC_MESSAGES/text.mo index 9daf6963dd04ce0b8a03d9b47e5bec5680667b09..6473d702156ace9e6274756d1cb8d2b5304c116d 100644 Binary files a/lti_consumer/translations/pt_BR/LC_MESSAGES/text.mo and b/lti_consumer/translations/pt_BR/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/ru/LC_MESSAGES/text.mo b/lti_consumer/translations/ru/LC_MESSAGES/text.mo index 6a05ffc0c97e991261982be454d381549603f7ff..f3293fc4b4e2ec6a9cc38a42d4bad020656b8ef5 100644 Binary files a/lti_consumer/translations/ru/LC_MESSAGES/text.mo and b/lti_consumer/translations/ru/LC_MESSAGES/text.mo differ diff --git a/lti_consumer/translations/zh_CN/LC_MESSAGES/text.mo b/lti_consumer/translations/zh_CN/LC_MESSAGES/text.mo index bd720a922a781fb3b6e1170eab986f9a6bfb25cf..1009173b8a38841f3dd7241dce522d371dd4415a 100644 Binary files a/lti_consumer/translations/zh_CN/LC_MESSAGES/text.mo and b/lti_consumer/translations/zh_CN/LC_MESSAGES/text.mo differ diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000000000000000000000000000000000000..da687bd6b5ecdc996b945b1b8f19b284cf51150b --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,3 @@ +# Core requirements + translation tools +-r base.txt +edx-i18n-tools==0.5.0 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000000000000000000000000000000000000..e136e8b1d029f1af017a8ff30f2321034480597c --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,159 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# make upgrade +# +appdirs==1.4.4 + # via + # -r requirements/base.txt + # fs +bleach==3.3.0 + # via -r requirements/base.txt +certifi==2020.12.5 + # via + # -r requirements/base.txt + # requests +chardet==4.0.0 + # via + # -r requirements/base.txt + # requests +django-filter==2.4.0 + # via -r requirements/base.txt +django==2.2.19 + # via + # -r requirements/base.txt + # django-filter + # edx-i18n-tools + # edx-opaque-keys + # jsonfield2 +edx-i18n-tools==0.5.0 + # via -r requirements/dev.in +edx-opaque-keys[django]==2.2.0 + # via -r requirements/base.txt +fs==2.4.12 + # via + # -r requirements/base.txt + # xblock +future==0.18.2 + # via + # -r requirements/base.txt + # pyjwkest +idna==2.10 + # via + # -r requirements/base.txt + # requests +jsonfield2==3.0.3 + # via -r requirements/base.txt +lazy==1.4 + # via -r requirements/base.txt +lxml==4.6.2 + # via + # -r requirements/base.txt + # xblock +mako==1.1.4 + # via + # -r requirements/base.txt + # xblock-utils +markupsafe==1.1.1 + # via + # -r requirements/base.txt + # mako + # xblock +oauthlib==3.1.0 + # via -r requirements/base.txt +packaging==20.9 + # via + # -r requirements/base.txt + # bleach +path.py==12.5.0 + # via edx-i18n-tools +path==15.1.2 + # via path.py +pbr==5.5.1 + # via + # -r requirements/base.txt + # stevedore +polib==1.1.0 + # via edx-i18n-tools +pycryptodomex==3.10.1 + # via + # -r requirements/base.txt + # pyjwkest +pyjwkest==1.4.2 + # via -r requirements/base.txt +pymongo==3.11.3 + # via + # -r requirements/base.txt + # edx-opaque-keys +pyparsing==2.4.7 + # via + # -r requirements/base.txt + # packaging +python-dateutil==2.8.1 + # via + # -r requirements/base.txt + # xblock +pytz==2021.1 + # via + # -r requirements/base.txt + # django + # fs + # xblock +pyyaml==5.4.1 + # via + # -r requirements/base.txt + # edx-i18n-tools + # xblock +requests==2.25.1 + # via + # -r requirements/base.txt + # pyjwkest +simplejson==3.17.2 + # via + # -r requirements/base.txt + # xblock-utils +six==1.15.0 + # via + # -r requirements/base.txt + # bleach + # edx-i18n-tools + # fs + # pyjwkest + # python-dateutil + # stevedore + # xblock +sqlparse==0.4.1 + # via + # -r requirements/base.txt + # django +stevedore==1.32.0 + # via + # -r requirements/base.txt + # edx-opaque-keys +urllib3==1.26.3 + # via + # -r requirements/base.txt + # requests +web-fragments==1.0.0 + # via + # -r requirements/base.txt + # xblock + # xblock-utils +webencodings==0.5.1 + # via + # -r requirements/base.txt + # bleach +webob==1.8.7 + # via + # -r requirements/base.txt + # xblock +xblock-utils==2.1.2 + # via -r requirements/base.txt +xblock==1.4.0 + # via + # -r requirements/base.txt + # xblock-utils + +# The following packages are considered to be unsafe in a requirements file: +# setuptools