Skip to content
Snippets Groups Projects
Unverified Commit 11fc1184 authored by Tim McCormack's avatar Tim McCormack Committed by GitHub
Browse files

Remove django-celery from transitive deps; remove unused tasks (#23318)


The video thumbnail and transcript tasks were the only things using chord_task from
edx-celeryutils, which in turn was blocking django-celery removal. But, they're no longer used.

Co-authored-by: default avatarDiana Huang <diana.k.huang@gmail.com>
parent 919dc576
Branches
Tags
No related merge requests found
"""
Command to migrate transcripts to django storage.
"""
import logging
from django.core.management import BaseCommand, CommandError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator
from six.moves import map
from cms.djangoapps.contentstore.tasks import (
DEFAULT_ALL_COURSES,
DEFAULT_COMMIT,
DEFAULT_FORCE_UPDATE,
enqueue_async_migrate_transcripts_tasks
)
from openedx.core.djangoapps.video_config.models import MigrationEnqueuedCourse, TranscriptMigrationSetting
from openedx.core.lib.command_utils import get_mutually_exclusive_required_option, parse_course_keys
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Example usage:
$ ./manage.py cms migrate_transcripts --all-courses --force-update --commit
$ ./manage.py cms migrate_transcripts --course-id 'Course1' --course-id 'Course2' --commit
$ ./manage.py cms migrate_transcripts --from-settings
"""
help = 'Migrates transcripts to S3 for one or more courses.'
def add_arguments(self, parser):
"""
Add arguments to the command parser.
"""
parser.add_argument(
'--course-id', '--course_id',
dest='course_ids',
action='append',
help=u'Migrates transcripts for the list of courses.'
)
parser.add_argument(
'--all-courses', '--all', '--all_courses',
dest='all_courses',
action='store_true',
default=DEFAULT_ALL_COURSES,
help=u'Migrates transcripts to the configured django storage for all courses.'
)
parser.add_argument(
'--from-settings', '--from_settings',
dest='from_settings',
help='Migrate Transcripts with settings set via django admin',
action='store_true',
default=False,
)
parser.add_argument(
'--force-update', '--force_update',
dest='force_update',
action='store_true',
default=DEFAULT_FORCE_UPDATE,
help=u'Force migrate transcripts for the requested courses, overwrite if already present.'
)
parser.add_argument(
'--commit',
dest='commit',
action='store_true',
default=DEFAULT_COMMIT,
help=u'Commits the discovered video transcripts to django storage. '
u'Without this flag, the command will return the transcripts discovered for migration.'
)
def _parse_course_key(self, raw_value):
""" Parses course key from string """
try:
result = CourseKey.from_string(raw_value)
except InvalidKeyError:
raise CommandError(u"Invalid course_key: '%s'." % raw_value)
if not isinstance(result, CourseLocator):
raise CommandError(u"Argument {0} is not a course key".format(raw_value))
return result
def _get_migration_options(self, options):
"""
Returns the command arguments configured via django admin.
"""
force_update = options['force_update']
commit = options['commit']
courses_mode = get_mutually_exclusive_required_option(options, 'course_ids', 'all_courses', 'from_settings')
if courses_mode == 'all_courses':
course_keys = [course.id for course in modulestore().get_course_summaries()]
elif courses_mode == 'course_ids':
course_keys = list(map(self._parse_course_key, options['course_ids']))
else:
migration_settings = self._latest_settings()
if migration_settings.all_courses:
all_courses = [course.id for course in modulestore().get_course_summaries()]
# Following is to avoid re-rerunning migrations for the already enqueued courses.
# Although the migrations job is idempotent, but we need to track if the transcript migration
# job was initiated for specific course(s) in order to elevate load from the workers and for
# the job to be able identify the past enqueued courses.
migrated_courses = MigrationEnqueuedCourse.objects.all().values_list('course_id', flat=True)
non_migrated_courses = [
course_key
for course_key in all_courses
if course_key not in migrated_courses
]
# Course batch to be migrated.
course_keys = non_migrated_courses[:migration_settings.batch_size]
log.info(
(u'[Transcript Migration] Courses(total): %s, '
u'Courses(migrated): %s, Courses(non-migrated): %s, '
u'Courses(migration-in-process): %s'),
len(all_courses),
len(migrated_courses),
len(non_migrated_courses),
len(course_keys),
)
else:
course_keys = parse_course_keys(migration_settings.course_ids.split())
force_update = migration_settings.force_update
commit = migration_settings.commit
return course_keys, force_update, commit
def _latest_settings(self):
"""
Return the latest version of the TranscriptMigrationSetting
"""
return TranscriptMigrationSetting.current()
def handle(self, *args, **options):
"""
Invokes the migrate transcripts enqueue function.
"""
migration_settings = self._latest_settings()
course_keys, force_update, commit = self._get_migration_options(options)
command_run = migration_settings.increment_run() if commit else -1
enqueue_async_migrate_transcripts_tasks(
course_keys=course_keys, commit=commit, command_run=command_run, force_update=force_update
)
if commit and options.get('from_settings') and migration_settings.all_courses:
for course_key in course_keys:
enqueued_course, created = MigrationEnqueuedCourse.objects.get_or_create(course_id=course_key)
if created:
enqueued_course.command_run = command_run
enqueued_course.save()
# -*- coding: utf-8 -*-
"""
Tests for course transcript migration management command.
"""
import itertools
import logging
from datetime import datetime
import ddt
import pytz
import six
from django.core.management import CommandError, call_command
from django.test import TestCase
from edxval import api as api
from mock import patch
from testfixtures import LogCapture
from openedx.core.djangoapps.video_config.models import MigrationEnqueuedCourse, TranscriptMigrationSetting
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.video_module import VideoBlock
from xmodule.video_module.transcripts_utils import save_to_store
LOGGER_NAME = "cms.djangoapps.contentstore.tasks"
SRT_FILEDATA = '''
0
00:00:00,270 --> 00:00:02,720
sprechen sie deutsch?
1
00:00:02,720 --> 00:00:05,430
Ja, ich spreche Deutsch
2
00:00:6,500 --> 00:00:08,600
可以用“我不太懂艺术 但我知道我喜欢什么”做比喻
'''
CRO_SRT_FILEDATA = '''
0
00:00:00,270 --> 00:00:02,720
Dobar dan!
1
00:00:02,720 --> 00:00:05,430
Kako ste danas?
2
00:00:6,500 --> 00:00:08,600
可以用“我不太懂艺术 但我知道我喜欢什么”做比喻
'''
VIDEO_DICT_STAR = dict(
client_video_id='TWINKLE TWINKLE',
duration=42.0,
edx_video_id='test_edx_video_id',
status='upload',
)
class TestArgParsing(TestCase):
"""
Tests for parsing arguments for the `migrate_transcripts` management command
"""
def test_no_args(self):
errstring = "Must specify exactly one of --course_ids, --all_courses, --from_settings"
with self.assertRaisesRegex(CommandError, errstring):
call_command('migrate_transcripts')
def test_invalid_course(self):
errstring = "Invalid course_key: 'invalid-course'."
with self.assertRaisesRegex(CommandError, errstring):
call_command('migrate_transcripts', '--course-id', 'invalid-course')
@ddt.ddt
class TestMigrateTranscripts(ModuleStoreTestCase):
"""
Tests migrating video transcripts in courses from contentstore to django storage
"""
def setUp(self):
""" Common setup. """
super(TestMigrateTranscripts, self).setUp()
self.store = modulestore()
self.course = CourseFactory.create()
self.course_2 = CourseFactory.create()
video = {
'edx_video_id': 'test_edx_video_id',
'client_video_id': 'test1.mp4',
'duration': 42.0,
'status': 'upload',
'courses': [six.text_type(self.course.id)],
'encoded_videos': [],
'created': datetime.now(pytz.utc)
}
api.create_video(video)
video_sample_xml = '''
<video display_name="Test Video"
edx_video_id="test_edx_video_id"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
download_track="false"
start_time="1.0"
download_video="false"
end_time="60.0">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
<handout src="http://www.example.com/handout"/>
<transcript language="ge" src="subs_grmtran1.srt" />
<transcript language="hr" src="subs_croatian1.srt" />
</video>
'''
video_sample_xml_2 = '''
<video display_name="Test Video 2"
edx_video_id="test_edx_video_id_2"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
download_track="false"
start_time="1.0"
download_video="false"
end_time="60.0">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
<handout src="http://www.example.com/handout"/>
<transcript language="ge" src="not_found.srt" />
</video>
'''
self.video_descriptor = ItemFactory.create(
parent_location=self.course.location, category='video',
**VideoBlock.parse_video_xml(video_sample_xml)
)
self.video_descriptor_2 = ItemFactory.create(
parent_location=self.course_2.location, category='video',
**VideoBlock.parse_video_xml(video_sample_xml_2)
)
save_to_store(SRT_FILEDATA, 'subs_grmtran1.srt', 'text/srt', self.video_descriptor.location)
save_to_store(CRO_SRT_FILEDATA, 'subs_croatian1.srt', 'text/srt', self.video_descriptor.location)
def test_migrated_transcripts_count_with_commit(self):
"""
Test migrating transcripts with commit
"""
# check that transcript does not exist
languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id)
self.assertEqual(len(languages), 0)
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
# now call migrate_transcripts command and check the transcript availability
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id), '--commit')
languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id)
self.assertEqual(len(languages), 2)
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
def test_migrated_transcripts_without_commit(self):
"""
Test migrating transcripts as a dry-run
"""
# check that transcripts do not exist
languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id)
self.assertEqual(len(languages), 0)
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
# now call migrate_transcripts command and check the transcript availability
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id))
# check that transcripts still do not exist
languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id)
self.assertEqual(len(languages), 0)
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
def test_migrate_transcripts_availability(self):
"""
Test migrating transcripts
"""
translations = self.video_descriptor.available_translations(self.video_descriptor.get_transcripts_info())
six.assertCountEqual(self, translations, ['hr', 'ge'])
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
# now call migrate_transcripts command and check the transcript availability
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id), '--commit')
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
def test_migrate_transcripts_idempotency(self):
"""
Test migrating transcripts multiple times
"""
translations = self.video_descriptor.available_translations(self.video_descriptor.get_transcripts_info())
six.assertCountEqual(self, translations, ['hr', 'ge'])
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
# now call migrate_transcripts command and check the transcript availability
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id), '--commit')
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
# now call migrate_transcripts command again and check the transcript availability
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id), '--commit')
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
# now call migrate_transcripts command with --force-update and check the transcript availability
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id), '--force-update', '--commit')
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr'))
self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge'))
def test_migrate_transcripts_logging(self):
"""
Test migrate transcripts logging and output
"""
course_id = six.text_type(self.course.id)
expected_log = (
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=-1] [video-transcripts-migration-process-started-for-course] '
u'[course={}]'.format(course_id))
),
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=-1] [video-transcript-will-be-migrated] '
u'[revision=rev-opt-published-only] [video={}] [edx_video_id=test_edx_video_id] '
u'[language_code=hr]'.format(self.video_descriptor.location))
),
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=-1] [video-transcript-will-be-migrated] '
u'[revision=rev-opt-published-only] [video={}] [edx_video_id=test_edx_video_id] '
u'[language_code=ge]'.format(self.video_descriptor.location))
),
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=-1] [transcripts-migration-tasks-submitted] '
u'[transcripts_count=2] [course={}] '
u'[revision=rev-opt-published-only] [video={}]'.format(course_id, self.video_descriptor.location))
)
)
with LogCapture(LOGGER_NAME, level=logging.INFO) as logger:
call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id))
logger.check(
*expected_log
)
def test_migrate_transcripts_exception_logging(self):
"""
Test migrate transcripts exception logging
"""
course_id = six.text_type(self.course_2.id)
expected_log = (
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=1] [video-transcripts-migration-process-started-for-course] '
u'[course={}]'.format(course_id))
),
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=1] [transcripts-migration-process-started-for-video-transcript] '
u'[revision=rev-opt-published-only] [video={}] [edx_video_id=test_edx_video_id_2] '
u'[language_code=ge]'.format(self.video_descriptor_2.location))
),
(
'cms.djangoapps.contentstore.tasks', 'ERROR',
(u'[Transcript Migration] [run=1] [video-transcript-migration-failed-with-known-exc] '
u'[revision=rev-opt-published-only] [video={}] [edx_video_id=test_edx_video_id_2] '
u'[language_code=ge]'.format(self.video_descriptor_2.location))
),
(
'cms.djangoapps.contentstore.tasks', 'INFO',
(u'[Transcript Migration] [run=1] [transcripts-migration-tasks-submitted] '
u'[transcripts_count=1] [course={}] '
u'[revision=rev-opt-published-only] [video={}]'.format(course_id, self.video_descriptor_2.location))
)
)
with LogCapture(LOGGER_NAME, level=logging.INFO) as logger:
call_command('migrate_transcripts', '--course-id', six.text_type(self.course_2.id), '--commit')
logger.check(
*expected_log
)
@ddt.data(*itertools.product([1, 2], [True, False], [True, False]))
@ddt.unpack
@patch('contentstore.management.commands.migrate_transcripts.log')
def test_migrate_transcripts_batch_size(self, batch_size, commit, all_courses, mock_logger):
"""
Test that migrations across course batches, is working as expected.
"""
migration_settings = TranscriptMigrationSetting.objects.create(
batch_size=batch_size, commit=commit, all_courses=all_courses
)
# Assert the number of job runs and migration enqueued courses.
self.assertEqual(migration_settings.command_run, 0)
self.assertEqual(MigrationEnqueuedCourse.objects.count(), 0)
call_command('migrate_transcripts', '--from-settings')
migration_settings = TranscriptMigrationSetting.current()
# Command run is only incremented if commit=True.
expected_command_run = 1 if commit else 0
self.assertEqual(migration_settings.command_run, expected_command_run)
if all_courses:
mock_logger.info.assert_called_with(
(u'[Transcript Migration] Courses(total): %s, Courses(migrated): %s, '
u'Courses(non-migrated): %s, Courses(migration-in-process): %s'),
2, 0, 2, batch_size
)
# enqueued courses are only persisted if commit=True and job is running for all courses.
enqueued_courses = batch_size if commit and all_courses else 0
self.assertEqual(MigrationEnqueuedCourse.objects.count(), enqueued_courses)
# -*- coding: utf-8 -*-
"""
Tests for course video thumbnails management command.
"""
import logging
from django.core.management import CommandError, call_command
from django.test import TestCase
from mock import patch
from six import text_type
from testfixtures import LogCapture
from openedx.core.djangoapps.video_config.models import VideoThumbnailSetting
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
LOGGER_NAME = "contentstore.management.commands.video_thumbnails"
def setup_video_thumbnails_config(batch_size=10, commit=False, all_course_videos=False, course_ids=''):
VideoThumbnailSetting.objects.create(
batch_size=batch_size,
commit=commit,
course_ids=course_ids,
all_course_videos=all_course_videos,
videos_per_task=2
)
class TestArgParsing(TestCase):
"""
Tests for parsing arguments for the `video_thumbnails` management command
"""
def test_invalid_course(self):
errstring = "Invalid key specified: <class 'opaque_keys.edx.locator.CourseLocator'>: invalid-course"
setup_video_thumbnails_config(course_ids='invalid-course')
with self.assertRaisesRegex(CommandError, errstring):
call_command('video_thumbnails')
class TestVideoThumbnails(ModuleStoreTestCase):
"""
Tests adding thumbnails to course videos from YouTube
"""
def setUp(self):
""" Common setup """
super(TestVideoThumbnails, self).setUp()
self.course = CourseFactory.create()
self.course_2 = CourseFactory.create()
@patch('edxval.api.get_course_video_ids_with_youtube_profile')
@patch('contentstore.management.commands.video_thumbnails.enqueue_update_thumbnail_tasks')
def test_video_thumbnails_without_commit(self, mock_enqueue_thumbnails, mock_course_videos):
"""
Test that when command is run without commit, correct information is logged.
"""
course_videos = [
(self.course.id, 'super-soaker', 'https://www.youtube.com/watch?v=OscRe3pSP80'),
(self.course_2.id, 'medium-soaker', 'https://www.youtube.com/watch?v=OscRe3pSP81')
]
mock_course_videos.return_value = course_videos
setup_video_thumbnails_config(all_course_videos=True)
with LogCapture(LOGGER_NAME, level=logging.INFO) as logger:
call_command('video_thumbnails')
# Verify that list of course video ids is logged.
logger.check(
(
LOGGER_NAME, 'INFO',
'[Video Thumbnails] Videos(updated): 0, Videos(update-in-process): 2'
),
(
LOGGER_NAME, 'INFO',
u'[video thumbnails] selected course videos: {course_videos} '.format(
course_videos=text_type(course_videos)
)
)
)
# Verify that `enqueue_update_thumbnail_tasks` is not called.
self.assertFalse(mock_enqueue_thumbnails.called)
@patch('edxval.api.get_course_video_ids_with_youtube_profile')
@patch('contentstore.management.commands.video_thumbnails.enqueue_update_thumbnail_tasks')
def test_video_thumbnails_with_commit(self, mock_enqueue_thumbnails, mock_course_videos):
"""
Test that when command is run with with commit, it works as expected.
"""
course_videos = [
(self.course.id, 'super-soaker', 'https://www.youtube.com/watch?v=OscRe3pSP80'),
(self.course_2.id, 'medium-soaker', 'https://www.youtube.com/watch?v=OscRe3pSP81')
]
mock_course_videos.return_value = course_videos
setup_video_thumbnails_config(commit=True, all_course_videos=True)
with LogCapture(LOGGER_NAME, level=logging.INFO) as logger:
call_command('video_thumbnails')
# Verify that command information correctly logged.
logger.check((
LOGGER_NAME, 'INFO',
'[Video Thumbnails] Videos(updated): 0, Videos(update-in-process): 2'
))
# Verify that `enqueue_update_thumbnail_tasks` is called.
self.assertTrue(mock_enqueue_thumbnails.called)
@patch('edxval.api.get_course_video_ids_with_youtube_profile')
@patch('contentstore.video_utils.download_youtube_video_thumbnail') # Mock(side_effect=Exception())
def test_video_thumbnails_scraping_failed(self, mock_scrape_thumbnails, mock_course_videos):
"""
Test that when scraping fails, it is handled correclty.
"""
course_videos = [
(self.course.id, 'super-soaker', 'OscRe3pSP80'),
(self.course_2.id, 'medium-soaker', 'OscRe3pSP81')
]
mock_scrape_thumbnails.side_effect = Exception('error')
mock_course_videos.return_value = course_videos
setup_video_thumbnails_config(commit=True, all_course_videos=True)
tasks_logger = "cms.djangoapps.contentstore.tasks"
with LogCapture(tasks_logger, level=logging.INFO) as logger:
call_command('video_thumbnails')
# Verify that tasks information is correctly logged.
logger.check(
(
tasks_logger, 'ERROR',
(u"[video thumbnails] [run=1] [video-thumbnails-scraping-failed-with-unknown-exc] "
u"[edx_video_id=super-soaker] [youtube_id=OscRe3pSP80] [course={}]".format(self.course.id))
),
(
tasks_logger, 'ERROR',
(u"[video thumbnails] [run=1] [video-thumbnails-scraping-failed-with-unknown-exc] "
u"[edx_video_id=medium-soaker] [youtube_id=OscRe3pSP81] [course={}]".format(self.course_2.id))
)
)
"""
Command to scrape thumbnails and add them to the course-videos.
"""
import logging
import edxval.api as edxval_api
from django.core.management import BaseCommand
from django.core.management.base import CommandError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from six import text_type
from cms.djangoapps.contentstore.tasks import enqueue_update_thumbnail_tasks
from openedx.core.djangoapps.video_config.models import VideoThumbnailSetting
log = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Example usage:
$ ./manage.py cms video_thumbnails
"""
help = 'Adds thumbnails from YouTube to videos'
def _get_command_options(self):
"""
Returns the command arguments configured via django admin.
"""
command_settings = self._latest_settings()
commit = command_settings.commit
if command_settings.all_course_videos:
course_videos = edxval_api.get_course_video_ids_with_youtube_profile(
offset=command_settings.offset, limit=command_settings.batch_size
)
log.info(
u'[Video Thumbnails] Videos(updated): %s, Videos(update-in-process): %s',
command_settings.offset, len(course_videos),
)
else:
validated_course_ids = self._validate_course_ids(command_settings.course_ids.split())
course_videos = edxval_api.get_course_video_ids_with_youtube_profile(validated_course_ids)
return course_videos, commit
def _validate_course_ids(self, course_ids):
"""
Validate a list of course key strings.
"""
try:
for course_id in course_ids:
CourseKey.from_string(course_id)
return course_ids
except InvalidKeyError as error:
raise CommandError(u'Invalid key specified: {}'.format(text_type(error)))
def _latest_settings(self):
"""
Return the latest version of the VideoThumbnailSetting
"""
return VideoThumbnailSetting.current()
def handle(self, *args, **options):
"""
Invokes the video thumbnail enqueue function.
"""
video_thumbnail_settings = self._latest_settings()
videos_per_task = video_thumbnail_settings.videos_per_task
course_videos, commit = self._get_command_options()
if commit:
command_run = video_thumbnail_settings.increment_run()
enqueue_update_thumbnail_tasks(
course_videos=course_videos,
videos_per_task=videos_per_task,
run=command_run
)
if video_thumbnail_settings.all_course_videos:
video_thumbnail_settings.update_offset()
else:
log.info(u'[video thumbnails] selected course videos: {course_videos} '.format(
course_videos=text_type(course_videos)
))
......@@ -15,7 +15,6 @@ from tempfile import NamedTemporaryFile, mkdtemp
from celery import group
from celery.task import task
from celery.utils.log import get_task_logger
from celery_utils.chordable_django_backend import chord, chord_task
from celery_utils.persist_on_failure import LoggedPersistOnFailureTask
from django.conf import settings
from django.contrib.auth import get_user_model
......@@ -76,347 +75,6 @@ User = get_user_model()
LOGGER = get_task_logger(__name__)
FILE_READ_CHUNK = 1024 # bytes
FULL_COURSE_REINDEX_THRESHOLD = 1
DEFAULT_ALL_COURSES = False
DEFAULT_FORCE_UPDATE = False
DEFAULT_COMMIT = False
MIGRATION_LOGS_PREFIX = 'Transcript Migration'
RETRY_DELAY_SECONDS = 30
COURSE_LEVEL_TIMEOUT_SECONDS = 1200
VIDEO_LEVEL_TIMEOUT_SECONDS = 300
def enqueue_update_thumbnail_tasks(course_videos, videos_per_task, run):
"""
Enqueue tasks to update video thumbnails from youtube.
Arguments:
course_videos: A list of tuples, each containing course ID, video ID and youtube ID.
videos_per_task: Number of course videos that can be processed by a single celery task.
run: This tracks the YT thumbnail scraping job runs.
"""
tasks = []
batch_size = len(course_videos)
# Further slice the course-videos batch into chunks on the
# basis of number of course-videos per task.
start = 0
end = videos_per_task
chunks_count = int(ceil(batch_size / float(videos_per_task)))
for __ in range(0, chunks_count): # pylint: disable=C7620
course_videos_chunk = course_videos[start:end]
tasks.append(task_scrape_youtube_thumbnail.s(
course_videos_chunk, run
))
start = end
end += videos_per_task
# Kick off a chord of scraping tasks
callback = task_scrape_youtube_thumbnail_callback.s(
run=run,
batch_size=batch_size,
videos_per_task=videos_per_task,
)
chord(tasks)(callback)
@chord_task(bind=True, routing_key=settings.VIDEO_TRANSCRIPT_MIGRATIONS_JOB_QUEUE)
def task_scrape_youtube_thumbnail_callback(self, results, run, # pylint: disable=unused-argument
batch_size, videos_per_task):
"""
Callback for collating the results of yt thumbnails scraping tasks chord.
"""
yt_thumbnails_scraping_tasks_count = len(list(results()))
LOGGER.info(
(u"[video thumbnails] [run=%s] [video-thumbnails-scraping-complete-for-a-batch] [tasks_count=%s] "
u"[batch_size=%s] [videos_per_task=%s]"),
run, yt_thumbnails_scraping_tasks_count, batch_size, videos_per_task
)
@chord_task(
bind=True,
base=LoggedPersistOnFailureTask,
default_retry_delay=RETRY_DELAY_SECONDS,
max_retries=1,
time_limit=COURSE_LEVEL_TIMEOUT_SECONDS,
routing_key=settings.SCRAPE_YOUTUBE_THUMBNAILS_JOB_QUEUE
)
def task_scrape_youtube_thumbnail(self, course_videos, run): # pylint: disable=unused-argument
"""
Task to scrape youtube thumbnails and update them in edxval for the given course-videos.
Arguments:
course_videos: A list of tuples, each containing course ID, video ID and youtube ID.
run: This tracks the YT thumbnail scraping job runs.
"""
for course_id, edx_video_id, youtube_id in course_videos:
try:
scrape_youtube_thumbnail(course_id, edx_video_id, youtube_id)
except Exception: # pylint: disable=broad-except
LOGGER.exception(
(u"[video thumbnails] [run=%s] [video-thumbnails-scraping-failed-with-unknown-exc] "
u"[edx_video_id=%s] [youtube_id=%s] [course=%s]"),
run,
edx_video_id,
youtube_id,
course_id
)
continue
@chord_task(bind=True, routing_key=settings.VIDEO_TRANSCRIPT_MIGRATIONS_JOB_QUEUE)
def task_status_callback(self, results, revision, # pylint: disable=unused-argument
course_id, command_run, video_location):
"""
Callback for collating the results of chord.
"""
transcript_tasks_count = len(list(results()))
LOGGER.info(
(u"[%s] [run=%s] [video-transcripts-migration-complete-for-a-video] [tasks_count=%s] [course_id=%s] "
u"[revision=%s] [video=%s]"),
MIGRATION_LOGS_PREFIX, command_run, transcript_tasks_count, course_id, revision, video_location
)
def enqueue_async_migrate_transcripts_tasks(course_keys,
command_run,
force_update=DEFAULT_FORCE_UPDATE,
commit=DEFAULT_COMMIT):
"""
Fires new Celery tasks for all the input courses or for all courses.
Arguments:
course_keys: Command line course ids as list of CourseKey objects
command_run: A positive number indicating the run counts for transcripts migrations
force_update: Overwrite file in S3. Default is False
commit: Update S3 or dry-run the command to see which transcripts will be affected. Default is False
"""
kwargs = {
'force_update': force_update,
'commit': commit,
'command_run': command_run
}
group([
async_migrate_transcript.s(text_type(course_key), **kwargs)
for course_key in course_keys
])()
def get_course_videos(course_key):
"""
Returns all videos in a course as list.
Arguments:
course_key: CourseKey object
"""
all_videos = {}
store = modulestore()
# include published videos of the course.
all_videos[ModuleStoreEnum.RevisionOption.published_only] = store.get_items(
course_key,
qualifiers={'category': 'video'},
revision=ModuleStoreEnum.RevisionOption.published_only,
include_orphans=False
)
# include draft videos of the course.
all_videos[ModuleStoreEnum.RevisionOption.draft_only] = store.get_items(
course_key,
qualifiers={'category': 'video'},
revision=ModuleStoreEnum.RevisionOption.draft_only,
include_orphans=False
)
return all_videos
@chord_task(
bind=True,
base=LoggedPersistOnFailureTask,
default_retry_delay=RETRY_DELAY_SECONDS,
max_retries=1,
time_limit=COURSE_LEVEL_TIMEOUT_SECONDS,
routing_key=settings.VIDEO_TRANSCRIPT_MIGRATIONS_JOB_QUEUE
)
def async_migrate_transcript(self, course_key, **kwargs): # pylint: disable=unused-argument
"""
Migrates the transcripts of all videos in a course as a new celery task.
"""
force_update = kwargs['force_update']
command_run = kwargs['command_run']
course_videos = get_course_videos(CourseKey.from_string(course_key))
LOGGER.info(
u"[%s] [run=%s] [video-transcripts-migration-process-started-for-course] [course=%s]",
MIGRATION_LOGS_PREFIX, command_run, course_key
)
for revision, videos in course_videos.items():
for video in videos:
# Gather transcripts from a video block.
all_transcripts = {}
if video.transcripts is not None:
all_transcripts.update(video.transcripts)
english_transcript = video.sub
if english_transcript:
all_transcripts.update({'en': video.sub})
sub_tasks = []
video_location = text_type(video.location)
for lang in all_transcripts:
sub_tasks.append(async_migrate_transcript_subtask.s(
video_location, revision, lang, force_update, **kwargs
))
if sub_tasks:
callback = task_status_callback.s(
revision=revision,
course_id=course_key,
command_run=command_run,
video_location=video_location
)
chord(sub_tasks)(callback)
LOGGER.info(
(u"[%s] [run=%s] [transcripts-migration-tasks-submitted] "
u"[transcripts_count=%s] [course=%s] [revision=%s] [video=%s]"),
MIGRATION_LOGS_PREFIX, command_run, len(sub_tasks), course_key, revision, video_location
)
else:
LOGGER.info(
u"[%s] [run=%s] [no-video-transcripts] [course=%s] [revision=%s] [video=%s]",
MIGRATION_LOGS_PREFIX, command_run, course_key, revision, video_location
)
def save_transcript_to_storage(command_run, edx_video_id, language_code, transcript_content, file_format, force_update):
"""
Pushes a given transcript's data to django storage.
Arguments:
command_run: A positive integer indicating the current run
edx_video_id: video ID
language_code: language code
transcript_content: content of the transcript
file_format: format of the transcript file
force_update: tells whether it needs to perform force update in
case of an existing transcript for the given video.
"""
transcript_present = is_transcript_available(video_id=edx_video_id, language_code=language_code)
if transcript_present and force_update:
create_or_update_video_transcript(
edx_video_id,
language_code,
dict({'file_format': file_format}),
ContentFile(transcript_content)
)
elif not transcript_present:
create_video_transcript(
edx_video_id,
language_code,
file_format,
ContentFile(transcript_content)
)
else:
LOGGER.info(
u"[%s] [run=%s] [do-not-override-existing-transcript] [edx_video_id=%s] [language_code=%s]",
MIGRATION_LOGS_PREFIX, command_run, edx_video_id, language_code
)
@chord_task(
bind=True,
base=LoggedPersistOnFailureTask,
default_retry_delay=RETRY_DELAY_SECONDS,
max_retries=2,
time_limit=VIDEO_LEVEL_TIMEOUT_SECONDS,
routing_key=settings.VIDEO_TRANSCRIPT_MIGRATIONS_JOB_QUEUE
)
def async_migrate_transcript_subtask(self, *args, **kwargs): # pylint: disable=unused-argument
"""
Migrates a transcript of a given video in a course as a new celery task.
"""
success, failure = 'Success', 'Failure'
video_location, revision, language_code, force_update = args
command_run = kwargs['command_run']
store = modulestore()
video = store.get_item(usage_key=BlockUsageLocator.from_string(video_location), revision=revision)
edx_video_id = clean_video_id(video.edx_video_id)
if not kwargs['commit']:
LOGGER.info(
(u'[%s] [run=%s] [video-transcript-will-be-migrated] '
u'[revision=%s] [video=%s] [edx_video_id=%s] [language_code=%s]'),
MIGRATION_LOGS_PREFIX, command_run, revision, video_location, edx_video_id, language_code
)
return success
LOGGER.info(
(u'[%s] [run=%s] [transcripts-migration-process-started-for-video-transcript] [revision=%s] '
u'[video=%s] [edx_video_id=%s] [language_code=%s]'),
MIGRATION_LOGS_PREFIX, command_run, revision, video_location, edx_video_id, language_code
)
try:
transcripts_info = video.get_transcripts_info()
transcript_content, _, _ = get_transcript_from_contentstore(
video=video,
language=language_code,
output_format=Transcript.SJSON,
transcripts_info=transcripts_info,
)
is_video_valid = edx_video_id and is_video_available(edx_video_id)
if not is_video_valid:
edx_video_id = create_external_video('external-video')
video.edx_video_id = edx_video_id
# determine branch published/draft
branch_setting = (
ModuleStoreEnum.Branch.published_only
if revision == ModuleStoreEnum.RevisionOption.published_only else
ModuleStoreEnum.Branch.draft_preferred
)
with store.branch_setting(branch_setting):
store.update_item(video, ModuleStoreEnum.UserID.mgmt_command)
LOGGER.info(
u'[%s] [run=%s] [generated-edx-video-id] [revision=%s] [video=%s] [edx_video_id=%s] [language_code=%s]',
MIGRATION_LOGS_PREFIX, command_run, revision, video_location, edx_video_id, language_code
)
save_transcript_to_storage(
command_run=command_run,
edx_video_id=edx_video_id,
language_code=language_code,
transcript_content=transcript_content,
file_format=Transcript.SJSON,
force_update=force_update,
)
except (NotFoundError, TranscriptsGenerationException, ValCannotCreateError):
LOGGER.exception(
(u'[%s] [run=%s] [video-transcript-migration-failed-with-known-exc] [revision=%s] [video=%s] '
u'[edx_video_id=%s] [language_code=%s]'),
MIGRATION_LOGS_PREFIX, command_run, revision, video_location, edx_video_id, language_code
)
return failure
except Exception:
LOGGER.exception(
(u'[%s] [run=%s] [video-transcript-migration-failed-with-unknown-exc] [revision=%s] '
u'[video=%s] [edx_video_id=%s] [language_code=%s]'),
MIGRATION_LOGS_PREFIX, command_run, revision, video_location, edx_video_id, language_code
)
raise
LOGGER.info(
(u'[%s] [run=%s] [video-transcript-migration-succeeded-for-a-video] [revision=%s] '
u'[video=%s] [edx_video_id=%s] [language_code=%s]'),
MIGRATION_LOGS_PREFIX, command_run, revision, video_location, edx_video_id, language_code
)
return success
def clone_instance(instance, field_values):
......
......@@ -55,7 +55,7 @@ defusedxml==0.5.0 # via -r requirements/edx/base.in, djangorestframework
git+https://github.com/django-compressor/django-appconf@1526a842ee084b791aa66c931b3822091a442853#egg=django-appconf # via -r requirements/edx/github.in, django-statici18n
django-babel-underscore==0.5.2 # via -r requirements/edx/base.in
django-babel==0.6.2 # via django-babel-underscore
git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f37e6b0#egg=django-celery==3.2.1+edx.2 # via -r requirements/edx/github.in, edx-celeryutils, super-csv
git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f37e6b0#egg=django-celery==3.2.1+edx.2 # via -r requirements/edx/github.in
django-classy-tags==1.0.0 # via django-sekizai
django-config-models==2.0.0 # via -r requirements/edx/base.in, edx-enterprise
django-cors-headers==2.5.3 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in
......@@ -97,13 +97,13 @@ edx-analytics-data-api-client==0.15.3 # via -r requirements/edx/base.in
edx-api-doc-tools==1.0.2 # via -r requirements/edx/base.in
edx-bulk-grades==0.6.6 # via -r requirements/edx/base.in, staff-graded-xblock
edx-ccx-keys==1.0.0 # via -r requirements/edx/base.in
edx-celeryutils==0.3.2 # via -r requirements/edx/base.in, super-csv
edx-celeryutils==0.4.0 # via -r requirements/edx/base.in, super-csv
edx-completion==3.1.1 # via -r requirements/edx/base.in
edx-django-release-util==0.3.6 # via -r requirements/edx/base.in
edx-django-sites-extensions==2.4.3 # via -r requirements/edx/base.in
edx-django-utils==3.0 # via -r requirements/edx/base.in, django-config-models, edx-drf-extensions, edx-enterprise, edx-rest-api-client
edx-drf-extensions==3.0.0 # via -r requirements/edx/base.in, edx-completion, edx-enterprise, edx-organizations, edx-proctoring, edx-rbac, edx-when, edxval
edx-enterprise==2.5.0 # via -r requirements/edx/base.in
edx-enterprise==2.5.1 # via -r requirements/edx/base.in
edx-i18n-tools==0.5.0 # via ora2
edx-milestones==0.2.6 # via -r requirements/edx/base.in
edx-opaque-keys[django]==2.0.1 # via -r requirements/edx/paver.txt, edx-bulk-grades, edx-ccx-keys, edx-completion, edx-drf-extensions, edx-enterprise, edx-milestones, edx-organizations, edx-proctoring, edx-user-state-client, edx-when, xmodule
......@@ -168,7 +168,7 @@ numpy==1.18.1 # via calc, chem, scipy
git+https://github.com/joestump/python-oauth2.git@b94f69b1ad195513547924e380d9265133e995fa#egg=oauth2 # via -r requirements/edx/github.in
oauthlib==2.1.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in, django-oauth-toolkit, lti-consumer-xblock, requests-oauthlib, social-auth-core
git+https://github.com/edx/edx-ora2.git@2.6.17#egg=ora2==2.6.17 # via -r requirements/edx/github.in
packaging==20.1 # via drf-yasg
packaging==20.3 # via drf-yasg
path.py==12.4.0 # via edx-enterprise, edx-i18n-tools, ora2, xmodule
path==13.1.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/paver.txt, path.py
pathtools==0.1.2 # via -r requirements/edx/paver.txt, watchdog
......@@ -218,7 +218,7 @@ sailthru-client==2.2.3 # via -r requirements/edx/base.in, edx-ace
scipy==1.4.1 # via calc, chem
semantic-version==2.8.4 # via edx-drf-extensions
shapely==1.7.0 # via -r requirements/edx/base.in
shortuuid==0.5.0 # via -r requirements/edx/base.in
shortuuid==0.5.1 # via -r requirements/edx/base.in
simplejson==3.17.0 # via -r requirements/edx/base.in, sailthru-client, super-csv, xblock-utils
six==1.14.0 # via -r requirements/edx/../edx-sandbox/shared.txt, -r requirements/edx/base.in, -r requirements/edx/paver.txt, analytics-python, bleach, calc, cryptography, django-appconf, django-classy-tags, django-countries, django-pyfs, django-sekizai, django-simple-history, django-statici18n, drf-yasg, edx-ace, edx-ccx-keys, edx-django-release-util, edx-drf-extensions, edx-enterprise, edx-i18n-tools, edx-milestones, edx-opaque-keys, edx-rbac, edx-search, event-tracking, fs, fs-s3fs, help-tokens, html5lib, isodate, libsass, mock, nltk, packaging, paver, pycontracts, pyjwkest, python-dateutil, python-memcached, python-swiftclient, social-auth-app-django, social-auth-core, stevedore, xblock
slumber==0.7.1 # via edx-bulk-grades, edx-enterprise, edx-rest-api-client
......@@ -229,7 +229,7 @@ soupsieve==2.0 # via beautifulsoup4
sqlparse==0.3.1 # via -r requirements/edx/base.in
staff-graded-xblock==0.7 # via -r requirements/edx/base.in
stevedore==1.32.0 # via -r requirements/edx/base.in, -r requirements/edx/paver.txt, code-annotations, edx-ace, edx-enterprise, edx-opaque-keys
super-csv==0.9.6 # via -r requirements/edx/base.in, edx-bulk-grades
super-csv==0.9.7 # via -r requirements/edx/base.in, edx-bulk-grades
sympy==1.5.1 # via symmath
testfixtures==6.14.0 # via edx-enterprise
text-unidecode==1.3 # via python-slugify
......
......@@ -66,7 +66,7 @@ distlib==0.3.0 # via -r requirements/edx/testing.txt, virtualenv
git+https://github.com/django-compressor/django-appconf@1526a842ee084b791aa66c931b3822091a442853#egg=django-appconf # via -r requirements/edx/testing.txt, django-statici18n
django-babel-underscore==0.5.2 # via -r requirements/edx/testing.txt
django-babel==0.6.2 # via -r requirements/edx/testing.txt, django-babel-underscore
git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f37e6b0#egg=django-celery==3.2.1+edx.2 # via -r requirements/edx/testing.txt, edx-celeryutils, super-csv
git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f37e6b0#egg=django-celery==3.2.1+edx.2 # via -r requirements/edx/testing.txt
django-classy-tags==1.0.0 # via -r requirements/edx/testing.txt, django-sekizai
django-config-models==2.0.0 # via -r requirements/edx/testing.txt, edx-enterprise
django-cors-headers==2.5.3 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt
......@@ -109,13 +109,13 @@ edx-analytics-data-api-client==0.15.3 # via -r requirements/edx/testing.txt
edx-api-doc-tools==1.0.2 # via -r requirements/edx/testing.txt
edx-bulk-grades==0.6.6 # via -r requirements/edx/testing.txt, staff-graded-xblock
edx-ccx-keys==1.0.0 # via -r requirements/edx/testing.txt
edx-celeryutils==0.3.2 # via -r requirements/edx/testing.txt, super-csv
edx-celeryutils==0.4.0 # via -r requirements/edx/testing.txt, super-csv
edx-completion==3.1.1 # via -r requirements/edx/testing.txt
edx-django-release-util==0.3.6 # via -r requirements/edx/testing.txt
edx-django-sites-extensions==2.4.3 # via -r requirements/edx/testing.txt
edx-django-utils==3.0 # via -r requirements/edx/testing.txt, django-config-models, edx-drf-extensions, edx-enterprise, edx-rest-api-client
edx-drf-extensions==3.0.0 # via -r requirements/edx/testing.txt, edx-completion, edx-enterprise, edx-organizations, edx-proctoring, edx-rbac, edx-when, edxval
edx-enterprise==2.5.0 # via -r requirements/edx/testing.txt
edx-enterprise==2.5.1 # via -r requirements/edx/testing.txt
edx-i18n-tools==0.5.0 # via -r requirements/edx/testing.txt, ora2
edx-lint==1.4.1 # via -r requirements/edx/testing.txt
edx-milestones==0.2.6 # via -r requirements/edx/testing.txt
......@@ -202,7 +202,7 @@ numpy==1.18.1 # via -r requirements/edx/testing.txt, calc, chem, pan
git+https://github.com/joestump/python-oauth2.git@b94f69b1ad195513547924e380d9265133e995fa#egg=oauth2 # via -r requirements/edx/testing.txt
oauthlib==2.1.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, django-oauth-toolkit, lti-consumer-xblock, requests-oauthlib, social-auth-core
git+https://github.com/edx/edx-ora2.git@2.6.17#egg=ora2==2.6.17 # via -r requirements/edx/testing.txt
packaging==20.1 # via -r requirements/edx/testing.txt, drf-yasg, pytest, sphinx, tox
packaging==20.3 # via -r requirements/edx/testing.txt, drf-yasg, pytest, sphinx, tox
pandas==0.22.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt
path.py==12.4.0 # via -r requirements/edx/testing.txt, edx-enterprise, edx-i18n-tools, ora2, xmodule
path==13.1.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, path.py
......@@ -277,7 +277,7 @@ scipy==1.4.1 # via -r requirements/edx/testing.txt, calc, chem
selenium==3.141.0 # via -r requirements/edx/testing.txt, bok-choy
semantic-version==2.8.4 # via -r requirements/edx/testing.txt, edx-drf-extensions
shapely==1.7.0 # via -r requirements/edx/testing.txt
shortuuid==0.5.0 # via -r requirements/edx/testing.txt
shortuuid==0.5.1 # via -r requirements/edx/testing.txt
simplejson==3.17.0 # via -r requirements/edx/testing.txt, sailthru-client, super-csv, xblock-utils
singledispatch==3.4.0.3 # via -r requirements/edx/testing.txt
six==1.14.0 # via -r requirements/edx/pip-tools.txt, -r requirements/edx/testing.txt, analytics-python, astroid, bleach, bok-choy, calc, cryptography, diff-cover, django-appconf, django-classy-tags, django-countries, django-pyfs, django-sekizai, django-simple-history, django-statici18n, drf-yasg, edx-ace, edx-ccx-keys, edx-django-release-util, edx-drf-extensions, edx-enterprise, edx-i18n-tools, edx-lint, edx-milestones, edx-opaque-keys, edx-rbac, edx-search, edx-sphinx-theme, event-tracking, freezegun, fs, fs-s3fs, help-tokens, html5lib, httpretty, isodate, jsonschema, libsass, mando, mock, nltk, packaging, pathlib2, paver, pip-tools, pycontracts, pyjwkest, pytest-xdist, python-dateutil, python-memcached, python-swiftclient, singledispatch, social-auth-app-django, social-auth-core, sphinxcontrib-httpdomain, stevedore, tox, transifex-client, virtualenv, xblock
......@@ -287,7 +287,7 @@ social-auth-core==3.2.0 # via -r requirements/edx/testing.txt, social-auth-app
git+https://github.com/jazzband/sorl-thumbnail.git@13bedfb7d2970809eda597e3ef79318a6fa80ac2#egg=sorl-thumbnail # via -r requirements/edx/testing.txt
sortedcontainers==2.1.0 # via -r requirements/edx/testing.txt, pdfminer.six
soupsieve==2.0 # via -r requirements/edx/testing.txt, beautifulsoup4
sphinx==2.4.3 # via edx-sphinx-theme, sphinxcontrib-httpdomain
sphinx==2.4.4 # via edx-sphinx-theme, sphinxcontrib-httpdomain
sphinxcontrib-applehelp==1.0.2 # via sphinx
sphinxcontrib-devhelp==1.0.2 # via sphinx
sphinxcontrib-htmlhelp==1.0.3 # via sphinx
......@@ -299,7 +299,7 @@ sphinxcontrib-serializinghtml==1.1.4 # via sphinx
sqlparse==0.3.1 # via -r requirements/edx/testing.txt, django-debug-toolbar
staff-graded-xblock==0.7 # via -r requirements/edx/testing.txt
stevedore==1.32.0 # via -r requirements/edx/testing.txt, code-annotations, edx-ace, edx-enterprise, edx-opaque-keys
super-csv==0.9.6 # via -r requirements/edx/testing.txt, edx-bulk-grades
super-csv==0.9.7 # via -r requirements/edx/testing.txt, edx-bulk-grades
sympy==1.5.1 # via -r requirements/edx/testing.txt, symmath
testfixtures==6.14.0 # via -r requirements/edx/testing.txt, edx-enterprise
text-unidecode==1.3 # via -r requirements/edx/testing.txt, faker, python-slugify
......
......@@ -65,7 +65,7 @@ distlib==0.3.0 # via virtualenv
git+https://github.com/django-compressor/django-appconf@1526a842ee084b791aa66c931b3822091a442853#egg=django-appconf # via -r requirements/edx/base.txt, django-statici18n
django-babel-underscore==0.5.2 # via -r requirements/edx/base.txt
django-babel==0.6.2 # via -r requirements/edx/base.txt, django-babel-underscore
git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f37e6b0#egg=django-celery==3.2.1+edx.2 # via -r requirements/edx/base.txt, edx-celeryutils, super-csv
git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f37e6b0#egg=django-celery==3.2.1+edx.2 # via -r requirements/edx/base.txt
django-classy-tags==1.0.0 # via -r requirements/edx/base.txt, django-sekizai
django-config-models==2.0.0 # via -r requirements/edx/base.txt, edx-enterprise
django-cors-headers==2.5.3 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt
......@@ -105,13 +105,13 @@ edx-analytics-data-api-client==0.15.3 # via -r requirements/edx/base.txt
edx-api-doc-tools==1.0.2 # via -r requirements/edx/base.txt
edx-bulk-grades==0.6.6 # via -r requirements/edx/base.txt, staff-graded-xblock
edx-ccx-keys==1.0.0 # via -r requirements/edx/base.txt
edx-celeryutils==0.3.2 # via -r requirements/edx/base.txt, super-csv
edx-celeryutils==0.4.0 # via -r requirements/edx/base.txt, super-csv
edx-completion==3.1.1 # via -r requirements/edx/base.txt
edx-django-release-util==0.3.6 # via -r requirements/edx/base.txt
edx-django-sites-extensions==2.4.3 # via -r requirements/edx/base.txt
edx-django-utils==3.0 # via -r requirements/edx/base.txt, django-config-models, edx-drf-extensions, edx-enterprise, edx-rest-api-client
edx-drf-extensions==3.0.0 # via -r requirements/edx/base.txt, edx-completion, edx-enterprise, edx-organizations, edx-proctoring, edx-rbac, edx-when, edxval
edx-enterprise==2.5.0 # via -r requirements/edx/base.txt
edx-enterprise==2.5.1 # via -r requirements/edx/base.txt
edx-i18n-tools==0.5.0 # via -r requirements/edx/base.txt, -r requirements/edx/testing.in, ora2
edx-lint==1.4.1 # via -r requirements/edx/testing.in
edx-milestones==0.2.6 # via -r requirements/edx/base.txt
......@@ -193,7 +193,7 @@ numpy==1.18.1 # via -r requirements/edx/base.txt, -r requirements/ed
git+https://github.com/joestump/python-oauth2.git@b94f69b1ad195513547924e380d9265133e995fa#egg=oauth2 # via -r requirements/edx/base.txt
oauthlib==2.1.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, django-oauth-toolkit, lti-consumer-xblock, requests-oauthlib, social-auth-core
git+https://github.com/edx/edx-ora2.git@2.6.17#egg=ora2==2.6.17 # via -r requirements/edx/base.txt
packaging==20.1 # via -r requirements/edx/base.txt, drf-yasg, pytest, tox
packaging==20.3 # via -r requirements/edx/base.txt, drf-yasg, pytest, tox
pandas==0.22.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/coverage.txt
path.py==12.4.0 # via -r requirements/edx/base.txt, edx-enterprise, edx-i18n-tools, ora2, xmodule
path==13.1.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, path.py
......@@ -265,7 +265,7 @@ scipy==1.4.1 # via -r requirements/edx/base.txt, calc, chem
selenium==3.141.0 # via -r requirements/edx/testing.in, bok-choy
semantic-version==2.8.4 # via -r requirements/edx/base.txt, edx-drf-extensions
shapely==1.7.0 # via -r requirements/edx/base.txt
shortuuid==0.5.0 # via -r requirements/edx/base.txt
shortuuid==0.5.1 # via -r requirements/edx/base.txt
simplejson==3.17.0 # via -r requirements/edx/base.txt, sailthru-client, super-csv, xblock-utils
singledispatch==3.4.0.3 # via -r requirements/edx/testing.in
six==1.14.0 # via -r requirements/edx/base.txt, -r requirements/edx/coverage.txt, analytics-python, astroid, bleach, bok-choy, calc, cryptography, diff-cover, django-appconf, django-classy-tags, django-countries, django-pyfs, django-sekizai, django-simple-history, django-statici18n, drf-yasg, edx-ace, edx-ccx-keys, edx-django-release-util, edx-drf-extensions, edx-enterprise, edx-i18n-tools, edx-lint, edx-milestones, edx-opaque-keys, edx-rbac, edx-search, event-tracking, freezegun, fs, fs-s3fs, help-tokens, html5lib, httpretty, isodate, libsass, mando, mock, nltk, packaging, pathlib2, paver, pycontracts, pyjwkest, pytest-xdist, python-dateutil, python-memcached, python-swiftclient, singledispatch, social-auth-app-django, social-auth-core, stevedore, tox, transifex-client, virtualenv, xblock
......@@ -277,7 +277,7 @@ soupsieve==2.0 # via -r requirements/edx/base.txt, beautifulsoup4
sqlparse==0.3.1 # via -r requirements/edx/base.txt
staff-graded-xblock==0.7 # via -r requirements/edx/base.txt
stevedore==1.32.0 # via -r requirements/edx/base.txt, code-annotations, edx-ace, edx-enterprise, edx-opaque-keys
super-csv==0.9.6 # via -r requirements/edx/base.txt, edx-bulk-grades
super-csv==0.9.7 # via -r requirements/edx/base.txt, edx-bulk-grades
sympy==1.5.1 # via -r requirements/edx/base.txt, symmath
testfixtures==6.14.0 # via -r requirements/edx/base.txt, -r requirements/edx/testing.in, edx-enterprise
text-unidecode==1.3 # via -r requirements/edx/base.txt, faker, python-slugify
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment