Integrate "olxcleaner" with course import (#27164)

* Integrating olxcleaner in course import

* Adding toggle removal date and addressing pylint issues.
This commit is contained in:
Awais Jibran
2021-03-31 13:26:14 +05:00
committed by GitHub
parent f3050a1dbd
commit ad7f6019fe
11 changed files with 148 additions and 46 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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