Skip to content

Commit dfe9cb8

Browse files
authored
feat: updates legacy libraries list API to include migration info [FC-0097] (#37286)
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.
1 parent 7275ce1 commit dfe9cb8

7 files changed

Lines changed: 266 additions & 46 deletions

File tree

cms/djangoapps/contentstore/rest_api/v1/serializers/home.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
from rest_framework import serializers
66

7-
from openedx.core.lib.api.serializers import CourseKeyField
8-
97
from cms.djangoapps.contentstore.rest_api.serializers.common import CourseCommonSerializer
8+
from openedx.core.lib.api.serializers import CourseKeyField
109

1110

1211
class UnsucceededCourseSerializer(serializers.Serializer):
@@ -29,6 +28,26 @@ class LibraryViewSerializer(serializers.Serializer):
2928
org = serializers.CharField()
3029
number = serializers.CharField()
3130
can_edit = serializers.BooleanField()
31+
is_migrated = serializers.SerializerMethodField()
32+
migrated_to_title = serializers.CharField(
33+
source="migrations__target__title",
34+
required=False
35+
)
36+
migrated_to_key = serializers.CharField(
37+
source="migrations__target__key",
38+
required=False
39+
)
40+
migrated_to_collection_key = serializers.CharField(
41+
source="migrations__target_collection__key",
42+
required=False
43+
)
44+
migrated_to_collection_title = serializers.CharField(
45+
source="migrations__target_collection__title",
46+
required=False
47+
)
48+
49+
def get_is_migrated(self, obj):
50+
return "migrations__target__key" in obj
3251

3352

3453
class CourseHomeTabSerializer(serializers.Serializer):

cms/djangoapps/contentstore/rest_api/v1/views/home.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
import edx_api_doc_tools as apidocs
44
from django.conf import settings
5+
from organizations import api as org_api
56
from rest_framework.request import Request
67
from rest_framework.response import Response
78
from rest_framework.views import APIView
8-
from organizations import api as org_api
9+
910
from openedx.core.lib.api.view_utils import view_auth_classes
1011

11-
from ....utils import get_home_context, get_course_context, get_library_context
12-
from ..serializers import StudioHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer
12+
from ....utils import get_course_context, get_home_context, get_library_context
13+
from ..serializers import CourseHomeTabSerializer, LibraryTabSerializer, StudioHomeSerializer
1314

1415

1516
@view_auth_classes(is_authenticated=True)
@@ -184,7 +185,17 @@ class HomePageLibrariesView(APIView):
184185
"org",
185186
apidocs.ParameterLocation.QUERY,
186187
description="Query param to filter by course org",
187-
)],
188+
),
189+
apidocs.query_parameter(
190+
"is_migrated",
191+
bool,
192+
description=(
193+
"Query param to filter by migrated status of library."
194+
" If present (true or false), it will filter by migration status"
195+
" else it will return all legacy libraries."
196+
),
197+
)
198+
],
188199
responses={
189200
200: LibraryTabSerializer,
190201
401: "The requester is not authenticated.",
@@ -197,6 +208,13 @@ def get(self, request: Request):
197208
**Example Request**
198209
199210
GET /api/contentstore/v1/home/libraries
211+
# Returns all legacy libraries
212+
213+
GET /api/contentstore/v1/home/libraries?is_migrated=true
214+
# Returns legacy libraries that were migrated to library v2
215+
216+
GET /api/contentstore/v1/home/libraries?is_migrated=false
217+
# Returns legacy libraries that were not migrated to library v2
200218
201219
**Response Values**
202220

cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
"""
22
Unit tests for home page view.
33
"""
4-
import ddt
5-
import pytz
64
from collections import OrderedDict
75
from datetime import datetime, timedelta
6+
7+
import ddt
8+
import pytz
89
from django.conf import settings
910
from django.test import override_settings
1011
from django.urls import reverse
12+
from opaque_keys.edx.locator import LibraryLocatorV2
13+
from openedx_learning.api import authoring as authoring_api
14+
from organizations.tests.factories import OrganizationFactory
1115
from rest_framework import status
1216

13-
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
1417
from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase
18+
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
19+
from cms.djangoapps.modulestore_migrator import api as migrator_api
20+
from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy
21+
from cms.djangoapps.modulestore_migrator.tests.factories import ModulestoreSourceFactory
1522
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
23+
from openedx.core.djangoapps.content_libraries import api as lib_api
1624

1725

1826
@ddt.ddt
@@ -131,7 +139,6 @@ def test_home_page_response(self):
131139
}
132140

133141
self.assertEqual(response.status_code, status.HTTP_200_OK)
134-
print(response.data)
135142
self.assertDictEqual(expected_response, response.data)
136143

