From 6200f2c83e6f305f0a746e0aa3024a68880708a8 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 27 Aug 2025 10:01:23 +0530 Subject: [PATCH 01/10] feat: updates legacy libraries list API to include migration info Adds migration info like `migrated_to_title`, `migrated_to_key` and `is_migrated` fields indicating whether the legacy library was migrated to library v2. If yes, it includes the new library name and key. Users can also filter by migration status using `is_migrated` query param. --- .../rest_api/v1/serializers/home.py | 12 ++++++++ .../contentstore/rest_api/v1/views/home.py | 19 +++++++++++- cms/djangoapps/contentstore/utils.py | 27 ++++++++++++----- cms/djangoapps/contentstore/views/course.py | 5 ++-- cms/djangoapps/modulestore_migrator/api.py | 30 ++++++++++++++++++- 5 files changed, 82 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index fdc06e9291d0..8af677771c1b 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -29,6 +29,18 @@ class LibraryViewSerializer(serializers.Serializer): org = serializers.CharField() number = serializers.CharField() can_edit = serializers.BooleanField() + is_migrated = serializers.SerializerMethodField() + migrated_to_title = serializers.CharField( + source="migrations__target__title", + required=False + ) + migrated_to_key = serializers.CharField( + source="migrations__target__key", + required=False + ) + + def get_is_migrated(self, obj): + return "migrations__target__key" in obj class CourseHomeTabSerializer(serializers.Serializer): diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index 62b56533878f..cbd44fa6c2c2 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -184,7 +184,17 @@ class HomePageLibrariesView(APIView): "org", apidocs.ParameterLocation.QUERY, description="Query param to filter by course org", - )], + ), + apidocs.query_parameter( + "is_migrated", + bool, + description=( + "Query param to filter by migrated status of library." + " If present (true or false), it will filter by migration status" + " else it will return all legacy libraries." + ), + ) + ], responses={ 200: LibraryTabSerializer, 401: "The requester is not authenticated.", @@ -197,6 +207,13 @@ def get(self, request: Request): **Example Request** GET /api/contentstore/v1/home/libraries + # Returns all legacy libraries + + GET /api/contentstore/v1/home/libraries?is_migrated=true + # Returns legacy libraries that were migrated to library v2 + + GET /api/contentstore/v1/home/libraries?is_migrated=false + # Returns legacy libraries that were not migrated to library v2 **Response Values** diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index c526111cf21f..882c425a1357 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -32,6 +32,7 @@ from openedx_events.learning.data import CourseNotificationData from openedx_events.learning.signals import COURSE_NOTIFICATION_REQUESTED from pytz import UTC +from rest_framework.fields import BooleanField from xblock.fields import Scope from cms.djangoapps.contentstore.toggles import ( @@ -61,6 +62,7 @@ ) from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.djangoapps.models.settings.course_metadata import CourseMetadata +from cms.djangoapps.modulestore_migrator.api import get_migration_info from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager from common.djangoapps.course_modes.models import CourseMode @@ -1584,12 +1586,12 @@ def get_library_context(request, request_is_json=False): It is used for both DRF and django views. """ from cms.djangoapps.contentstore.views.course import ( + _accessible_libraries_iter, + _format_library_for_view, + _get_course_creator_status, get_allowed_organizations, get_allowed_organizations_for_libraries, user_can_create_organizations, - _accessible_libraries_iter, - _get_course_creator_status, - _format_library_for_view, ) from cms.djangoapps.contentstore.views.library import ( user_can_view_create_library_button, @@ -1598,9 +1600,22 @@ def get_library_context(request, request_is_json=False): user_can_create_library, ) - libraries = _accessible_libraries_iter(request.user) if libraries_v1_enabled() else [] + libraries = set(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) + library_keys = [lib.location.library_key for lib in libraries] + migration_info = get_migration_info(library_keys) + is_migrated_filter = request.GET.get('is_migrated', None) data = { - 'libraries': [_format_library_for_view(lib, request) for lib in libraries], + 'libraries': [ + _format_library_for_view( + lib, + request, + migration_info.get(lib.location.library_key) + ) + for lib in libraries + if is_migrated_filter is None or ( + BooleanField().to_internal_value(is_migrated_filter) == (lib.location.library_key in migration_info) + ) + ] } if not request_is_json: @@ -1716,9 +1731,7 @@ def get_home_context(request, no_course=False): get_allowed_organizations, get_allowed_organizations_for_libraries, user_can_create_organizations, - _accessible_libraries_iter, _get_course_creator_status, - _format_library_for_view, ) from cms.djangoapps.contentstore.views.library import ( user_can_view_create_library_button, diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index ffb93ed010d6..fa8769dc0cb9 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -7,7 +7,7 @@ import random import re import string -from typing import Dict +from typing import Dict, NamedTuple, Optional import django.utils from ccx_keys.locator import CCXLocator @@ -669,7 +669,7 @@ def library_listing(request): return render_to_response('index.html', data) -def _format_library_for_view(library, request): +def _format_library_for_view(library, request, migrated_to: Optional[NamedTuple]): """ Return a dict of the data which the view requires for each library """ @@ -681,6 +681,7 @@ def _format_library_for_view(library, request): 'org': library.display_org_with_default, 'number': library.display_number_with_default, 'can_edit': has_studio_write_access(request.user, library.location.library_key), + **(migrated_to._asdict() if migrated_to is not None else {}), } diff --git a/cms/djangoapps/modulestore_migrator/api.py b/cms/djangoapps/modulestore_migrator/api.py index 5f6ca543a091..1d5d30eee273 100644 --- a/cms/djangoapps/modulestore_migrator/api.py +++ b/cms/djangoapps/modulestore_migrator/api.py @@ -2,12 +2,14 @@ API for migration from modulestore to learning core """ from opaque_keys.edx.locator import LibraryLocatorV2 -from opaque_keys.edx.keys import LearningContextKey +from opaque_keys.edx.keys import CourseKey, LearningContextKey +from opaque_keys.edx.locator import LibraryLocator from openedx_learning.api.authoring import get_collection from celery.result import AsyncResult from openedx.core.djangoapps.content_libraries.api import get_library from openedx.core.types.user import AuthUser +from user_tasks.models import UserTaskStatus from . import tasks from .data import RepeatHandlingStrategy @@ -16,6 +18,8 @@ __all__ = ( "start_migration_to_library", + "is_successfully_migrated", + "get_migration_info", ) @@ -56,3 +60,27 @@ def start_migration_to_library( preserve_url_slugs=preserve_url_slugs, forward_source_to_target=forward_source_to_target, ) + + +def is_successfully_migrated(source_key: CourseKey | LibraryLocator) -> bool: + """ + Check if the source course/library has been migrated successfully. + """ + return ModulestoreSource.objects.get_or_create(key=str(source_key))[0].migrations.filter( + task_status__state=UserTaskStatus.SUCCEEDED + ).exists() + + +def get_migration_info(source_keys: list[CourseKey | LibraryLocator]) -> dict: + """ + Check if the source course/library has been migrated successfully and return target info + """ + return ModulestoreSource.objects.filter( + migrations__task_status__state=UserTaskStatus.SUCCEEDED, + key__in=source_keys + ).prefetch_related('migrations__target').values_list( + 'migrations__target__key', + 'migrations__target__title', + 'key', + named=True + ).in_bulk(field_name='key') From 95993326304846e465b01d580f0983644fe50d03 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 27 Aug 2025 16:53:19 +0530 Subject: [PATCH 02/10] test: migration status in libraries api --- .../rest_api/v1/views/tests/test_home.py | 111 ++++++++++++++++-- 1 file changed, 100 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 3e88e0401060..a363480275c4 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -9,10 +9,17 @@ from django.test import override_settings from django.urls import reverse from rest_framework import status +from opaque_keys.edx.locator import LibraryLocatorV2 +from organizations.tests.factories import OrganizationFactory from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase +from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy +from cms.djangoapps.modulestore_migrator.tests.factories import ModulestoreSourceFactory +from cms.djangoapps.modulestore_migrator import api as migrator_api +from openedx.core.djangoapps.content_libraries import api as lib_api from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from xmodule.modulestore.django import modulestore @ddt.ddt @@ -131,7 +138,6 @@ def test_home_page_response(self): } self.assertEqual(response.status_code, status.HTTP_200_OK) - print(response.data) self.assertDictEqual(expected_response, response.data) def test_home_page_response_with_api_v2(self): @@ -246,23 +252,106 @@ class HomePageLibrariesViewTest(LibraryTestCase): def setUp(self): super().setUp() + # Create an additional legacy library + self.lib_key_1 = self._create_library(library="lib1") + self.organization = OrganizationFactory() + + # Create a new v2 library + self.lib_key_v2 = LibraryLocatorV2.from_string( + f"lib:{self.organization.short_name}:test-key" + ) + lib_api.create_library( + org=self.organization, + slug=self.lib_key_v2.slug, + title="Test Library", + ) + # Create a migration source for the legacy library + self.source = ModulestoreSourceFactory(key=self.lib_key_1) self.url = reverse("cms.djangoapps.contentstore:v1:libraries") + # Migrate self.lib_key_1 to self.lib_key_v2 + migrator_api.start_migration_to_library( + user=self.user, + source_key=self.source.key, + target_library_key=self.lib_key_v2, + target_collection_slug=None, + composition_level=CompositionLevel.Component.value, + repeat_handling_strategy=RepeatHandlingStrategy.Skip.value, + preserve_url_slugs=True, + forward_source_to_target=False, + ) def test_home_page_libraries_response(self): """Check successful response content""" response = self.client.get(self.url) expected_response = { - "libraries": [{ - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib', - 'url': '/library/library-v1:org+lib', - 'org': 'org', - 'number': 'lib', - 'can_edit': True - }], + "libraries": [ + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True, + 'is_migrated': False, + }, + # Second legacy library was migrated so it will include + # migrated_to_title and migrated_to_key as well + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib1', + 'url': '/library/library-v1:org+lib1', + 'org': 'org', + 'number': 'lib1', + 'can_edit': True, + 'is_migrated': True, + 'migrated_to_title': 'Test Library', + 'migrated_to_key': 'lib:name0:test-key', + }, + ], } self.assertEqual(response.status_code, status.HTTP_200_OK) - print(response.data) - self.assertDictEqual(expected_response, response.data) + self.assertDictEqual(expected_response, response.json()) + + # Fetch legacy libraries that were migrated to v2 + response = self.client.get(self.url + '?is_migrated=true') + + expected_response = { + "libraries": [ + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib1', + 'url': '/library/library-v1:org+lib1', + 'org': 'org', + 'number': 'lib1', + 'can_edit': True, + 'is_migrated': True, + 'migrated_to_title': 'Test Library', + 'migrated_to_key': 'lib:name0:test-key', + } + ], + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.json()) + + # Fetch legacy libraries that were not migrated to v2 + response = self.client.get(self.url + '?is_migrated=false') + + expected_response = { + "libraries": [ + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True, + 'is_migrated': False, + }, + ], + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.json()) From 8b62b48ad53c7fc994f1ca880d207318dc30d031 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 27 Aug 2025 16:54:16 +0530 Subject: [PATCH 03/10] chore: organize imports --- .../contentstore/rest_api/v1/views/home.py | 7 ++++--- .../rest_api/v1/views/tests/test_home.py | 14 +++++++------- cms/djangoapps/contentstore/utils.py | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index cbd44fa6c2c2..a4e93de9caff 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -2,14 +2,15 @@ import edx_api_doc_tools as apidocs from django.conf import settings +from organizations import api as org_api from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from organizations import api as org_api + from openedx.core.lib.api.view_utils import view_auth_classes -from ....utils import get_home_context, get_course_context, get_library_context -from ..serializers import StudioHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer +from ....utils import get_course_context, get_home_context, get_library_context +from ..serializers import CourseHomeTabSerializer, LibraryTabSerializer, StudioHomeSerializer @view_auth_classes(is_authenticated=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index a363480275c4..8f2d20c61469 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -1,25 +1,25 @@ """ Unit tests for home page view. """ -import ddt -import pytz from collections import OrderedDict from datetime import datetime, timedelta + +import ddt +import pytz from django.conf import settings from django.test import override_settings from django.urls import reverse -from rest_framework import status from opaque_keys.edx.locator import LibraryLocatorV2 from organizations.tests.factories import OrganizationFactory +from rest_framework import status -from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.modulestore_migrator import api as migrator_api from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy from cms.djangoapps.modulestore_migrator.tests.factories import ModulestoreSourceFactory -from cms.djangoapps.modulestore_migrator import api as migrator_api -from openedx.core.djangoapps.content_libraries import api as lib_api from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory -from xmodule.modulestore.django import modulestore +from openedx.core.djangoapps.content_libraries import api as lib_api @ddt.ddt diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 882c425a1357..9d1c367c7128 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -26,7 +26,7 @@ from milestones import api as milestones_api from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey, UsageKeyV2 -from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocator, BlockUsageLocator +from opaque_keys.edx.locator import BlockUsageLocator, LibraryContainerLocator, LibraryLocator from openedx_events.content_authoring.data import DuplicatedXBlockData from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED from openedx_events.learning.data import CourseNotificationData @@ -89,8 +89,8 @@ from common.djangoapps.xblock_django.api import deprecated_xblocks from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core import toggles as core_toggles -from openedx.core.djangoapps.content_libraries.api import get_container from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.content_libraries.api import get_container from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND From ec4f7d083589b897e6c044961b8ccc18f267dab1 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 28 Aug 2025 20:00:18 +0530 Subject: [PATCH 04/10] feat: include collection info in api --- .../rest_api/v1/serializers/home.py | 11 ++- .../rest_api/v1/views/tests/test_home.py | 73 ++++++++++++------- cms/djangoapps/contentstore/utils.py | 2 +- cms/djangoapps/modulestore_migrator/api.py | 2 + 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 8af677771c1b..bbc45ddf9a37 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -4,9 +4,8 @@ from rest_framework import serializers -from openedx.core.lib.api.serializers import CourseKeyField - from cms.djangoapps.contentstore.rest_api.serializers.common import CourseCommonSerializer +from openedx.core.lib.api.serializers import CourseKeyField class UnsucceededCourseSerializer(serializers.Serializer): @@ -38,6 +37,14 @@ class LibraryViewSerializer(serializers.Serializer): source="migrations__target__key", required=False ) + migrated_to_collection_key = serializers.CharField( + source="migrations__target_collection__key", + required=False + ) + migrated_to_collection_title = serializers.CharField( + source="migrations__target_collection__title", + required=False + ) def get_is_migrated(self, obj): return "migrations__target__key" in obj diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 8f2d20c61469..7c43674208f5 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -10,6 +10,7 @@ from django.test import override_settings from django.urls import reverse from opaque_keys.edx.locator import LibraryLocatorV2 +from openedx_learning.api import authoring as authoring_api from organizations.tests.factories import OrganizationFactory from rest_framework import status @@ -265,15 +266,26 @@ def setUp(self): slug=self.lib_key_v2.slug, title="Test Library", ) + library = lib_api.ContentLibrary.objects.get(slug=self.lib_key_v2.slug) + learning_package = library.learning_package # Create a migration source for the legacy library self.source = ModulestoreSourceFactory(key=self.lib_key_1) self.url = reverse("cms.djangoapps.contentstore:v1:libraries") + # Create a collection to migrate this library to + collection_key = "test-collection" + authoring_api.create_collection( + learning_package_id=learning_package.id, + key=collection_key, + title="Test Collection", + created_by=self.user.id, + ) + # Migrate self.lib_key_1 to self.lib_key_v2 migrator_api.start_migration_to_library( user=self.user, source_key=self.source.key, target_library_key=self.lib_key_v2, - target_collection_slug=None, + target_collection_slug=collection_key, composition_level=CompositionLevel.Component.value, repeat_handling_strategy=RepeatHandlingStrategy.Skip.value, preserve_url_slugs=True, @@ -284,35 +296,38 @@ def test_home_page_libraries_response(self): """Check successful response content""" response = self.client.get(self.url) - expected_response = { - "libraries": [ - { - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib', - 'url': '/library/library-v1:org+lib', - 'org': 'org', - 'number': 'lib', - 'can_edit': True, - 'is_migrated': False, - }, - # Second legacy library was migrated so it will include - # migrated_to_title and migrated_to_key as well - { - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib1', - 'url': '/library/library-v1:org+lib1', - 'org': 'org', - 'number': 'lib1', - 'can_edit': True, - 'is_migrated': True, - 'migrated_to_title': 'Test Library', - 'migrated_to_key': 'lib:name0:test-key', - }, - ], - } + expected_response = [ + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True, + 'is_migrated': False, + }, + # Second legacy library was migrated so it will include + # migrated_to_title and migrated_to_key as well + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib1', + 'url': '/library/library-v1:org+lib1', + 'org': 'org', + 'number': 'lib1', + 'can_edit': True, + 'is_migrated': True, + 'migrated_to_title': 'Test Library', + 'migrated_to_key': 'lib:name0:test-key', + 'migrated_to_collection_key': 'test-collection', + 'migrated_to_collection_title': 'Test Collection', + }, + ] self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(expected_response, response.json()) + sorted_response = sorted(response.json()['libraries'], key=lambda x: x['library_key']) + self.assertEqual(len(expected_response), len(sorted_response)) + for i in range(len(sorted_response)): + self.assertDictEqual(expected_response[i], sorted_response[i]) # Fetch legacy libraries that were migrated to v2 response = self.client.get(self.url + '?is_migrated=true') @@ -329,6 +344,8 @@ def test_home_page_libraries_response(self): 'is_migrated': True, 'migrated_to_title': 'Test Library', 'migrated_to_key': 'lib:name0:test-key', + 'migrated_to_collection_key': 'test-collection', + 'migrated_to_collection_title': 'Test Collection', } ], } diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 9d1c367c7128..f432c8b79ed0 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1609,7 +1609,7 @@ def get_library_context(request, request_is_json=False): _format_library_for_view( lib, request, - migration_info.get(lib.location.library_key) + migrated_to=migration_info.get(lib.location.library_key) ) for lib in libraries if is_migrated_filter is None or ( diff --git a/cms/djangoapps/modulestore_migrator/api.py b/cms/djangoapps/modulestore_migrator/api.py index 1d5d30eee273..6b4c3d6bdc74 100644 --- a/cms/djangoapps/modulestore_migrator/api.py +++ b/cms/djangoapps/modulestore_migrator/api.py @@ -81,6 +81,8 @@ def get_migration_info(source_keys: list[CourseKey | LibraryLocator]) -> dict: ).prefetch_related('migrations__target').values_list( 'migrations__target__key', 'migrations__target__title', + 'migrations__target_collection__key', + 'migrations__target_collection__title', 'key', named=True ).in_bulk(field_name='key') From 67958fd3ac11371d273bea61043868b09c05f296 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 29 Aug 2025 16:22:47 +0530 Subject: [PATCH 05/10] feat: add paginaton to legacy libraries api --- .../rest_api/v1/serializers/__init__.py | 2 +- .../contentstore/rest_api/v1/views/home.py | 75 ++++++++++++++++--- cms/djangoapps/contentstore/utils.py | 2 +- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index f96cb9adeaa8..851c97698510 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -9,7 +9,7 @@ from .course_waffle_flags import CourseWaffleFlagsSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer from .group_configurations import CourseGroupConfigurationsSerializer -from .home import StudioHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer +from .home import CourseHomeTabSerializer, LibraryTabSerializer, LibraryViewSerializer, StudioHomeSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index a4e93de9caff..b375ef8849ef 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -1,8 +1,12 @@ """ API Views for course home """ +from collections import OrderedDict + import edx_api_doc_tools as apidocs from django.conf import settings +from django.core.paginator import Paginator from organizations import api as org_api +from rest_framework.fields import BooleanField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -10,7 +14,7 @@ from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_course_context, get_home_context, get_library_context -from ..serializers import CourseHomeTabSerializer, LibraryTabSerializer, StudioHomeSerializer +from ..serializers import CourseHomeTabSerializer, LibraryTabSerializer, LibraryViewSerializer, StudioHomeSerializer @view_auth_classes(is_authenticated=True) @@ -179,6 +183,14 @@ class HomePageLibrariesView(APIView): """ View for getting all courses and libraries available to the logged in user. """ + def get_paginated_response(self, data, paginator): + """Return a paginated style `Response` object for the given output data.""" + return Response(OrderedDict([ + ('count', paginator.count), + ('num_pages', paginator.num_pages), + ('results', data), + ])) + @apidocs.schema( parameters=[ apidocs.string_parameter( @@ -194,7 +206,22 @@ class HomePageLibrariesView(APIView): " If present (true or false), it will filter by migration status" " else it will return all legacy libraries." ), - ) + ), + apidocs.query_parameter( + "pagination", + bool, + description="Returns a paginated response if set to true", + ), + apidocs.query_parameter( + "page_size", + int, + description="If pagination is true, then sets page size", + ), + apidocs.query_parameter( + "page_num", + int, + description="If pagination is true, then returns specified page number", + ), ], responses={ 200: LibraryTabSerializer, @@ -225,22 +252,46 @@ def get(self, request: Request): **Example Response** + If `pagination=false` or not set + ```json { "libraries": [ { - "display_name": "My First Library", - "library_key": "library-v1:new+CPSPR", - "url": "/library/library-v1:new+CPSPR", - "org": "new", - "number": "CPSPR", - "can_edit": true + "display_name": "My First Library", + "library_key": "library-v1:new+CPSPR", + "url": "/library/library-v1:new+CPSPR", + "org": "new", + "number": "CPSPR", + "can_edit": true } - ], } + ], + } ``` + + If `pagination=true` + { + "count": 1, + "num_pages": 1, + "results": [ + { + "display_name": "My First Library", + "library_key": "library-v1:new+CPSPR", + "url": "/library/library-v1:new+CPSPR", + "org": "new", + "number": "CPSPR", + "can_edit": true + } + ], + } """ library_context = get_library_context(request) - serializer = LibraryTabSerializer(library_context) - - return Response(serializer.data) + if BooleanField().to_internal_value(request.GET.get("pagination", False)): + paginator = Paginator(library_context.get("libraries", []), request.GET.get("page_size", 10)) + serializer = LibraryViewSerializer(paginator.get_page(request.GET.get("page_num", 1)), many=True) + return self.get_paginated_response(serializer.data, paginator) + else: + # Support legacy view that doesn't support pagination + serializer = LibraryTabSerializer(library_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index f432c8b79ed0..7140e7f85365 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1600,7 +1600,7 @@ def get_library_context(request, request_is_json=False): user_can_create_library, ) - libraries = set(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) + libraries = list(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) library_keys = [lib.location.library_key for lib in libraries] migration_info = get_migration_info(library_keys) is_migrated_filter = request.GET.get('is_migrated', None) From f066973c239f7478601e3f5bba9c14a748eb18de Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 29 Aug 2025 16:29:26 +0530 Subject: [PATCH 06/10] test: paginated response --- .../rest_api/v1/views/tests/test_home.py | 96 +++++++++++++------ 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 7c43674208f5..491f1f00c76f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -296,38 +296,37 @@ def test_home_page_libraries_response(self): """Check successful response content""" response = self.client.get(self.url) - expected_response = [ - { - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib', - 'url': '/library/library-v1:org+lib', - 'org': 'org', - 'number': 'lib', - 'can_edit': True, - 'is_migrated': False, - }, - # Second legacy library was migrated so it will include - # migrated_to_title and migrated_to_key as well - { - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib1', - 'url': '/library/library-v1:org+lib1', - 'org': 'org', - 'number': 'lib1', - 'can_edit': True, - 'is_migrated': True, - 'migrated_to_title': 'Test Library', - 'migrated_to_key': 'lib:name0:test-key', - 'migrated_to_collection_key': 'test-collection', - 'migrated_to_collection_title': 'Test Collection', - }, - ] + expected_response = { + "libraries": [ + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True, + 'is_migrated': False, + }, + # Second legacy library was migrated so it will include + # migrated_to_title and migrated_to_key as well + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib1', + 'url': '/library/library-v1:org+lib1', + 'org': 'org', + 'number': 'lib1', + 'can_edit': True, + 'is_migrated': True, + 'migrated_to_title': 'Test Library', + 'migrated_to_key': 'lib:name0:test-key', + 'migrated_to_collection_key': 'test-collection', + 'migrated_to_collection_title': 'Test Collection', + }, + ] + } self.assertEqual(response.status_code, status.HTTP_200_OK) - sorted_response = sorted(response.json()['libraries'], key=lambda x: x['library_key']) - self.assertEqual(len(expected_response), len(sorted_response)) - for i in range(len(sorted_response)): - self.assertDictEqual(expected_response[i], sorted_response[i]) + self.assertDictEqual(expected_response, response.json()) # Fetch legacy libraries that were migrated to v2 response = self.client.get(self.url + '?is_migrated=true') @@ -372,3 +371,40 @@ def test_home_page_libraries_response(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.json()) + + # Test paginated response + response = self.client.get(self.url + "?pagination=true") + + expected_response = { + "count": 2, + "num_pages": 1, + "results": [ + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True, + 'is_migrated': False, + }, + # Second legacy library was migrated so it will include + # migrated_to_title and migrated_to_key as well + { + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib1', + 'url': '/library/library-v1:org+lib1', + 'org': 'org', + 'number': 'lib1', + 'can_edit': True, + 'is_migrated': True, + 'migrated_to_title': 'Test Library', + 'migrated_to_key': 'lib:name0:test-key', + 'migrated_to_collection_key': 'test-collection', + 'migrated_to_collection_title': 'Test Collection', + }, + ] + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.json()) From 44af5d4d9e7aff6b1d369d1d6d9d7e34717140db Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 3 Sep 2025 19:53:03 +0530 Subject: [PATCH 07/10] Revert "feat: add paginaton to legacy libraries api" This reverts commit c1c7dcacfd03cbf9c337f21471d8cbe245fb4527. --- .../rest_api/v1/serializers/__init__.py | 2 +- .../contentstore/rest_api/v1/views/home.py | 75 +++---------------- cms/djangoapps/contentstore/utils.py | 2 +- 3 files changed, 14 insertions(+), 65 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 851c97698510..f96cb9adeaa8 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -9,7 +9,7 @@ from .course_waffle_flags import CourseWaffleFlagsSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer from .group_configurations import CourseGroupConfigurationsSerializer -from .home import CourseHomeTabSerializer, LibraryTabSerializer, LibraryViewSerializer, StudioHomeSerializer +from .home import StudioHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index b375ef8849ef..a4e93de9caff 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -1,12 +1,8 @@ """ API Views for course home """ -from collections import OrderedDict - import edx_api_doc_tools as apidocs from django.conf import settings -from django.core.paginator import Paginator from organizations import api as org_api -from rest_framework.fields import BooleanField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -14,7 +10,7 @@ from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_course_context, get_home_context, get_library_context -from ..serializers import CourseHomeTabSerializer, LibraryTabSerializer, LibraryViewSerializer, StudioHomeSerializer +from ..serializers import CourseHomeTabSerializer, LibraryTabSerializer, StudioHomeSerializer @view_auth_classes(is_authenticated=True) @@ -183,14 +179,6 @@ class HomePageLibrariesView(APIView): """ View for getting all courses and libraries available to the logged in user. """ - def get_paginated_response(self, data, paginator): - """Return a paginated style `Response` object for the given output data.""" - return Response(OrderedDict([ - ('count', paginator.count), - ('num_pages', paginator.num_pages), - ('results', data), - ])) - @apidocs.schema( parameters=[ apidocs.string_parameter( @@ -206,22 +194,7 @@ def get_paginated_response(self, data, paginator): " If present (true or false), it will filter by migration status" " else it will return all legacy libraries." ), - ), - apidocs.query_parameter( - "pagination", - bool, - description="Returns a paginated response if set to true", - ), - apidocs.query_parameter( - "page_size", - int, - description="If pagination is true, then sets page size", - ), - apidocs.query_parameter( - "page_num", - int, - description="If pagination is true, then returns specified page number", - ), + ) ], responses={ 200: LibraryTabSerializer, @@ -252,46 +225,22 @@ def get(self, request: Request): **Example Response** - If `pagination=false` or not set - ```json { "libraries": [ { - "display_name": "My First Library", - "library_key": "library-v1:new+CPSPR", - "url": "/library/library-v1:new+CPSPR", - "org": "new", - "number": "CPSPR", - "can_edit": true + "display_name": "My First Library", + "library_key": "library-v1:new+CPSPR", + "url": "/library/library-v1:new+CPSPR", + "org": "new", + "number": "CPSPR", + "can_edit": true } - ], - } + ], } ``` - - If `pagination=true` - { - "count": 1, - "num_pages": 1, - "results": [ - { - "display_name": "My First Library", - "library_key": "library-v1:new+CPSPR", - "url": "/library/library-v1:new+CPSPR", - "org": "new", - "number": "CPSPR", - "can_edit": true - } - ], - } """ library_context = get_library_context(request) - if BooleanField().to_internal_value(request.GET.get("pagination", False)): - paginator = Paginator(library_context.get("libraries", []), request.GET.get("page_size", 10)) - serializer = LibraryViewSerializer(paginator.get_page(request.GET.get("page_num", 1)), many=True) - return self.get_paginated_response(serializer.data, paginator) - else: - # Support legacy view that doesn't support pagination - serializer = LibraryTabSerializer(library_context) - return Response(serializer.data) + serializer = LibraryTabSerializer(library_context) + + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 7140e7f85365..f432c8b79ed0 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1600,7 +1600,7 @@ def get_library_context(request, request_is_json=False): user_can_create_library, ) - libraries = list(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) + libraries = set(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) library_keys = [lib.location.library_key for lib in libraries] migration_info = get_migration_info(library_keys) is_migrated_filter = request.GET.get('is_migrated', None) From 65ffd9af93a43be607efbcbf201ad78f0e69a83f Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 3 Sep 2025 19:55:54 +0530 Subject: [PATCH 08/10] Revert paginated test --- .../rest_api/v1/views/tests/test_home.py | 37 ------------------- cms/djangoapps/contentstore/utils.py | 2 +- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 491f1f00c76f..cd7592c46629 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -371,40 +371,3 @@ def test_home_page_libraries_response(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.json()) - - # Test paginated response - response = self.client.get(self.url + "?pagination=true") - - expected_response = { - "count": 2, - "num_pages": 1, - "results": [ - { - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib', - 'url': '/library/library-v1:org+lib', - 'org': 'org', - 'number': 'lib', - 'can_edit': True, - 'is_migrated': False, - }, - # Second legacy library was migrated so it will include - # migrated_to_title and migrated_to_key as well - { - 'display_name': 'Test Library', - 'library_key': 'library-v1:org+lib1', - 'url': '/library/library-v1:org+lib1', - 'org': 'org', - 'number': 'lib1', - 'can_edit': True, - 'is_migrated': True, - 'migrated_to_title': 'Test Library', - 'migrated_to_key': 'lib:name0:test-key', - 'migrated_to_collection_key': 'test-collection', - 'migrated_to_collection_title': 'Test Collection', - }, - ] - } - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(expected_response, response.json()) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index f432c8b79ed0..7140e7f85365 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1600,7 +1600,7 @@ def get_library_context(request, request_is_json=False): user_can_create_library, ) - libraries = set(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) + libraries = list(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else []) library_keys = [lib.location.library_key for lib in libraries] migration_info = get_migration_info(library_keys) is_migrated_filter = request.GET.get('is_migrated', None) From ec1ec348ebf7ab52e71f84ef87bc3bc4c1482507 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 25 Sep 2025 15:34:22 +0530 Subject: [PATCH 09/10] refactor: replace in_bulk as it is not supported by django 5.2 --- cms/djangoapps/modulestore_migrator/api.py | 34 ++++++------ .../modulestore_migrator/tests/test_api.py | 54 +++++++++++++++---- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/cms/djangoapps/modulestore_migrator/api.py b/cms/djangoapps/modulestore_migrator/api.py index 6b4c3d6bdc74..bbd33825efae 100644 --- a/cms/djangoapps/modulestore_migrator/api.py +++ b/cms/djangoapps/modulestore_migrator/api.py @@ -1,21 +1,19 @@ """ API for migration from modulestore to learning core """ -from opaque_keys.edx.locator import LibraryLocatorV2 +from celery.result import AsyncResult from opaque_keys.edx.keys import CourseKey, LearningContextKey -from opaque_keys.edx.locator import LibraryLocator +from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2 from openedx_learning.api.authoring import get_collection -from celery.result import AsyncResult +from user_tasks.models import UserTaskStatus from openedx.core.djangoapps.content_libraries.api import get_library from openedx.core.types.user import AuthUser -from user_tasks.models import UserTaskStatus from . import tasks from .data import RepeatHandlingStrategy from .models import ModulestoreSource - __all__ = ( "start_migration_to_library", "is_successfully_migrated", @@ -75,14 +73,18 @@ def get_migration_info(source_keys: list[CourseKey | LibraryLocator]) -> dict: """ Check if the source course/library has been migrated successfully and return target info """ - return ModulestoreSource.objects.filter( - migrations__task_status__state=UserTaskStatus.SUCCEEDED, - key__in=source_keys - ).prefetch_related('migrations__target').values_list( - 'migrations__target__key', - 'migrations__target__title', - 'migrations__target_collection__key', - 'migrations__target_collection__title', - 'key', - named=True - ).in_bulk(field_name='key') + return { + info.key: info + for info in ModulestoreSource.objects.filter( + migrations__task_status__state=UserTaskStatus.SUCCEEDED, key__in=source_keys + ) + .select_related('migrations__target') + .values_list( + 'migrations__target__key', + 'migrations__target__title', + 'migrations__target_collection__key', + 'migrations__target_collection__title', + 'key', + named=True, + ) + } diff --git a/cms/djangoapps/modulestore_migrator/tests/test_api.py b/cms/djangoapps/modulestore_migrator/tests/test_api.py index a130fcd6a7ba..6eeb84a35429 100644 --- a/cms/djangoapps/modulestore_migrator/tests/test_api.py +++ b/cms/djangoapps/modulestore_migrator/tests/test_api.py @@ -2,22 +2,22 @@ Test cases for the modulestore migrator API. """ +import pytest from opaque_keys.edx.locator import LibraryLocatorV2 from openedx_learning.api import authoring as authoring_api from organizations.tests.factories import OrganizationFactory -import pytest -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from common.djangoapps.student.tests.factories import UserFactory +from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase from cms.djangoapps.modulestore_migrator import api from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy from cms.djangoapps.modulestore_migrator.models import ModulestoreMigration from cms.djangoapps.modulestore_migrator.tests.factories import ModulestoreSourceFactory +from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.content_libraries import api as lib_api @pytest.mark.django_db -class TestModulestoreMigratorAPI(ModuleStoreTestCase): +class TestModulestoreMigratorAPI(LibraryTestCase): """ Test cases for the modulestore migrator API. """ @@ -26,16 +26,16 @@ def setUp(self): super().setUp() self.organization = OrganizationFactory() - self.lib_key = LibraryLocatorV2.from_string( + self.lib_key_v2 = LibraryLocatorV2.from_string( f"lib:{self.organization.short_name}:test-key" ) lib_api.create_library( org=self.organization, - slug=self.lib_key.slug, + slug=self.lib_key_v2.slug, title="Test Library", ) - self.library = lib_api.ContentLibrary.objects.get(slug=self.lib_key.slug) - self.learning_package = self.library.learning_package + self.library_v2 = lib_api.ContentLibrary.objects.get(slug=self.lib_key_v2.slug) + self.learning_package = self.library_v2.learning_package def test_start_migration_to_library(self): """ @@ -47,7 +47,7 @@ def test_start_migration_to_library(self): api.start_migration_to_library( user=user, source_key=source.key, - target_library_key=self.library.library_key, + target_library_key=self.library_v2.library_key, target_collection_slug=None, composition_level=CompositionLevel.Component.value, repeat_handling_strategy=RepeatHandlingStrategy.Skip.value, @@ -84,7 +84,7 @@ def test_start_migration_to_library_with_collection(self): api.start_migration_to_library( user=user, source_key=source.key, - target_library_key=self.library.library_key, + target_library_key=self.library_v2.library_key, target_collection_slug=collection_key, composition_level=CompositionLevel.Component.value, repeat_handling_strategy=RepeatHandlingStrategy.Skip.value, @@ -106,10 +106,42 @@ def test_forking_is_not_implemented(self): api.start_migration_to_library( user=user, source_key=source.key, - target_library_key=self.library.library_key, + target_library_key=self.library_v2.library_key, target_collection_slug=None, composition_level=CompositionLevel.Component.value, repeat_handling_strategy=RepeatHandlingStrategy.Fork.value, preserve_url_slugs=True, forward_source_to_target=False, ) + + def test_get_migration_info(self): + """ + Test that the API can retrieve migration info. + """ + user = UserFactory() + + collection_key = "test-collection" + authoring_api.create_collection( + learning_package_id=self.learning_package.id, + key=collection_key, + title="Test Collection", + created_by=user.id, + ) + + api.start_migration_to_library( + user=user, + source_key=self.lib_key, + target_library_key=self.library_v2.library_key, + target_collection_slug=collection_key, + composition_level=CompositionLevel.Component.value, + repeat_handling_strategy=RepeatHandlingStrategy.Skip.value, + preserve_url_slugs=True, + forward_source_to_target=True, + ) + result = api.get_migration_info([self.lib_key]) + row = result.get(self.lib_key) + assert row is not None + assert row.migrations__target__key == str(self.lib_key_v2) + assert row.migrations__target__title == "Test Library" + assert row.migrations__target_collection__key == collection_key + assert row.migrations__target_collection__title == "Test Collection" From 6f234c39a618996dd04780988b2c896e8a9b00d1 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 25 Sep 2025 15:41:12 +0530 Subject: [PATCH 10/10] fix: lint issues --- cms/djangoapps/modulestore_migrator/api.py | 1 - .../modulestore_migrator/tests/test_api.py | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/modulestore_migrator/api.py b/cms/djangoapps/modulestore_migrator/api.py index bbd33825efae..969cb9d53776 100644 --- a/cms/djangoapps/modulestore_migrator/api.py +++ b/cms/djangoapps/modulestore_migrator/api.py @@ -78,7 +78,6 @@ def get_migration_info(source_keys: list[CourseKey | LibraryLocator]) -> dict: for info in ModulestoreSource.objects.filter( migrations__task_status__state=UserTaskStatus.SUCCEEDED, key__in=source_keys ) - .select_related('migrations__target') .values_list( 'migrations__target__key', 'migrations__target__title', diff --git a/cms/djangoapps/modulestore_migrator/tests/test_api.py b/cms/djangoapps/modulestore_migrator/tests/test_api.py index 6eeb84a35429..0942bfe4adef 100644 --- a/cms/djangoapps/modulestore_migrator/tests/test_api.py +++ b/cms/djangoapps/modulestore_migrator/tests/test_api.py @@ -138,10 +138,11 @@ def test_get_migration_info(self): preserve_url_slugs=True, forward_source_to_target=True, ) - result = api.get_migration_info([self.lib_key]) - row = result.get(self.lib_key) - assert row is not None - assert row.migrations__target__key == str(self.lib_key_v2) - assert row.migrations__target__title == "Test Library" - assert row.migrations__target_collection__key == collection_key + with self.assertNumQueries(1): + result = api.get_migration_info([self.lib_key]) + row = result.get(self.lib_key) + assert row is not None + assert row.migrations__target__key == str(self.lib_key_v2) + assert row.migrations__target__title == "Test Library" + assert row.migrations__target_collection__key == collection_key assert row.migrations__target_collection__title == "Test Collection"