From ad7f6019fe97044c6414ce6710c9c74507c7ee89 Mon Sep 17 00:00:00 2001 From: Awais Jibran Date: Wed, 31 Mar 2021 13:26:14 +0500 Subject: [PATCH] Integrate "olxcleaner" with course import (#27164) * Integrating olxcleaner in course import * Adding toggle removal date and addressing pylint issues. --- cms/djangoapps/cms_user_tasks/signals.py | 7 +- cms/djangoapps/cms_user_tasks/tasks.py | 12 +- cms/djangoapps/cms_user_tasks/tests.py | 1 - cms/djangoapps/contentstore/tasks.py | 111 +++++++++++++----- cms/djangoapps/contentstore/toggles.py | 23 ++++ .../emails/user_task_complete_email.txt | 13 ++ requirements/edx-sandbox/py35.txt | 2 +- requirements/edx/base.in | 1 + requirements/edx/base.txt | 8 +- requirements/edx/development.txt | 8 +- requirements/edx/testing.txt | 8 +- 11 files changed, 148 insertions(+), 46 deletions(-) diff --git a/cms/djangoapps/cms_user_tasks/signals.py b/cms/djangoapps/cms_user_tasks/signals.py index 815d69f664..fadace693e 100644 --- a/cms/djangoapps/cms_user_tasks/signals.py +++ b/cms/djangoapps/cms_user_tasks/signals.py @@ -1,8 +1,6 @@ """ Receivers of signals sent from django-user-tasks """ - - import logging from urllib.parse import urljoin @@ -33,7 +31,6 @@ def user_task_stopped_handler(sender, **kwargs): # pylint: disable=unused-argum None """ status = kwargs['status'] - # Only send email when the entire task is complete, should only send when # a chain / chord / etc completes, not on sub-tasks. if status.parent is None: @@ -47,8 +44,10 @@ def user_task_stopped_handler(sender, **kwargs): # pylint: disable=unused-argum reverse('usertaskstatus-detail', args=[status.uuid]) ) + user_email = status.user.email + task_name = status.name.lower() try: # Need to str state_text here because it is a proxy object and won't serialize correctly - send_task_complete_email.delay(status.name.lower(), str(status.state_text), status.user.email, detail_url) + send_task_complete_email.delay(task_name, str(status.state_text), user_email, detail_url) except Exception: # pylint: disable=broad-except LOGGER.exception("Unable to queue send_task_complete_email") diff --git a/cms/djangoapps/cms_user_tasks/tasks.py b/cms/djangoapps/cms_user_tasks/tasks.py index 01515182ac..89d4e7a217 100644 --- a/cms/djangoapps/cms_user_tasks/tasks.py +++ b/cms/djangoapps/cms_user_tasks/tasks.py @@ -1,7 +1,7 @@ """ Celery tasks used by cms_user_tasks """ - +import json from boto.exception import NoAuthHandlerFound from celery import shared_task @@ -21,7 +21,7 @@ TASK_COMPLETE_EMAIL_TIMEOUT = 60 @shared_task(bind=True) @set_code_owner_attribute -def send_task_complete_email(self, task_name, task_state_text, dest_addr, detail_url): +def send_task_complete_email(self, task_name, task_state_text, dest_addr, detail_url, olx_validation_text=None): """ Sending an email to the users when an async task completes. """ @@ -32,6 +32,14 @@ def send_task_complete_email(self, task_name, task_state_text, dest_addr, detail 'task_status': task_state_text, 'detail_url': detail_url } + if olx_validation_text: + try: + olx_validations = json.loads(olx_validation_text) + context['olx_validation_errors'] = True + context['error_summary'] = olx_validations.get('error_summary') + context['error_report'] = olx_validations.get('error_report') + except ValueError: # includes simplejson.decoder.JSONDecodeError + LOGGER.error(f'Unable to parse CourseOlx validation text: {olx_validation_text}') subject = render_to_string('emails/user_task_complete_email_subject.txt', context) # Eliminate any newlines diff --git a/cms/djangoapps/cms_user_tasks/tests.py b/cms/djangoapps/cms_user_tasks/tests.py index bbc6cd098b..130f3f6aef 100644 --- a/cms/djangoapps/cms_user_tasks/tests.py +++ b/cms/djangoapps/cms_user_tasks/tests.py @@ -2,7 +2,6 @@ Unit tests for integration of the django-user-tasks app and its REST API. """ - import logging from unittest import mock from uuid import uuid4 diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 149179a156..0d1dbbff4a 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -10,6 +10,7 @@ import tarfile from datetime import datetime from tempfile import NamedTemporaryFile, mkdtemp +import olxcleaner from ccx_keys.locator import CCXLocator from celery import shared_task from celery.utils.log import get_task_logger @@ -25,9 +26,10 @@ from edx_django_utils.monitoring import ( set_code_owner_attribute, set_code_owner_attribute_from_module, set_custom_attribute, - set_custom_attributes_for_course_key, + set_custom_attributes_for_course_key ) -from common.djangoapps.util.monitoring import monitor_import_failure +from olxcleaner.exceptions import ErrorLevel +from olxcleaner.reporting import report_error_summary, report_errors from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import LibraryLocator from organizations.api import add_organization_course, ensure_organization @@ -47,6 +49,7 @@ from cms.djangoapps.contentstore.utils import initialize_permissions, reverse_us from cms.djangoapps.models.settings.course_metadata import CourseMetadata from common.djangoapps.course_action_state.models import CourseRerunState from common.djangoapps.student.auth import has_course_author_access +from common.djangoapps.util.monitoring import monitor_import_failure from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines from openedx.core.djangoapps.embargo.models import CountryAccessRule, RestrictedCourse from openedx.core.lib.extract_tar import safetar_extractall @@ -60,6 +63,7 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml, export_librar from xmodule.modulestore.xml_importer import import_course_from_xml, import_library_from_xml from .outlines import update_outline_from_modulestore +from .toggles import course_import_olx_validation_is_enabled User = get_user_model() @@ -442,6 +446,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan return file_is_valid def file_exists_in_storage(): + """Verify archive path exists in storage.""" archive_path_exists = course_import_export_storage.exists(archive_path) if not archive_path_exists: @@ -452,6 +457,39 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan monitor_import_failure(courselike_key, current_step, message=message) return archive_path_exists + def verify_root_name_exists(course_dir, root_name): + """Verify root xml file exists.""" + + def get_all_files(directory): + """ + For each file in the directory, yield a 2-tuple of (file-name, + directory-path) + """ + for directory_path, _dirnames, filenames in os.walk(directory): + for filename in filenames: + yield (filename, directory_path) + + def get_dir_for_filename(directory, filename): + """ + Returns the directory path for the first file found in the directory + with the given name. If there is no file in the directory with + the specified name, return None. + """ + for name, directory_path in get_all_files(directory): + if name == filename: + return directory_path + return None + + dirpath = get_dir_for_filename(course_dir, root_name) + if not dirpath: + message = f'Could not find the {root_name} file in the package.' + with translation_language(language): + self.status.fail(_('Could not find the {0} file in the package.').format(root_name)) + LOGGER.error(f'{log_prefix}: {message}') + monitor_import_failure(courselike_key, current_step, message=message) + return + return dirpath + user = validate_user() if not user: return @@ -543,34 +581,11 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan self.status.increment_completed_steps() LOGGER.info(f'{log_prefix}: Uploaded file extracted. Verification step started') - # find the 'course.xml' file - def get_all_files(directory): - """ - For each file in the directory, yield a 2-tuple of (file-name, - directory-path) - """ - for directory_path, _dirnames, filenames in os.walk(directory): - for filename in filenames: - yield (filename, directory_path) - - def get_dir_for_filename(directory, filename): - """ - Returns the directory path for the first file found in the directory - with the given name. If there is no file in the directory with - the specified name, return None. - """ - for name, directory_path in get_all_files(directory): - if name == filename: - return directory_path - return None - - dirpath = get_dir_for_filename(course_dir, root_name) + dirpath = verify_root_name_exists(course_dir, root_name) if not dirpath: - message = f'Could not find the {root_name} file in the package.' - with translation_language(language): - self.status.fail(_('Could not find the {0} file in the package.').format(root_name)) - LOGGER.error(f'{log_prefix}: {message}') - monitor_import_failure(courselike_key, current_step, message=message) + return + + if not validate_course_olx(courselike_key, dirpath, self.status): return dirpath = os.path.relpath(dirpath, data_root) @@ -643,3 +658,41 @@ def update_outline_from_modulestore_task(course_key_str): except Exception: # pylint disable=broad-except LOGGER.exception("Could not create course outline for course %s", course_key_str) raise # Re-raise so that errors are noted in reporting. + + +def validate_course_olx(courselike_key, course_dir, status): + """ + Validates course olx and records the errors as artifact. + Arguments: + course_dir: complete path to the course olx + status: UserTaskStatus object. + """ + olx_is_valid = True + log_prefix = f'Course import {courselike_key}' + + if not course_import_olx_validation_is_enabled(): + return olx_is_valid + try: + __, errorstore, __ = olxcleaner.validate(course_dir, steps=8) + except Exception: # pylint: disable=broad-except + LOGGER.exception(f'{log_prefix}: CourseOlx Could not be validated') + return olx_is_valid + + has_errors = errorstore.return_error(ErrorLevel.ERROR.value) + if not has_errors: + return olx_is_valid + + errorstore.errors = [error for error in errorstore.errors if error.level_val == ErrorLevel.ERROR.value] + error_summary = report_error_summary(errorstore) + error_report = report_errors(errorstore) + message = json.dumps({ + 'error_summary': error_summary, + 'error_report': error_report, + }) + UserTaskArtifact.objects.create(status=status, name='OLX_VALIDATION_ERROR', text=message) + LOGGER.error(f'{log_prefix}: CourseOlx validation failed') + + # TODO: Do not fail the task until we have some data about kinds of + # olx validation failures. TNL-8151 + + return olx_is_valid diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 899ccabb52..281288b255 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -38,9 +38,32 @@ SPLIT_LIBRARY_ON_DASHBOARD = LegacyWaffleFlag( module_name=__name__ ) +# Waffle flag to enable olx validation during course import. +# .. toggle_name: course_import_olx_validation +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Studio Import +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2021-04-01 +# .. toggle_target_removal_date: 2021-05-01 +# .. toggle_warnings: ?? +# .. toggle_tickets: TNL-8151 +COURSE_IMPORT_OLX_VALIDATION = LegacyWaffleFlag( + waffle_namespace=LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE), + flag_name='course_import_olx_validation', + module_name=__name__ +) + def split_library_view_on_dashboard(): """ check if data new view for library is enabled on studio dashboard. """ return SPLIT_LIBRARY_ON_DASHBOARD.is_enabled() + + +def course_import_olx_validation_is_enabled(): + """ + Check if course olx validation is enabled on course import. + """ + return COURSE_IMPORT_OLX_VALIDATION.is_enabled() diff --git a/cms/templates/emails/user_task_complete_email.txt b/cms/templates/emails/user_task_complete_email.txt index 05b49cc8b8..3cfd1fe014 100644 --- a/cms/templates/emails/user_task_complete_email.txt +++ b/cms/templates/emails/user_task_complete_email.txt @@ -9,3 +9,16 @@ ${_("Your {task_name} task has completed with the status '{task_status}'. Use th ${_("Your {task_name} task has completed with the status '{task_status}'. Sign in to view the details of your task or download any files created.").format(task_name=task_name, task_status=task_status)} % endif + +% if olx_validation_errors: + +${_("Here are some validation errors we found with your course content.")} + + +% for error in error_report: + ${error} +% endfor + +% else: + +% endif diff --git a/requirements/edx-sandbox/py35.txt b/requirements/edx-sandbox/py35.txt index 06ad16a98f..ae6d3dd84d 100644 --- a/requirements/edx-sandbox/py35.txt +++ b/requirements/edx-sandbox/py35.txt @@ -20,7 +20,7 @@ matplotlib==2.2.4 # via -c requirements/edx-sandbox/../constraints.txt, mpmath==1.2.1 # via sympy networkx==2.2 # via -r requirements/edx-sandbox/py35.in nltk==3.5 # via -r requirements/edx-sandbox/shared.txt, chem -numpy==1.16.5 # via -r requirements/edx-sandbox/py35.in, chem, matplotlib, openedx-calc, scipy +numpy==1.16.5 # via -r requirements/edx-sandbox/py35.in, chem, matplotlib, openedx-calc openedx-calc==1.0.9 # via -r requirements/edx-sandbox/py35.in pycparser==2.20 # via -r requirements/edx-sandbox/shared.txt, cffi pyparsing==2.2.0 # via -r requirements/edx-sandbox/py35.in, chem, matplotlib, openedx-calc diff --git a/requirements/edx/base.in b/requirements/edx/base.in index a6a49643e4..4555aa7169 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -115,6 +115,7 @@ mysqlclient # Driver for the default production relation newrelic # New Relic agent for performance monitoring nodeenv # Utility for managing Node.js environments; we use this for deployments and testing oauthlib # OAuth specification support for authenticating via LTI or other Open edX services +olxcleaner # Library to support syntex validation during course import openedx-calc # Library supporting mathematical calculations for Open edX ora2 piexif # Exif image metadata manipulation, used in the profile_images app diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 34e81a7682..204603c279 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -147,7 +147,7 @@ lazy==1.4 # via -r requirements/edx/paver.txt, acid-xblock, lti- libsass==0.10.0 # via -r requirements/edx/paver.txt, ora2 loremipsum==1.0.5 # via ora2 lti-consumer-xblock==2.7.1 # via -r requirements/edx/base.in -lxml==4.5.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/../edx-sandbox/shared.txt, capa, edxval, lti-consumer-xblock, ora2, safe-lxml, xblock, xmlsec +lxml==4.5.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/../edx-sandbox/shared.txt, capa, edxval, lti-consumer-xblock, olxcleaner, ora2, safe-lxml, xblock, xmlsec mailsnake==1.6.4 # via -r requirements/edx/base.in mako==1.1.4 # via -r requirements/edx/base.in, acid-xblock, lti-consumer-xblock, xblock-google-drive, xblock-utils markdown==3.3.4 # via -r requirements/edx/base.in, django-wiki, staff-graded-xblock, xblock-poll @@ -164,6 +164,7 @@ nltk==3.5 # via -r requirements/edx/../edx-sandbox/shared.txt, c nodeenv==1.5.0 # via -r requirements/edx/base.in numpy==1.20.2 # via chem, openedx-calc, scipy oauthlib==3.0.1 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in, django-oauth-toolkit, lti-consumer-xblock, requests-oauthlib, social-auth-core +olxcleaner==0.1.4 # via -r requirements/edx/base.in openedx-calc==2.0.1 # via -r requirements/edx/base.in ora2==3.4.0 # via -r requirements/edx/base.in packaging==20.9 # via bleach, drf-yasg @@ -182,18 +183,19 @@ pycryptodomex==3.10.1 # via -r requirements/edx/base.in, edx-proctoring, lti pygments==2.8.1 # via -r requirements/edx/base.in pyjwkest==1.4.2 # via -r requirements/edx/base.in, edx-drf-extensions, lti-consumer-xblock pyjwt[crypto]==1.7.1 # via -r requirements/edx/base.in, drf-jwt, edx-rest-api-client, social-auth-core +pylatexenc==2.9 # via olxcleaner pymongo==3.10.1 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in, -r requirements/edx/paver.txt, edx-opaque-keys, event-tracking, mongodbproxy, mongoengine pynliner==0.8.0 # via -r requirements/edx/base.in pyparsing==2.4.7 # via chem, openedx-calc, packaging, pycontracts pysrt==1.1.2 # via -r requirements/edx/base.in, edxval -python-dateutil==2.4.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in, analytics-python, botocore, edx-ace, edx-drf-extensions, edx-enterprise, edx-event-routing-backends, edx-proctoring, icalendar, ora2, xblock +python-dateutil==2.4.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in, analytics-python, botocore, edx-ace, edx-drf-extensions, edx-enterprise, edx-event-routing-backends, edx-proctoring, icalendar, olxcleaner, ora2, xblock python-levenshtein==0.12.2 # via -r requirements/edx/base.in python-memcached==1.59 # via -r requirements/edx/paver.txt python-slugify==4.0.1 # via code-annotations python-swiftclient==3.11.1 # via ora2 python3-openid==3.2.0 ; python_version >= "3" # via -r requirements/edx/base.in, social-auth-core python3-saml==1.9.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in -pytz==2021.1 # via -r requirements/edx/base.in, babel, capa, celery, django, django-ses, edx-completion, edx-enterprise, edx-event-routing-backends, edx-proctoring, edx-submissions, edx-tincan-py35, event-tracking, fs, icalendar, ora2, tincan, xblock +pytz==2021.1 # via -r requirements/edx/base.in, babel, capa, celery, django, django-ses, edx-completion, edx-enterprise, edx-event-routing-backends, edx-proctoring, edx-submissions, edx-tincan-py35, event-tracking, fs, icalendar, olxcleaner, ora2, tincan, xblock pyuca==1.2 # via -r requirements/edx/base.in pyyaml==5.4.1 # via -r requirements/edx/base.in, code-annotations, edx-django-release-util, edx-i18n-tools, xblock random2==1.0.1 # via -r requirements/edx/base.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 88457f8014..3025640a33 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -176,7 +176,7 @@ lazy==1.4 # via -r requirements/edx/testing.txt, acid-xblock, bo libsass==0.10.0 # via -r requirements/edx/testing.txt, ora2 loremipsum==1.0.5 # via -r requirements/edx/testing.txt, ora2 lti-consumer-xblock==2.7.1 # via -r requirements/edx/testing.txt -lxml==4.5.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, capa, edxval, lti-consumer-xblock, ora2, pyquery, safe-lxml, xblock, xmlsec +lxml==4.5.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, capa, edxval, lti-consumer-xblock, olxcleaner, ora2, pyquery, safe-lxml, xblock, xmlsec m2r==0.2.1 # via sphinxcontrib-openapi mailsnake==1.6.4 # via -r requirements/edx/testing.txt mako==1.1.4 # via -r requirements/edx/testing.txt, acid-xblock, lti-consumer-xblock, xblock-google-drive, xblock-utils @@ -197,6 +197,7 @@ nltk==3.5 # via -r requirements/edx/testing.txt, chem nodeenv==1.5.0 # via -r requirements/edx/testing.txt numpy==1.20.2 # via -r requirements/edx/testing.txt, chem, openedx-calc, scipy oauthlib==3.0.1 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, django-oauth-toolkit, lti-consumer-xblock, requests-oauthlib, social-auth-core +olxcleaner==0.1.4 # via -r requirements/edx/testing.txt openedx-calc==2.0.1 # via -r requirements/edx/testing.txt ora2==3.4.0 # via -r requirements/edx/testing.txt packaging==20.9 # via -r requirements/edx/testing.txt, bleach, drf-yasg, pytest, sphinx, tox @@ -219,6 +220,7 @@ pycryptodomex==3.10.1 # via -r requirements/edx/testing.txt, edx-proctoring, pygments==2.8.1 # via -r requirements/edx/testing.txt, diff-cover, sphinx pyjwkest==1.4.2 # via -r requirements/edx/testing.txt, edx-drf-extensions, lti-consumer-xblock pyjwt[crypto]==1.7.1 # via -r requirements/edx/testing.txt, drf-jwt, edx-rest-api-client, social-auth-core +pylatexenc==2.9 # via -r requirements/edx/testing.txt, olxcleaner pylint-celery==0.3 # via -r requirements/edx/testing.txt, edx-lint pylint-django==2.4.2 # via -r requirements/edx/testing.txt, edx-lint pylint-plugin-utils==0.6 # via -r requirements/edx/testing.txt, pylint-celery, pylint-django @@ -238,14 +240,14 @@ pytest-metadata==1.8.0 # via -r requirements/edx/testing.txt, pytest-json-rep pytest-randomly==3.5.0 # via -r requirements/edx/testing.txt pytest-xdist[psutil]==2.2.1 # via -r requirements/edx/testing.txt pytest==6.2.2 # via -r requirements/edx/testing.txt, pytest-attrib, pytest-cov, pytest-django, pytest-forked, pytest-json-report, pytest-metadata, pytest-randomly, pytest-xdist -python-dateutil==2.4.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, analytics-python, botocore, edx-ace, edx-drf-extensions, edx-enterprise, edx-event-routing-backends, edx-proctoring, faker, freezegun, icalendar, ora2, xblock +python-dateutil==2.4.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, analytics-python, botocore, edx-ace, edx-drf-extensions, edx-enterprise, edx-event-routing-backends, edx-proctoring, faker, freezegun, icalendar, olxcleaner, ora2, xblock python-levenshtein==0.12.2 # via -r requirements/edx/testing.txt python-memcached==1.59 # via -r requirements/edx/testing.txt python-slugify==4.0.1 # via -r requirements/edx/testing.txt, code-annotations, transifex-client python-swiftclient==3.11.1 # via -r requirements/edx/testing.txt, ora2 python3-openid==3.2.0 ; python_version >= "3" # via -r requirements/edx/testing.txt, social-auth-core python3-saml==1.9.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt -pytz==2021.1 # via -r requirements/edx/testing.txt, babel, capa, celery, django, django-ses, edx-completion, edx-enterprise, edx-event-routing-backends, edx-proctoring, edx-submissions, edx-tincan-py35, event-tracking, fs, icalendar, ora2, tincan, xblock +pytz==2021.1 # via -r requirements/edx/testing.txt, babel, capa, celery, django, django-ses, edx-completion, edx-enterprise, edx-event-routing-backends, edx-proctoring, edx-submissions, edx-tincan-py35, event-tracking, fs, icalendar, olxcleaner, ora2, tincan, xblock pyuca==1.2 # via -r requirements/edx/testing.txt pywatchman==1.4.1 # via -r requirements/edx/development.in pyyaml==5.4.1 # via -r requirements/edx/testing.txt, code-annotations, edx-django-release-util, edx-i18n-tools, sphinxcontrib-openapi, xblock diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index f42af422f8..bbea0c5967 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -170,7 +170,7 @@ lazy==1.4 # via -r requirements/edx/base.txt, acid-xblock, bok-c libsass==0.10.0 # via -r requirements/edx/base.txt, ora2 loremipsum==1.0.5 # via -r requirements/edx/base.txt, ora2 lti-consumer-xblock==2.7.1 # via -r requirements/edx/base.txt -lxml==4.5.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, capa, edxval, lti-consumer-xblock, ora2, pyquery, safe-lxml, xblock, xmlsec +lxml==4.5.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, capa, edxval, lti-consumer-xblock, olxcleaner, ora2, pyquery, safe-lxml, xblock, xmlsec mailsnake==1.6.4 # via -r requirements/edx/base.txt mako==1.1.4 # via -r requirements/edx/base.txt, acid-xblock, lti-consumer-xblock, xblock-google-drive, xblock-utils markdown==3.3.4 # via -r requirements/edx/base.txt, django-wiki, staff-graded-xblock, xblock-poll @@ -189,6 +189,7 @@ nltk==3.5 # via -r requirements/edx/base.txt, chem nodeenv==1.5.0 # via -r requirements/edx/base.txt numpy==1.20.2 # via -r requirements/edx/base.txt, chem, openedx-calc, scipy oauthlib==3.0.1 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, django-oauth-toolkit, lti-consumer-xblock, requests-oauthlib, social-auth-core +olxcleaner==0.1.4 # via -r requirements/edx/base.txt openedx-calc==2.0.1 # via -r requirements/edx/base.txt ora2==3.4.0 # via -r requirements/edx/base.txt packaging==20.9 # via -r requirements/edx/base.txt, bleach, drf-yasg, pytest, tox @@ -210,6 +211,7 @@ pycryptodomex==3.10.1 # via -r requirements/edx/base.txt, edx-proctoring, lt pygments==2.8.1 # via -r requirements/edx/base.txt, -r requirements/edx/coverage.txt, diff-cover pyjwkest==1.4.2 # via -r requirements/edx/base.txt, edx-drf-extensions, lti-consumer-xblock pyjwt[crypto]==1.7.1 # via -r requirements/edx/base.txt, drf-jwt, edx-rest-api-client, social-auth-core +pylatexenc==2.9 # via -r requirements/edx/base.txt, olxcleaner pylint-celery==0.3 # via edx-lint pylint-django==2.4.2 # via edx-lint pylint-plugin-utils==0.6 # via pylint-celery, pylint-django @@ -228,14 +230,14 @@ pytest-metadata==1.8.0 # via -r requirements/edx/testing.in, pytest-json-repo pytest-randomly==3.5.0 # via -r requirements/edx/testing.in pytest-xdist[psutil]==2.2.1 # via -r requirements/edx/testing.in pytest==6.2.2 # via -r requirements/edx/testing.in, pytest-attrib, pytest-cov, pytest-django, pytest-forked, pytest-json-report, pytest-metadata, pytest-randomly, pytest-xdist -python-dateutil==2.4.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, analytics-python, botocore, edx-ace, edx-drf-extensions, edx-enterprise, edx-event-routing-backends, edx-proctoring, faker, freezegun, icalendar, ora2, xblock +python-dateutil==2.4.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, analytics-python, botocore, edx-ace, edx-drf-extensions, edx-enterprise, edx-event-routing-backends, edx-proctoring, faker, freezegun, icalendar, olxcleaner, ora2, xblock python-levenshtein==0.12.2 # via -r requirements/edx/base.txt python-memcached==1.59 # via -r requirements/edx/base.txt python-slugify==4.0.1 # via -r requirements/edx/base.txt, code-annotations, transifex-client python-swiftclient==3.11.1 # via -r requirements/edx/base.txt, ora2 python3-openid==3.2.0 ; python_version >= "3" # via -r requirements/edx/base.txt, social-auth-core python3-saml==1.9.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt -pytz==2021.1 # via -r requirements/edx/base.txt, babel, capa, celery, django, django-ses, edx-completion, edx-enterprise, edx-event-routing-backends, edx-proctoring, edx-submissions, edx-tincan-py35, event-tracking, fs, icalendar, ora2, tincan, xblock +pytz==2021.1 # via -r requirements/edx/base.txt, babel, capa, celery, django, django-ses, edx-completion, edx-enterprise, edx-event-routing-backends, edx-proctoring, edx-submissions, edx-tincan-py35, event-tracking, fs, icalendar, olxcleaner, ora2, tincan, xblock pyuca==1.2 # via -r requirements/edx/base.txt pyyaml==5.4.1 # via -r requirements/edx/base.txt, code-annotations, edx-django-release-util, edx-i18n-tools, xblock random2==1.0.1 # via -r requirements/edx/base.txt