Skip to content

Commit 5ef6be4

Browse files
authored
feat: add library migration list endpoint [FC-0112] (#37567)
This PR adds the `/api/modulestore_migrator/v1/library/:libraryId/migrations/courses/` endpoint, which returns all course migrations for a target library.
1 parent 7f8ba45 commit 5ef6be4

3 files changed

Lines changed: 138 additions & 7 deletions

File tree

cms/djangoapps/modulestore_migrator/rest_api/v1/serializers.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from opaque_keys import InvalidKeyError
66
from opaque_keys.edx.keys import LearningContextKey
77
from opaque_keys.edx.locator import LibraryLocatorV2
8+
from openedx_learning.api.authoring_models import Collection
89
from rest_framework import serializers
10+
from user_tasks.models import UserTaskStatus
911
from user_tasks.serializers import StatusSerializer
1012

1113
from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy
12-
from cms.djangoapps.modulestore_migrator.models import ModulestoreMigration
14+
from cms.djangoapps.modulestore_migrator.models import ModulestoreMigration, ModulestoreSource
1315

1416

1517
class ModulestoreMigrationSerializer(serializers.Serializer):
@@ -173,3 +175,65 @@ def get_fields(self):
173175
fields = super().get_fields()
174176
fields.pop('name', None)
175177
return fields
178+
179+
180+
class LibraryMigrationCourseSourceSerializer(serializers.ModelSerializer):
181+
"""
182+
Serializer for the source course of a library migration.
183+
"""
184+
display_name = serializers.SerializerMethodField()
185+
186+
class Meta:
187+
model = ModulestoreSource
188+
fields = ['key', 'display_name']
189+
190+
def get_display_name(self, obj):
191+
"""
192+
Return the display name of the source course
193+
"""
194+
return self.context["course_names"].get(str(obj.key), None)
195+
196+
197+
class LibraryMigrationCollectionSerializer(serializers.ModelSerializer):
198+
"""
199+
Serializer for the target collection of a library migration.
200+
"""
201+
class Meta:
202+
model = Collection
203+
fields = ["key", "title"]
204+
205+
206+
class LibraryMigrationCourseSerializer(serializers.ModelSerializer):
207+
"""
208+
Serializer for the course or legacylibrary migrations to V2 library.
209+
"""
210+
source = LibraryMigrationCourseSourceSerializer() # type: ignore[assignment]
211+
target_collection = LibraryMigrationCollectionSerializer(required=False)
212+
state = serializers.SerializerMethodField()
213+
progress = serializers.SerializerMethodField()
214+
215+
class Meta:
216+
model = ModulestoreMigration
217+
fields = [
218+
'source',
219+
'target_collection',
220+
'state',
221+
'progress',
222+
]
223+
224+
def get_state(self, obj: ModulestoreMigration):
225+
"""
226+
Return the state of the migration.
227+
"""
228+
if obj.is_failed or obj.task_status.state in [UserTaskStatus.FAILED, UserTaskStatus.CANCELED]:
229+
return UserTaskStatus.FAILED
230+
elif obj.task_status.state == UserTaskStatus.SUCCEEDED:
231+
return UserTaskStatus.SUCCEEDED
232+
233+
return UserTaskStatus.IN_PROGRESS
234+
235+
def get_progress(self, obj: ModulestoreMigration):
236+
"""
237+
Return the progress of the migration.
238+
"""
239+
return obj.task_status.completed_steps / obj.task_status.total_steps

cms/djangoapps/modulestore_migrator/rest_api/v1/urls.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33
"""
44

55
from rest_framework.routers import SimpleRouter
6-
from .views import MigrationViewSet, BulkMigrationViewSet
6+
7+
from .views import BulkMigrationViewSet, LibraryCourseMigrationViewSet, MigrationViewSet
78

89
ROUTER = SimpleRouter()
910
ROUTER.register(r'migrations', MigrationViewSet, basename='migrations')
1011
ROUTER.register(r'bulk_migration', BulkMigrationViewSet, basename='bulk-migration')
12+
ROUTER.register(
13+
r'library/(?P<lib_key_str>[^/.]+)/migrations/courses',
14+
LibraryCourseMigrationViewSet,
15+
basename='library-migrations',
16+
)
17+
1118

1219
urlpatterns = ROUTER.urls

cms/djangoapps/modulestore_migrator/rest_api/v1/views.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,30 @@
66
import edx_api_doc_tools as apidocs
77
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
88
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
9+
from opaque_keys import InvalidKeyError
10+
from opaque_keys.edx.locator import LibraryLocatorV2
11+
from rest_framework import status
12+
from rest_framework.exceptions import ParseError
13+
from rest_framework.mixins import ListModelMixin
914
from rest_framework.permissions import IsAdminUser
1015
from rest_framework.response import Response
11-
from rest_framework import status
16+
from rest_framework.viewsets import GenericViewSet
1217
from user_tasks.models import UserTaskStatus
1318
from user_tasks.views import StatusViewSet
1419

15-
from cms.djangoapps.modulestore_migrator.api import start_migration_to_library, start_bulk_migration_to_library
20+
from cms.djangoapps.modulestore_migrator.api import start_bulk_migration_to_library, start_migration_to_library
21+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
22+
from openedx.core.djangoapps.content_libraries import api as lib_api
1623
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
1724

25+
from ...models import ModulestoreMigration
1826
from .serializers import (
19-
StatusWithModulestoreMigrationsSerializer,
20-
ModulestoreMigrationSerializer,
2127
BulkModulestoreMigrationSerializer,
28+
LibraryMigrationCourseSerializer,
29+
ModulestoreMigrationSerializer,
30+
StatusWithModulestoreMigrationsSerializer,
2231
)
2332

24-
2533
log = logging.getLogger(__name__)
2634

2735

@@ -328,3 +336,55 @@ def cancel(self, request, *args, **kwargs):
328336
We disable this endpoint to avoid confusion.
329337
"""
330338
raise NotImplementedError
339+
340+
341+
@apidocs.schema_for(
342+
"list",
343+
"List all course migrations to a library.",
344+
responses={
345+
201: LibraryMigrationCourseSerializer,
346+
401: "The requester is not authenticated.",
347+
403: "The requester does not have permission to access the library.",
348+
},
349+
)
350+
class LibraryCourseMigrationViewSet(GenericViewSet, ListModelMixin):
351+
"""
352+
Show infomation about migrations related to a destination library.
353+
"""
354+
355+
serializer_class = LibraryMigrationCourseSerializer
356+
pagination_class = None
357+
queryset = ModulestoreMigration.objects.all().select_related('target_collection', 'target', 'task_status')
358+
359+
def get_serializer_context(self):
360+
"""
361+
Add course name list to the serializer context.
362+
363+
We need to display the course names in the migration view, and we get all of
364+
them here to avoid futher queries.
365+
"""
366+
context = super().get_serializer_context()
367+
queryset = self.get_queryset()
368+
course_keys = queryset.values_list('source__key', flat=True)
369+
courses = CourseOverview.get_all_courses(course_keys=course_keys)
370+
context['course_names'] = dict((str(course.id), course.display_name) for course in courses)
371+
return context
372+
373+
def get_queryset(self):
374+
"""
375+
Override the default queryset to filter by the library key and check permissions.
376+
"""
377+
queryset = super().get_queryset()
378+
lib_key_str = self.kwargs['lib_key_str']
379+
try:
380+
library_key = LibraryLocatorV2.from_string(lib_key_str)
381+
except InvalidKeyError as exc:
382+
raise ParseError(detail=f"Malformed library key: {lib_key_str}") from exc
383+
lib_api.require_permission_for_library_key(
384+
library_key,
385+
self.request.user,
386+
lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY
387+
)
388+
queryset = queryset.filter(target__key=library_key, source__key__startswith='course-v1')
389+
390+
return queryset

0 commit comments

Comments
 (0)