diff --git a/README.rst b/README.rst index 06f8436dc48e54f00ee531be7c5e8f77aa83c985..f571c2c48a8b02465c012df084cf93d1bf660166 100644 --- a/README.rst +++ b/README.rst @@ -318,6 +318,13 @@ Please do not report security issues in public. Send security concerns via email Changelog ========= +2.11.0 - 2021-06-10 +------------------- + +* Move ``CourseEditLTIFieldsEnabledFlag`` from ``edx-platform`` to this repo + while retaining data from existing model. + + 2.10.1 - 2021-06-09 ------------------- diff --git a/lti_consumer/admin.py b/lti_consumer/admin.py index a32e833ef9abf44b63426a98cbb65ccfb72a4145..c5fae563f0d5192b66abb631c355ce5d9dea93e2 100644 --- a/lti_consumer/admin.py +++ b/lti_consumer/admin.py @@ -1,11 +1,15 @@ """ Admin views for LTI related models. """ +from config_models.admin import KeyedConfigurationModelAdmin from django.contrib import admin + +from lti_consumer.forms import CourseEditLTIFieldsEnabledAdminForm from lti_consumer.models import ( + CourseEditLTIFieldsEnabledFlag, LtiAgsLineItem, - LtiConfiguration, LtiAgsScore, + LtiConfiguration, LtiDlContentItem, ) @@ -19,6 +23,22 @@ class LtiConfigurationAdmin(admin.ModelAdmin): readonly_fields = ('location', 'config_id') +class CourseEditLTIFieldsEnabledFlagAdmin(KeyedConfigurationModelAdmin): + """ + Admin for LTI Fields Editing feature on course-by-course basis. + Allows searching by course id. + """ + form = CourseEditLTIFieldsEnabledAdminForm + search_fields = ['course_id'] + fieldsets = ( + (None, { + 'fields': ('course_id', 'enabled'), + 'description': 'Enter a valid course id. If it is invalid, an error message will be displayed.' + }), + ) + + +admin.site.register(CourseEditLTIFieldsEnabledFlag, CourseEditLTIFieldsEnabledFlagAdmin) admin.site.register(LtiConfiguration, LtiConfigurationAdmin) admin.site.register(LtiAgsLineItem) admin.site.register(LtiAgsScore) diff --git a/lti_consumer/forms.py b/lti_consumer/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..7ea866ad85b6c7e6e2827c810f786189eb7a8c69 --- /dev/null +++ b/lti_consumer/forms.py @@ -0,0 +1,29 @@ +""" +Defines a form for providing validation of LTI consumer course-specific configuration. +""" + + +import logging + +from django import forms + +from lti_consumer.models import CourseEditLTIFieldsEnabledFlag +from lti_consumer.plugin.compat import clean_course_id + +log = logging.getLogger(__name__) + + +class CourseEditLTIFieldsEnabledAdminForm(forms.ModelForm): + """ + Form for LTI consumer course-specific configuration to verify the course id. + """ + + class Meta: + model = CourseEditLTIFieldsEnabledFlag + fields = '__all__' + + def clean_course_id(self): + """ + Validate the course id + """ + return clean_course_id(self) diff --git a/lti_consumer/migrations/0011_courseeditltifieldsenabledflag.py b/lti_consumer/migrations/0011_courseeditltifieldsenabledflag.py new file mode 100644 index 0000000000000000000000000000000000000000..ec6cc4873f5ff5c853a9e78e8a6140d6a78f283e --- /dev/null +++ b/lti_consumer/migrations/0011_courseeditltifieldsenabledflag.py @@ -0,0 +1,62 @@ +# Generated by Django 2.2.20 on 2021-05-05 11:56 +import logging + +import django.db.models.deletion +import opaque_keys.edx.django.models +from django.conf import settings +from django.db import migrations, models + +log = logging.getLogger(__name__) + + +class CreateModelIfNotExists(migrations.CreateModel): + """ + Creates the database table if it doesn't already exist. + + This can be used to move a database model from one app to another. + """ + + def database_forwards(self, app_label, schema_editor, from_state, to_state): + # Get the name of the database table + db_table = to_state.apps.get_model(app_label, self.name)._meta.db_table + # If the database table for this model already exists, do nothing, otherwise + # create the table as usual. + if db_table in schema_editor.connection.introspection.table_names(): + log.info(f"{db_table} already exists. Skipping creation.") + else: + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + # Do nothing when applying this is reverse because the original creation of this + # table was handled by edx-platform, so we don't want to delete the table on reversal. + pass + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('lti_consumer', '0010_backfill-empty-string-lti-config'), + ] + + operations = [ + CreateModelIfNotExists( + name='CourseEditLTIFieldsEnabledFlag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), + ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), + ('course_id', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)), + ('changed_by', + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + verbose_name='Changed by', + )), + ], + options={ + 'db_table': 'xblock_config_courseeditltifieldsenabledflag', + }, + ) + ] diff --git a/lti_consumer/models.py b/lti_consumer/models.py index 3cd76ec732023250ff7e8b37bc84599830ee80ab..dec339eab7712fb50c1dcb207e8eb4c2056f8a2d 100644 --- a/lti_consumer/models.py +++ b/lti_consumer/models.py @@ -10,7 +10,8 @@ from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField from Cryptodome.PublicKey import RSA -from opaque_keys.edx.django.models import UsageKeyField +from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField +from config_models.models import ConfigurationModel # LTI 1.1 from lti_consumer.lti_1p1.consumer import LtiConsumer1p1 @@ -18,6 +19,7 @@ from lti_consumer.lti_1p1.consumer import LtiConsumer1p1 from lti_consumer.lti_1p3.consumer import LtiAdvantageConsumer from lti_consumer.lti_1p3.key_handlers import PlatformKeyHandler from lti_consumer.plugin import compat +from lti_consumer.plugin.compat import request_cached from lti_consumer.utils import ( get_lms_base, get_lti_ags_lineitems_url, @@ -531,3 +533,54 @@ class LtiDlContentItem(models.Model): class Meta: app_label = 'lti_consumer' + + +class CourseEditLTIFieldsEnabledFlag(ConfigurationModel): + """ + Enables the editing of "request username" and "request email" fields + of LTI consumer for a specific course. + + .. no_pii: + """ + KEY_FIELDS = ('course_id',) + + course_id = CourseKeyField(max_length=255, db_index=True) + + @classmethod + @request_cached + def lti_access_to_learners_editable(cls, course_id, is_already_sharing_learner_info): + """ + Looks at the currently active configuration model to determine whether + the feature that enables editing of "request username" and "request email" + fields of LTI consumer is available or not. + + Backwards Compatibility: + Enable this feature for a course run who was sharing learner username/email + in the past. + + Arguments: + course_id (CourseKey): course id for which we need to check this configuration + is_already_sharing_learner_info (bool): indicates whether LTI consumer is + already sharing edX learner username/email. + """ + course_specific_config = (CourseEditLTIFieldsEnabledFlag.objects + .filter(course_id=course_id) + .order_by('-change_date') + .first()) + + if is_already_sharing_learner_info and not course_specific_config: + CourseEditLTIFieldsEnabledFlag.objects.create(course_id=course_id, enabled=True) + return True + + return course_specific_config.enabled if course_specific_config else False + + def __str__(self): + return ( + f"Course '{self.course_id}': " + f"Edit LTI access to Learner information {'' if self.enabled else 'Not '}Enabled" + ) + + class Meta: + # This model was moved from edx-platform, with intention of retaining existing data. + # This is referencing the original table name. + db_table = "xblock_config_courseeditltifieldsenabledflag" diff --git a/lti_consumer/plugin/compat.py b/lti_consumer/plugin/compat.py index c5cbcefc747ec0cb4e54a14c3941d09fe499f9c2..588f81991357f5be27389a7959c69021f5e364e7 100644 --- a/lti_consumer/plugin/compat.py +++ b/lti_consumer/plugin/compat.py @@ -1,10 +1,19 @@ """ Compatibility layer to isolate core-platform method calls from implementation. """ +import logging +from typing import Callable + from django.core.exceptions import ValidationError +from django.forms import ModelForm +from opaque_keys.edx.keys import CourseKey + from lti_consumer.exceptions import LtiError +log = logging.getLogger(__name__) + + # Waffle flags configuration # Namespace @@ -185,3 +194,25 @@ def get_lti_pii_course_waffle_flag(): # pylint: disable=import-error,import-outside-toplevel from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag return CourseWaffleFlag(WAFFLE_NAMESPACE, LTI_NRPS_TRANSMIT_PII, __name__) + + +def request_cached(func) -> Callable[[Callable], Callable]: + """ + Import the `request_cached` decorator from LMS and apply it if available. + """ + try: + # pylint: disable=import-outside-toplevel + from openedx.core.lib.cache_utils import request_cached as lms_request_cached + return lms_request_cached(func) + except ImportError: + log.warning("Unable to import `request_cached`. This is normal if running tests.") + return func + + +def clean_course_id(model_form: ModelForm) -> CourseKey: + """ + Import and run `clean_course_id` from LMS + """ + # pylint: disable=import-error,import-outside-toplevel + from openedx.core.lib.courses import clean_course_id as lms_clean_course_id + return lms_clean_course_id(model_form) diff --git a/lti_consumer/tests/unit/test_models.py b/lti_consumer/tests/unit/test_models.py index 67b9c6baf87d0f0e71d8c552bc1153fe7ecd6a3b..b6f3b9ac9361ae2c664524ab91adc831c9a540ec 100644 --- a/lti_consumer/tests/unit/test_models.py +++ b/lti_consumer/tests/unit/test_models.py @@ -1,20 +1,26 @@ """ Unit tests for LTI models. """ -from datetime import datetime, timedelta, timezone +from contextlib import contextmanager +from datetime import datetime, timedelta from unittest.mock import patch +import ddt from Cryptodome.PublicKey import RSA from django.core.exceptions import ValidationError from django.test.testcases import TestCase +from django.utils import timezone +from edx_django_utils.cache import RequestCache from jwkest.jwk import RSAKey +from opaque_keys.edx.locator import CourseLocator from lti_consumer.lti_1p1.consumer import LtiConsumer1p1 from lti_consumer.lti_xblock import LtiConsumerXBlock from lti_consumer.models import ( + CourseEditLTIFieldsEnabledFlag, LtiAgsLineItem, - LtiConfiguration, LtiAgsScore, + LtiConfiguration, LtiDlContentItem, ) from lti_consumer.tests.unit.test_utils import make_xblock @@ -353,3 +359,85 @@ class TestLtiDlContentItemModel(TestCase): str(content_item), "[CONFIG_ON_XBLOCK] lti_1p3 - block-v1:course+test+2020+type@problem+block@test: image" ) + + +@contextmanager +def lti_consumer_fields_editing_flag(course_id, enabled_for_course=False): + """ + Yields CourseEditLTIFieldsEnabledFlag record for unit tests + + Arguments: + course_id (CourseLocator): course locator to control this feature for. + enabled_for_course (bool): whether feature is enabled for 'course_id' + """ + RequestCache.clear_all_namespaces() + CourseEditLTIFieldsEnabledFlag.objects.create(course_id=course_id, enabled=enabled_for_course) + yield + + +@ddt.ddt +class TestLTIConsumerHideFieldsFlag(TestCase): + """ + Tests the behavior of the flags for lti consumer fields' editing feature. + These are set via Django admin settings. + """ + + def setUp(self): + super().setUp() + self.course_id = CourseLocator(org="edx", course="course", run="run") + + @ddt.data( + (True, True), + (True, False), + (False, True), + (False, False), + ) + @ddt.unpack + def test_lti_fields_editing_feature_flags(self, enabled_for_course, is_already_sharing_learner_info): + """ + Test that feature flag works correctly with course-specific configuration in combination with + a boolean which indicates whether a course-run already sharing learner username/email - given + the course-specific configuration record is present. + """ + with lti_consumer_fields_editing_flag( + course_id=self.course_id, + enabled_for_course=enabled_for_course + ): + feature_enabled = CourseEditLTIFieldsEnabledFlag.lti_access_to_learners_editable( + self.course_id, + is_already_sharing_learner_info, + ) + self.assertEqual(feature_enabled, enabled_for_course) + + @ddt.data(True, False) + def test_lti_fields_editing_is_backwards_compatible(self, is_already_sharing_learner_info): + """ + Test that feature flag works correctly with a boolean which indicates whether a course-run already + sharing learner username/email - given the course-specific configuration record is not set previously. + + This tests the backward compatibility which currently is: if an existing course run is already + sharing learner information then this feature should be enabled for that course run by default. + """ + feature_enabled = CourseEditLTIFieldsEnabledFlag.lti_access_to_learners_editable( + self.course_id, + is_already_sharing_learner_info, + ) + feature_flag_created = CourseEditLTIFieldsEnabledFlag.objects.filter(course_id=self.course_id).exists() + self.assertEqual(feature_flag_created, is_already_sharing_learner_info) + self.assertEqual(feature_enabled, is_already_sharing_learner_info) + + def test_enable_disable_course_flag(self): + """ + Ensures that the flag, once enabled for a course, can also be disabled. + """ + with lti_consumer_fields_editing_flag( + course_id=self.course_id, + enabled_for_course=True + ): + self.assertTrue(CourseEditLTIFieldsEnabledFlag.lti_access_to_learners_editable(self.course_id, False)) + + with lti_consumer_fields_editing_flag( + course_id=self.course_id, + enabled_for_course=False + ): + self.assertFalse(CourseEditLTIFieldsEnabledFlag.lti_access_to_learners_editable(self.course_id, False)) diff --git a/requirements/base.in b/requirements/base.in index bd89e7b262d3e25038c0367a71bd4d18e723bea0..8f799d6776038e68759dc49b45a0ce35529662f5 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -14,3 +14,4 @@ pyjwkest edx-opaque-keys[django] django-filter jsonfield2 +django-config-models # Configuration models for Django allowing config management with auditing diff --git a/requirements/base.txt b/requirements/base.txt index 1af3ae7f4aceeb750416b525b8dead4f40b6c083..fc0cb17d5aeaafbea18544feb4d19d4c9091f77f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,15 +12,32 @@ certifi==2021.5.30 # via requests chardet==4.0.0 # via requests +django-config-models==2.1.1 + # via + # -c requirements/constraints.txt + # -r requirements/base.in +django-crum==0.7.9 + # via edx-django-utils django-filter==2.4.0 # via -r requirements/base.in +django-waffle==2.2.0 + # via edx-django-utils django==2.2.24 # via # -c requirements/common_constraints.txt # -r requirements/base.in + # django-config-models # django-filter + # djangorestframework + # edx-django-utils # edx-opaque-keys # jsonfield2 +djangorestframework==3.12.4 + # via + # -c requirements/constraints.txt + # django-config-models +edx-django-utils==4.1.0 + # via django-config-models edx-opaque-keys[django]==2.2.1 # via -r requirements/base.in fs==2.4.13 @@ -29,8 +46,10 @@ future==0.18.2 # via pyjwkest idna==2.10 # via requests -jsonfield2==4.0.0.post0 - # via -r requirements/base.in +jsonfield2==3.0.3 + # via + # -c requirements/constraints.txt + # -r requirements/base.in lazy==1.4 # via -r requirements/base.in lxml==4.6.3 @@ -45,12 +64,16 @@ markupsafe==2.0.1 # via # mako # xblock +newrelic==6.4.1.158 + # via edx-django-utils oauthlib==3.1.1 # via -r requirements/base.in packaging==20.9 # via bleach pbr==5.6.0 # via stevedore +psutil==5.8.0 + # via edx-django-utils pycryptodomex==3.10.1 # via # -r requirements/base.in @@ -83,7 +106,9 @@ six==1.16.0 sqlparse==0.4.1 # via django stevedore==3.3.0 - # via edx-opaque-keys + # via + # edx-django-utils + # edx-opaque-keys urllib3==1.26.5 # via requests web-fragments==1.0.0 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 2e75642932749671d0b26458e2aa87b312de5497..d5584b91eaefe7b6844cc135c99f9429aea686ee 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -10,8 +10,14 @@ # Common constraints for edx repos -c common_constraints.txt - + # TODO: Many pinned dependencies should be unpinned and/or moved to this constraints file. # Same as in edx-platform djangorestframework<4 + +# jsonfield2 3.1.0 drops support for python 3.5 +jsonfield2<3.1.0 + +# Same as in edx-platform +django-config-models>=1.0.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index cf5ef56a6e7393019f3c0765b5a5c3f547d6eedd..0d3b798d48f34d0d16e3f852975e306e535fd9c3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -18,15 +18,37 @@ chardet==4.0.0 # via # -r requirements/base.txt # requests +django-config-models==2.1.1 + # via -r requirements/base.txt +django-crum==0.7.9 + # via + # -r requirements/base.txt + # edx-django-utils django-filter==2.4.0 # via -r requirements/base.txt +django-waffle==2.2.0 + # via + # -r requirements/base.txt + # edx-django-utils django==2.2.24 # via # -r requirements/base.txt + # django-config-models + # django-crum # django-filter + # djangorestframework + # edx-django-utils # edx-i18n-tools # edx-opaque-keys # jsonfield2 +djangorestframework==3.12.4 + # via + # -r requirements/base.txt + # django-config-models +edx-django-utils==4.1.0 + # via + # -r requirements/base.txt + # django-config-models edx-i18n-tools==0.5.0 # via -r requirements/dev.in edx-opaque-keys[django]==2.2.1 @@ -43,7 +65,7 @@ idna==2.10 # via # -r requirements/base.txt # requests -jsonfield2==4.0.0.post0 +jsonfield2==3.0.3 # via -r requirements/base.txt lazy==1.4 # via -r requirements/base.txt @@ -60,6 +82,10 @@ markupsafe==2.0.1 # -r requirements/base.txt # mako # xblock +newrelic==6.4.1.158 + # via + # -r requirements/base.txt + # edx-django-utils oauthlib==3.1.1 # via -r requirements/base.txt packaging==20.9 @@ -68,7 +94,7 @@ packaging==20.9 # bleach path.py==12.5.0 # via edx-i18n-tools -path==15.1.2 +path==16.0.0 # via path.py pbr==5.6.0 # via @@ -76,6 +102,10 @@ pbr==5.6.0 # stevedore polib==1.1.1 # via edx-i18n-tools +psutil==5.8.0 + # via + # -r requirements/base.txt + # edx-django-utils pycryptodomex==3.10.1 # via # -r requirements/base.txt @@ -128,6 +158,7 @@ sqlparse==0.4.1 stevedore==3.3.0 # via # -r requirements/base.txt + # edx-django-utils # edx-opaque-keys urllib3==1.26.5 # via diff --git a/requirements/test.txt b/requirements/test.txt index c19a819bfdc0e63334da16c4d4c79333d2efb436..fe9387087993ad314356866adb86bb6df1055243 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -51,16 +51,31 @@ cryptography==3.4.7 # via secretstorage ddt==1.4.2 # via -r requirements/test.in +django-config-models==2.1.1 + # via + # -c requirements/constraints.txt + # -r requirements/base.txt +django-crum==0.7.9 + # via + # -r requirements/base.txt + # edx-django-utils django-filter==2.4.0 # via -r requirements/base.txt django-pyfs==3.0 # via -r requirements/test.in +django-waffle==2.2.0 + # via + # -r requirements/base.txt + # edx-django-utils # via # -c requirements/common_constraints.txt # -r requirements/base.txt + # django-config-models + # django-crum # django-filter # django-pyfs # djangorestframework + # edx-django-utils # edx-lint # edx-opaque-keys # jsonfield2 @@ -68,13 +83,19 @@ django-pyfs==3.0 djangorestframework==3.12.4 # via # -c requirements/constraints.txt + # -r requirements/base.txt # -r requirements/test.in + # django-config-models docopt==0.6.2 # via coveralls docutils==0.16 # via # -c requirements/common_constraints.txt # readme-renderer +edx-django-utils==4.1.0 + # via + # -r requirements/base.txt + # django-config-models edx-lint==5.0.0 # via -r requirements/test.in edx-opaque-keys[django]==2.2.1 @@ -111,8 +132,10 @@ jmespath==0.10.0 # via # boto3 # botocore -jsonfield2==4.0.0.post0 - # via -r requirements/base.txt +jsonfield2==3.0.3 + # via + # -c requirements/constraints.txt + # -r requirements/base.txt keyring==23.0.1 # via twine lazy-object-proxy==1.6.0 @@ -137,6 +160,10 @@ mccabe==0.6.1 # via pylint mock==4.0.3 # via -r requirements/test.in +newrelic==6.4.1.158 + # via + # -r requirements/base.txt + # edx-django-utils oauthlib==3.1.1 # via -r requirements/base.txt packaging==20.9 @@ -149,6 +176,10 @@ pbr==5.6.0 # stevedore pkginfo==1.7.0 # via twine +psutil==5.8.0 + # via + # -r requirements/base.txt + # edx-django-utils pycodestyle==2.7.0 # via -r requirements/test.in pycparser==2.20 @@ -242,12 +273,13 @@ stevedore==3.3.0 # via # -r requirements/base.txt # code-annotations + # edx-django-utils # edx-opaque-keys text-unidecode==1.3 # via python-slugify toml==0.10.2 # via pylint -tqdm==4.61.0 +tqdm==4.61.1 # via twine twine==3.4.1 # via -r requirements/test.in diff --git a/requirements/travis.txt b/requirements/travis.txt index 6147e6ad9ba4787dfd8378dff7b4f96284c72e24..dfe95c130335ebfc8faf0fa7cd2cd06708119278 100644 --- a/requirements/travis.txt +++ b/requirements/travis.txt @@ -74,17 +74,32 @@ distlib==0.3.2 # via # -r requirements/tox.txt # virtualenv +django-config-models==2.1.1 + # via + # -c requirements/constraints.txt + # -r requirements/test.txt +django-crum==0.7.9 + # via + # -r requirements/test.txt + # edx-django-utils django-filter==2.4.0 # via -r requirements/test.txt django-pyfs==3.0 # via -r requirements/test.txt +django-waffle==2.2.0 + # via + # -r requirements/test.txt + # edx-django-utils django==2.2.24 # via # -c requirements/common_constraints.txt # -r requirements/test.txt + # django-config-models + # django-crum # django-filter # django-pyfs # djangorestframework + # edx-django-utils # edx-lint # edx-opaque-keys # jsonfield2 @@ -93,6 +108,7 @@ djangorestframework==3.12.4 # via # -c requirements/constraints.txt # -r requirements/test.txt + # django-config-models docopt==0.6.2 # via # -r requirements/test.txt @@ -102,6 +118,10 @@ docutils==0.16 # -c requirements/common_constraints.txt # -r requirements/test.txt # readme-renderer +edx-django-utils==4.1.0 + # via + # -r requirements/test.txt + # django-config-models edx-lint==5.0.0 # via -r requirements/test.txt edx-opaque-keys[django]==2.2.1 @@ -152,8 +172,10 @@ jmespath==0.10.0 # -r requirements/test.txt # boto3 # botocore -jsonfield2==4.0.0.post0 - # via -r requirements/test.txt +jsonfield2==3.0.3 + # via + # -c requirements/constraints.txt + # -r requirements/test.txt keyring==23.0.1 # via # -r requirements/test.txt @@ -184,6 +206,10 @@ mccabe==0.6.1 # pylint mock==4.0.3 # via -r requirements/test.txt +newrelic==6.4.1.158 + # via + # -r requirements/test.txt + # edx-django-utils oauthlib==3.1.1 # via -r requirements/test.txt packaging==20.9 @@ -204,6 +230,10 @@ pluggy==0.13.1 # via # -r requirements/tox.txt # tox +psutil==5.8.0 + # via + # -r requirements/test.txt + # edx-django-utils py==1.10.0 # via # -r requirements/tox.txt @@ -325,6 +355,7 @@ stevedore==3.3.0 # via # -r requirements/test.txt # code-annotations + # edx-django-utils # edx-opaque-keys text-unidecode==1.3 # via @@ -338,7 +369,7 @@ toml==0.10.2 # tox tox==3.23.1 # via -r requirements/tox.txt -tqdm==4.61.0 +tqdm==4.61.1 # via # -r requirements/test.txt # twine diff --git a/setup.py b/setup.py index 0f7c54eac2b4e947efb1e33c74788375b2a00397..38a2ad4e0d02f47f192a71442d5ad04793021201 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ with open('README.rst') as _f: setup( name='lti-consumer-xblock', - version='2.10.1', + version='2.11.0', author='Open edX project', author_email='oscm@edx.org', description='This XBlock implements the consumer side of the LTI specification.',