Skip to content

Commit f2482a2

Browse files
committed
feat: add library migration list endpoint
1 parent 7cd4170 commit f2482a2

3 files changed

Lines changed: 133 additions & 7 deletions

File tree

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

Lines changed: 63 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,63 @@ 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:
229+
return UserTaskStatus.FAILED
230+
231+
return obj.task_status.state
232+
233+
def get_progress(self, obj: ModulestoreMigration):
234+
"""
235+
Return the progress of the migration.
236+
"""
237+
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: 62 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,52 @@ 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+
context = super().get_serializer_context()
364+
queryset = self.get_queryset()
365+
course_keys = queryset.values_list('source__key', flat=True)
366+
courses = CourseOverview.get_all_courses(course_keys=course_keys)
367+
context['course_names'] = dict((str(course.id), course.display_name) for course in courses)
368+
return context
369+
370+
def get_queryset(self):
371+
"""
372+
Override the default queryset to filter by the library key.
373+
"""
374+
queryset = super().get_queryset()
375+
lib_key_str = self.kwargs['lib_key_str']
376+
try:
377+
library_key = LibraryLocatorV2.from_string(lib_key_str)
378+
except InvalidKeyError as exc:
379+
raise ParseError(detail=f"Malformed library key: {lib_key_str}") from exc
380+
lib_api.require_permission_for_library_key(
381+
library_key,
382+
self.request.user,
383+
lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY
384+
)
385+
queryset = queryset.filter(target__key=library_key, source__key__startswith='course-v1')
386+
387+
return queryset

0 commit comments

Comments
 (0)