-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat: allow block publish if was published before Library v2 migration #38865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
asadali145
wants to merge
1
commit into
openedx:master
Choose a base branch
from
mitodl:asad/v1-v2-lib-migration-fixes-and-command
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
...pps/contentstore/management/commands/migrate_course_legacy_library_blocks_to_item_bank.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| """ | ||
| Management command to migrate legacy library content blocks to Item Bank blocks for course(s). | ||
|
|
||
| This command can be run for a specific list of courses or for all courses. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
|
|
||
| from django.contrib.auth.models import User # pylint: disable=imported-auth-user | ||
| from django.core.management.base import BaseCommand, CommandError | ||
| from opaque_keys import InvalidKeyError | ||
| from opaque_keys.edx.keys import CourseKey | ||
|
|
||
| from cms.djangoapps.contentstore.tasks import migrate_course_legacy_library_blocks_to_item_bank | ||
| from openedx.core.djangoapps.content.course_overviews.models import CourseOverview | ||
| from xmodule.modulestore.django import modulestore # pylint: disable=wrong-import-order | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| """ | ||
| Migrate legacy library content blocks to Item Bank blocks for course(s). | ||
|
|
||
| Examples: | ||
| # Migrate specific courses. | ||
| $ ./manage.py cms migrate_course_legacy_library_blocks_to_item_bank \ | ||
| --course-ids course-v1:edX+DemoX+2024,course-v1:edX+Demo2+2024 --user-id 3 | ||
|
|
||
| # Migrate all courses. | ||
| $ ./manage.py cms migrate_course_legacy_library_blocks_to_item_bank --all-courses --user-id 3 | ||
|
|
||
| # Migrate all courses, re-publishing blocks that were published before the migration. | ||
| $ ./manage.py cms migrate_course_legacy_library_blocks_to_item_bank --all-courses --user-id 3 \ | ||
| --persist-publish-state | ||
| """ | ||
|
|
||
| def add_arguments(self, parser): | ||
| parser.add_argument( | ||
| '--course-ids', | ||
| help='Comma-separated list of course keys to migrate.', | ||
| ) | ||
| parser.add_argument( | ||
| '--all-courses', | ||
| action='store_true', | ||
| help='Migrate legacy library content blocks for all courses.', | ||
| ) | ||
| parser.add_argument( | ||
| '--user-id', | ||
| type=int, | ||
| required=True, | ||
| help='ID of the user performing the migration.', | ||
| ) | ||
| parser.add_argument( | ||
| '--persist-publish-state', | ||
| action='store_true', | ||
| help='Re-publish blocks that were published before the migration. Defaults to False.', | ||
| ) | ||
|
|
||
| def handle(self, *args, **options): | ||
| course_ids = options['course_ids'] | ||
| all_courses = options['all_courses'] | ||
| user_id = options['user_id'] | ||
| persist_publish_state = options['persist_publish_state'] | ||
|
|
||
| if not course_ids and not all_courses: | ||
| raise CommandError('Either --course-ids or --all-courses argument should be provided.') | ||
| if course_ids and all_courses: | ||
| raise CommandError('Only one of --course-ids or --all-courses argument should be provided.') | ||
|
|
||
| try: | ||
| User.objects.get(id=user_id) | ||
| except User.DoesNotExist: | ||
| raise CommandError(f'No user found with id: {user_id}') # pylint: disable=raise-missing-from # noqa: B904 | ||
|
|
||
| if all_courses: | ||
| raw_course_ids = CourseOverview.get_all_course_keys() | ||
| else: | ||
| raw_course_ids = [course_id.strip() for course_id in course_ids.split(',') if course_id.strip()] | ||
|
|
||
| course_keys = [] | ||
| for raw_course_id in raw_course_ids: | ||
| try: | ||
| course_key = CourseKey.from_string(str(raw_course_id)) | ||
| except InvalidKeyError: | ||
| log.error(f'Invalid course key: {raw_course_id}, skipping..') | ||
| continue | ||
| if not all_courses and not modulestore().get_course(course_key): | ||
| log.warning(f'Course not found: {course_key}, skipping..') | ||
| continue | ||
| course_keys.append(course_key) | ||
|
|
||
| for course_key in course_keys: | ||
| log.info(f'Dispatching legacy library migration for course: {course_key}') | ||
| migrate_course_legacy_library_blocks_to_item_bank.delay(user_id, str(course_key), persist_publish_state) | ||
|
|
||
| log.info(f'Dispatched migration for {len(course_keys)} course(s)') |
108 changes: 108 additions & 0 deletions
108
...store/management/commands/tests/test_migrate_course_legacy_library_blocks_to_item_bank.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| """ | ||
| Tests for `migrate_course_legacy_library_blocks_to_item_bank` Studio (cms) management command. | ||
| """ | ||
| from unittest import mock | ||
|
|
||
| import ddt | ||
| from django.core.management import CommandError, call_command | ||
|
|
||
| from common.djangoapps.student.tests.factories import UserFactory | ||
| from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory | ||
| from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # pylint: disable=wrong-import-order | ||
| from xmodule.modulestore.tests.factories import CourseFactory # pylint: disable=wrong-import-order | ||
|
|
||
|
|
||
| @ddt.ddt | ||
| class MigrateCourseLegacyLibraryBlocksToItemBankTests(ModuleStoreTestCase): | ||
| """ Tests for the `migrate_course_legacy_library_blocks_to_item_bank` management command. """ | ||
| TASK_PATCH_LOCATION = ( | ||
| 'cms.djangoapps.contentstore.management.commands.migrate_course_legacy_library_blocks_to_item_bank' | ||
| '.migrate_course_legacy_library_blocks_to_item_bank' | ||
| ) | ||
|
|
||
| def setUp(self): | ||
| """ Setup method - create a user and courses to migrate """ | ||
| super().setUp() | ||
| self.user = UserFactory() | ||
| self.first_course = CourseFactory.create() | ||
| self.second_course = CourseFactory.create() | ||
|
|
||
| def _call_command(self, **options): | ||
| """ Invoke the command, defaulting `user_id` to a valid user. """ | ||
| options.setdefault('user_id', self.user.id) | ||
| call_command('migrate_course_legacy_library_blocks_to_item_bank', **options) | ||
|
|
||
| @ddt.data( | ||
| ({}, 'Either --course-ids or --all-courses argument should be provided.'), | ||
| ( | ||
| {'course_ids': 'course-v1:test+course+run', 'all_courses': True}, | ||
| 'Only one of --course-ids or --all-courses argument should be provided.', | ||
| ), | ||
| ) | ||
| @ddt.unpack | ||
| def test_invalid_course_selector_raises_command_error(self, options, expected_message): | ||
| """ Test that specifying neither, or both, of --course-ids/--all-courses raises a CommandError. """ | ||
| with self.assertRaisesRegex(CommandError, expected_message): # noqa: PT027 | ||
| self._call_command(**options) | ||
|
|
||
| def test_invalid_user_id_raises_command_error(self): | ||
| """ Test that an unknown --user-id raises a CommandError. """ | ||
| invalid_user_id = self.user.id + 1000 | ||
| with self.assertRaisesRegex(CommandError, f'No user found with id: {invalid_user_id}'): # noqa: PT027 | ||
| call_command( | ||
| 'migrate_course_legacy_library_blocks_to_item_bank', | ||
| all_courses=True, | ||
| user_id=invalid_user_id, | ||
| ) | ||
|
|
||
| @ddt.data( | ||
| 'invalid_key', | ||
| 'course-v1:test+nonexistent+run', | ||
| ) | ||
| def test_unparsable_or_nonexistent_course_id_is_skipped(self, bad_course_id): | ||
| """ | ||
| Test that an unparsable or nonexistent course key passed via --course-ids is skipped, | ||
| while other, valid, course keys are still dispatched. | ||
| """ | ||
| course_ids = f'{bad_course_id},{self.first_course.id}' | ||
| with mock.patch(self.TASK_PATCH_LOCATION) as patched_task: | ||
| self._call_command(course_ids=course_ids) | ||
|
|
||
| patched_task.delay.assert_called_once_with(self.user.id, str(self.first_course.id), False) | ||
|
|
||
| def test_course_ids_dispatches_task_for_each_course(self): | ||
| """ Test that the task is dispatched once per course key passed via --course-ids. """ | ||
| course_ids = f'{self.first_course.id},{self.second_course.id}' | ||
| with mock.patch(self.TASK_PATCH_LOCATION) as patched_task: | ||
| self._call_command(course_ids=course_ids) | ||
|
|
||
| expected_calls = [ | ||
| mock.call(self.user.id, str(self.first_course.id), False), | ||
| mock.call(self.user.id, str(self.second_course.id), False), | ||
| ] | ||
| self.assertEqual(patched_task.delay.mock_calls, expected_calls) # noqa: PT009 | ||
|
|
||
| def test_all_courses_dispatches_task_for_every_course(self): | ||
| """ Test that --all-courses dispatches the task for every course known to CourseOverview. """ | ||
| CourseOverviewFactory(id=self.first_course.id) | ||
| CourseOverviewFactory(id=self.second_course.id) | ||
|
|
||
| with mock.patch(self.TASK_PATCH_LOCATION) as patched_task: | ||
| self._call_command(all_courses=True) | ||
|
|
||
| expected_calls = [ | ||
| mock.call(self.user.id, str(self.first_course.id), False), | ||
| mock.call(self.user.id, str(self.second_course.id), False), | ||
| ] | ||
| self.assertCountEqual(patched_task.delay.mock_calls, expected_calls) # noqa: PT009 | ||
|
|
||
| @ddt.data(True, False) | ||
| def test_persist_publish_state_passed_to_task(self, persist_publish_state): | ||
| """ Test that --persist-publish-state is forwarded to the task, defaulting to False. """ | ||
| with mock.patch(self.TASK_PATCH_LOCATION) as patched_task: | ||
| self._call_command( | ||
| course_ids=str(self.first_course.id), | ||
| persist_publish_state=persist_publish_state, | ||
| ) | ||
|
|
||
| patched_task.delay.assert_called_once_with(self.user.id, str(self.first_course.id), persist_publish_state) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.