Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion cms/djangoapps/modulestore_migrator/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import LearningContextKey
from opaque_keys.edx.locator import LibraryLocatorV2
from openedx_learning.api.authoring_models import Collection
from rest_framework import serializers
from user_tasks.models import UserTaskStatus
from user_tasks.serializers import StatusSerializer

from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy
from cms.djangoapps.modulestore_migrator.models import ModulestoreMigration
from cms.djangoapps.modulestore_migrator.models import ModulestoreMigration, ModulestoreSource


class ModulestoreMigrationSerializer(serializers.Serializer):
Expand Down Expand Up @@ -173,3 +175,65 @@ def get_fields(self):
fields = super().get_fields()
fields.pop('name', None)
return fields


class LibraryMigrationCourseSourceSerializer(serializers.ModelSerializer):
"""
Serializer for the source course of a library migration.
"""
display_name = serializers.SerializerMethodField()

class Meta:
model = ModulestoreSource
fields = ['key', 'display_name']

def get_display_name(self, obj):
"""
Return the display name of the source course
"""
return self.context["course_names"].get(str(obj.key), None)


class LibraryMigrationCollectionSerializer(serializers.ModelSerializer):
"""
Serializer for the target collection of a library migration.
"""
class Meta:
model = Collection
fields = ["key", "title"]


class LibraryMigrationCourseSerializer(serializers.ModelSerializer):
"""
Serializer for the course or legacylibrary migrations to V2 library.
"""
source = LibraryMigrationCourseSourceSerializer() # type: ignore[assignment]
target_collection = LibraryMigrationCollectionSerializer(required=False)
state = serializers.SerializerMethodField()
progress = serializers.SerializerMethodField()

class Meta:
model = ModulestoreMigration
fields = [
'source',
'target_collection',
'state',
'progress',
]

def get_state(self, obj: ModulestoreMigration):
"""
Return the state of the migration.
"""
if obj.is_failed or obj.task_status.state in [UserTaskStatus.FAILED, UserTaskStatus.CANCELED]:
return UserTaskStatus.FAILED
elif obj.task_status.state == UserTaskStatus.SUCCEEDED:
return UserTaskStatus.SUCCEEDED

return UserTaskStatus.IN_PROGRESS

def get_progress(self, obj: ModulestoreMigration):
"""
Return the progress of the migration.
"""
return obj.task_status.completed_steps / obj.task_status.total_steps
9 changes: 8 additions & 1 deletion cms/djangoapps/modulestore_migrator/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
"""

from rest_framework.routers import SimpleRouter
from .views import MigrationViewSet, BulkMigrationViewSet

from .views import BulkMigrationViewSet, LibraryCourseMigrationViewSet, MigrationViewSet

ROUTER = SimpleRouter()
ROUTER.register(r'migrations', MigrationViewSet, basename='migrations')
ROUTER.register(r'bulk_migration', BulkMigrationViewSet, basename='bulk-migration')
ROUTER.register(
r'library/(?P<lib_key_str>[^/.]+)/migrations/courses',
LibraryCourseMigrationViewSet,
basename='library-migrations',
)


urlpatterns = ROUTER.urls
70 changes: 65 additions & 5 deletions cms/djangoapps/modulestore_migrator/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,30 @@
import edx_api_doc_tools as apidocs
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import LibraryLocatorV2
from rest_framework import status
from rest_framework.exceptions import ParseError
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework import status
from rest_framework.viewsets import GenericViewSet
from user_tasks.models import UserTaskStatus
from user_tasks.views import StatusViewSet

from cms.djangoapps.modulestore_migrator.api import start_migration_to_library, start_bulk_migration_to_library
from cms.djangoapps.modulestore_migrator.api import start_bulk_migration_to_library, start_migration_to_library
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content_libraries import api as lib_api
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser

from ...models import ModulestoreMigration
from .serializers import (
StatusWithModulestoreMigrationsSerializer,
ModulestoreMigrationSerializer,
BulkModulestoreMigrationSerializer,
LibraryMigrationCourseSerializer,
ModulestoreMigrationSerializer,
StatusWithModulestoreMigrationsSerializer,
)


log = logging.getLogger(__name__)


Expand Down Expand Up @@ -328,3 +336,55 @@ def cancel(self, request, *args, **kwargs):
We disable this endpoint to avoid confusion.
"""
raise NotImplementedError


@apidocs.schema_for(
"list",
"List all course migrations to a library.",
responses={
201: LibraryMigrationCourseSerializer,
401: "The requester is not authenticated.",
403: "The requester does not have permission to access the library.",
},
)
class LibraryCourseMigrationViewSet(GenericViewSet, ListModelMixin):
"""
Show infomation about migrations related to a destination library.
"""

serializer_class = LibraryMigrationCourseSerializer
pagination_class = None
queryset = ModulestoreMigration.objects.all().select_related('target_collection', 'target', 'task_status')

def get_serializer_context(self):
"""
Add course name list to the serializer context.

We need to display the course names in the migration view, and we get all of
them here to avoid futher queries.
"""
context = super().get_serializer_context()
queryset = self.get_queryset()
course_keys = queryset.values_list('source__key', flat=True)
courses = CourseOverview.get_all_courses(course_keys=course_keys)
context['course_names'] = dict((str(course.id), course.display_name) for course in courses)
return context

def get_queryset(self):
"""
Override the default queryset to filter by the library key and check permissions.
"""
queryset = super().get_queryset()
lib_key_str = self.kwargs['lib_key_str']
try:
library_key = LibraryLocatorV2.from_string(lib_key_str)
except InvalidKeyError as exc:
raise ParseError(detail=f"Malformed library key: {lib_key_str}") from exc
lib_api.require_permission_for_library_key(
library_key,
self.request.user,
lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY
)
queryset = queryset.filter(target__key=library_key, source__key__startswith='course-v1')

return queryset
Loading