137144
def test_home_page_response_with_api_v2(self):
@@ -246,23 +253,121 @@ class HomePageLibrariesViewTest(LibraryTestCase):
246253

247254
def setUp(self):
248255
super().setUp()
256+
# Create an additional legacy library
257+
self.lib_key_1 = self._create_library(library="lib1")
258+
self.organization = OrganizationFactory()
259+
260+
# Create a new v2 library
261+
self.lib_key_v2 = LibraryLocatorV2.from_string(
262+
f"lib:{self.organization.short_name}:test-key"
263+
)
264+
lib_api.create_library(
265+
org=self.organization,
266+
slug=self.lib_key_v2.slug,
267+
title="Test Library",
268+
)
269+
library = lib_api.ContentLibrary.objects.get(slug=self.lib_key_v2.slug)
270+
learning_package = library.learning_package
271+
# Create a migration source for the legacy library
272+
self.source = ModulestoreSourceFactory(key=self.lib_key_1)
249273
self.url = reverse("cms.djangoapps.contentstore:v1:libraries")
274+
# Create a collection to migrate this library to
275+
collection_key = "test-collection"
276+
authoring_api.create_collection(
277+
learning_package_id=learning_package.id,
278+
key=collection_key,
279+
title="Test Collection",
280+
created_by=self.user.id,
281+
)
282+
283+
# Migrate self.lib_key_1 to self.lib_key_v2
284+
migrator_api.start_migration_to_library(
285+
user=self.user,
286+
source_key=self.source.key,
287+
target_library_key=self.lib_key_v2,
288+
target_collection_slug=collection_key,
289+
composition_level=CompositionLevel.Component.value,
290+
repeat_handling_strategy=RepeatHandlingStrategy.Skip.value,
291+
preserve_url_slugs=True,
292+
forward_source_to_target=False,
293+
)
250294

251295
def test_home_page_libraries_response(self):
252296
"""Check successful response content"""
253297
response = self.client.get(self.url)
254298

255299
expected_response = {
256-
"libraries": [{
257-
'display_name': 'Test Library',
258-
'library_key': 'library-v1:org+lib',
259-
'url': '/library/library-v1:org+lib',
260-
'org': 'org',
261-
'number': 'lib',
262-
'can_edit': True
263-
}],
300+
"libraries": [
301+
{
302+
'display_name': 'Test Library',
303+
'library_key': 'library-v1:org+lib',
304+
'url': '/library/library-v1:org+lib',
305+
'org': 'org',
306+
'number': 'lib',
307+
'can_edit': True,
308+
'is_migrated': False,
309+
},
310+
# Second legacy library was migrated so it will include
311+
# migrated_to_title and migrated_to_key as well
312+
{
313+
'display_name': 'Test Library',
314+
'library_key': 'library-v1:org+lib1',
315+
'url': '/library/library-v1:org+lib1',
316+
'org': 'org',
317+
'number': 'lib1',
318+
'can_edit': True,
319+
'is_migrated': True,
320+
'migrated_to_title': 'Test Library',
321+
'migrated_to_key': 'lib:name0:test-key',
322+
'migrated_to_collection_key': 'test-collection',
323+
'migrated_to_collection_title': 'Test Collection',
324+
},
325+
]
264326
}
265327

266328
self.assertEqual(response.status_code, status.HTTP_200_OK)
267-
print(response.data)
268-
self.assertDictEqual(expected_response, response.data)
329+
self.assertDictEqual(expected_response, response.json())
330+
331+
# Fetch legacy libraries that were migrated to v2
332+
response = self.client.get(self.url + '?is_migrated=true')
333+
334+
expected_response = {
335+
"libraries": [
336+
{
337+
'display_name': 'Test Library',
338+
'library_key': 'library-v1:org+lib1',
339+
'url': '/library/library-v1:org+lib1',
340+
'org': 'org',
341+
'number': 'lib1',
342+
'can_edit': True,
343+
'is_migrated': True,
344+
'migrated_to_title': 'Test Library',
345+
'migrated_to_key': 'lib:name0:test-key',
346+
'migrated_to_collection_key': 'test-collection',
347+
'migrated_to_collection_title': 'Test Collection',
348+
}
349+
],
350+
}
351+
352+
self.assertEqual(response.status_code, status.HTTP_200_OK)
353+
self.assertDictEqual(expected_response, response.json())
354+
355+
# Fetch legacy libraries that were not migrated to v2
356+
response = self.client.get(self.url + '?is_migrated=false')
357+
358+
expected_response = {
359+
"libraries": [
360+
{
361+
'display_name': 'Test Library',
362+
'library_key': 'library-v1:org+lib',
363+
'url': '/library/library-v1:org+lib',
364+
'org': 'org',
365+
'number': 'lib',
366+
'can_edit': True,
367+
'is_migrated': False,
368+
},
369+
],
370+
}
371+
372+
self.assertEqual(response.status_code, status.HTTP_200_OK)
373+
self.assertDictEqual(expected_response, response.json())

cms/djangoapps/contentstore/utils.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
from milestones import api as milestones_api
2727
from opaque_keys import InvalidKeyError
2828
from opaque_keys.edx.keys import CourseKey, UsageKey, UsageKeyV2
29-
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocator, BlockUsageLocator
29+
from opaque_keys.edx.locator import BlockUsageLocator, LibraryContainerLocator, LibraryLocator
3030
from openedx_events.content_authoring.data import DuplicatedXBlockData
3131
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
3232
from openedx_events.learning.data import CourseNotificationData
3333
from openedx_events.learning.signals import COURSE_NOTIFICATION_REQUESTED
3434
from pytz import UTC
35+
from rest_framework.fields import BooleanField
3536
from xblock.fields import Scope
3637

3738
from cms.djangoapps.contentstore.toggles import (
@@ -61,6 +62,7 @@
6162
)
6263
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
6364
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
65+
from cms.djangoapps.modulestore_migrator.api import get_migration_info
6466
from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError
6567
from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager
6668
from common.djangoapps.course_modes.models import CourseMode
@@ -87,8 +89,8 @@
8789
from common.djangoapps.xblock_django.api import deprecated_xblocks
8890
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
8991
from openedx.core import toggles as core_toggles
90-
from openedx.core.djangoapps.content_libraries.api import get_container
9192
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
93+
from openedx.core.djangoapps.content_libraries.api import get_container
9294
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
9395
from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course
9496
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND
@@ -1584,12 +1586,12 @@ def get_library_context(request, request_is_json=False):
15841586
It is used for both DRF and django views.
15851587
"""
15861588
from cms.djangoapps.contentstore.views.course import (
1589+
_accessible_libraries_iter,
1590+
_format_library_for_view,
1591+
_get_course_creator_status,
15871592
get_allowed_organizations,
15881593
get_allowed_organizations_for_libraries,
15891594
user_can_create_organizations,
1590-
_accessible_libraries_iter,
1591-
_get_course_creator_status,
1592-
_format_library_for_view,
15931595
)
15941596
from cms.djangoapps.contentstore.views.library import (
15951597
user_can_view_create_library_button,
@@ -1598,9 +1600,22 @@ def get_library_context(request, request_is_json=False):
15981600
user_can_create_library,
15991601
)
16001602

1601-
libraries = _accessible_libraries_iter(request.user) if libraries_v1_enabled() else []
1603+
libraries = list(_accessible_libraries_iter(request.user) if libraries_v1_enabled() else [])
1604+
library_keys = [lib.location.library_key for lib in libraries]
1605+
migration_info = get_migration_info(library_keys)
1606+
is_migrated_filter = request.GET.get('is_migrated', None)
16021607
data = {
1603-
'libraries': [_format_library_for_view(lib, request) for lib in libraries],
1608+
'libraries': [
1609+
_format_library_for_view(
1610+
lib,
1611+
request,
1612+
migrated_to=migration_info.get(lib.location.library_key)
1613+
)
1614+
for lib in libraries
1615+
if is_migrated_filter is None or (
1616+
BooleanField().to_internal_value(is_migrated_filter) == (lib.location.library_key in migration_info)
1617+
)
1618+
]
16041619
}
16051620

16061621
if not request_is_json:
@@ -1716,9 +1731,7 @@ def get_home_context(request, no_course=False):
17161731
get_allowed_organizations,
17171732
get_allowed_organizations_for_libraries,
17181733
user_can_create_organizations,
1719-
_accessible_libraries_iter,
17201734
_get_course_creator_status,
1721-
_format_library_for_view,
17221735
)
17231736
from cms.djangoapps.contentstore.views.library import (
17241737
user_can_view_create_library_button,

cms/djangoapps/contentstore/views/course.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import random
88
import re
99
import string
10-
from typing import Dict
10+
from typing import Dict, NamedTuple, Optional
1111

1212
import django.utils
1313
from ccx_keys.locator import CCXLocator
@@ -669,7 +669,7 @@ def library_listing(request):
669669
return render_to_response('index.html', data)
670670

671671

672-
def _format_library_for_view(library, request):
672+
def _format_library_for_view(library, request, migrated_to: Optional[NamedTuple]):
673673
"""
674674
Return a dict of the data which the view requires for each library
675675
"""
@@ -681,6 +681,7 @@ def _format_library_for_view(library, request):
681681
'org': library.display_org_with_default,
682682
'number': library.display_number_with_default,
683683
'can_edit': has_studio_write_access(request.user, library.location.library_key),
684+
**(migrated_to._asdict() if migrated_to is not None else {}),
684685
}
685686

686687

0 commit comments

Comments
 (0)