From 8431e0807af793108d9e36706372bdeb53b42a94 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Wed, 14 Aug 2024 07:36:15 +0300 Subject: [PATCH] feat: Add Library Collections REST endpoints --- .../core/djangoapps/content_libraries/api.py | 6 +- .../collections/rest_api/v1/views.py | 110 ++++++++++++++++++ .../content_libraries/serializers.py | 21 ++++ .../core/djangoapps/content_libraries/urls.py | 6 + 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 openedx/core/djangoapps/content_libraries/collections/rest_api/v1/views.py diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index d2c6eeeeabd9..e9e7b990edd1 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -86,7 +86,7 @@ LIBRARY_BLOCK_UPDATED, ) from openedx_learning.api import authoring as authoring_api -from openedx_learning.api.authoring_models import Component, MediaType +from openedx_learning.api.authoring_models import Component, MediaType, LearningPackage from organizations.models import Organization from xblock.core import XBlock from xblock.exceptions import XBlockNotFoundError @@ -150,6 +150,7 @@ class ContentLibraryMetadata: Class that represents the metadata about a content library. """ key = attr.ib(type=LibraryLocatorV2) + learning_package = attr.ib(type=LearningPackage) title = attr.ib("") description = attr.ib("") num_blocks = attr.ib(0) @@ -323,6 +324,7 @@ def get_metadata(queryset, text_search=None): has_unpublished_changes=False, has_unpublished_deletes=False, license=lib.license, + learning_package=lib.learning_package, ) for lib in queryset ] @@ -408,6 +410,7 @@ def get_library(library_key): license=ref.license, created=learning_package.created, updated=learning_package.updated, + learning_package=learning_package ) @@ -479,6 +482,7 @@ def create_library( allow_public_learning=ref.allow_public_learning, allow_public_read=ref.allow_public_read, license=library_license, + learning_package=ref.learning_package ) diff --git a/openedx/core/djangoapps/content_libraries/collections/rest_api/v1/views.py b/openedx/core/djangoapps/content_libraries/collections/rest_api/v1/views.py new file mode 100644 index 000000000000..9fefa29e6170 --- /dev/null +++ b/openedx/core/djangoapps/content_libraries/collections/rest_api/v1/views.py @@ -0,0 +1,110 @@ +""" +Collections API Views +""" + +from __future__ import annotations + +from django.http import Http404 + +# from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework.status import HTTP_405_METHOD_NOT_ALLOWED + +from opaque_keys.edx.locator import LibraryLocatorV2 + +from openedx.core.djangoapps.content_libraries import api, permissions +from openedx.core.djangoapps.content_libraries.serializers import ( + ContentLibraryCollectionSerializer, + ContentLibraryCollectionCreateOrUpdateSerializer, +) + +from openedx_learning.api.authoring_models import Collection +from openedx_learning.api import authoring as authoring_api + + +class LibraryCollectionsView(ModelViewSet): + """ + Views to get, create and update Library Collections. + """ + + serializer_class = ContentLibraryCollectionSerializer + + def retrieve(self, request, lib_key_str, pk=None): + """ + Retrieve the Content Library Collection + """ + try: + collection = authoring_api.get_collection(pk) + except Collection.DoesNotExist as exc: + raise Http404 from exc + + # Check if user has permissions to view this collection by checking if + # user has permission to view the Content Library it belongs to + library_key = LibraryLocatorV2.from_string(lib_key_str) + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY) + serializer = self.get_serializer(collection) + return Response(serializer.data) + + def list(self, request, lib_key_str): + """ + List Collections that belong to Content Library + """ + # Check if user has permissions to view collections by checking if user + # has permission to view the Content Library they belong to + library_key = LibraryLocatorV2.from_string(lib_key_str) + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY) + content_library = api.get_library(library_key) + collections = authoring_api.get_learning_package_collections(content_library.learning_package.id) + serializer = self.get_serializer(collections, many=True) + return Response(serializer.data) + + def create(self, request, lib_key_str): + """ + Create a Collection that belongs to a Content Library + """ + # Check if user has permissions to create a collection in the Content Library + # by checking if user has permission to edit the Content Library + library_key = LibraryLocatorV2.from_string(lib_key_str) + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) + create_serializer = ContentLibraryCollectionCreateOrUpdateSerializer(data=request.data) + create_serializer.is_valid(raise_exception=True) + content_library = api.get_library(library_key) + collection = authoring_api.create_collection( + content_library.learning_package.id, + create_serializer.validated_data["title"], + request.user.id, + create_serializer.validated_data["description"] + ) + serializer = self.get_serializer(collection) + return Response(serializer.data) + + def partial_update(self, request, lib_key_str, pk=None): + """ + Update a Collection that belongs to a Content Library + """ + # Check if user has permissions to update a collection in the Content Library + # by checking if user has permission to edit the Content Library + library_key = LibraryLocatorV2.from_string(lib_key_str) + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) + + try: + collection = authoring_api.get_collection(pk) + except Collection.DoesNotExist as exc: + raise Http404 from exc + + update_serializer = ContentLibraryCollectionCreateOrUpdateSerializer( + collection, data=request.data, partial=True + ) + update_serializer.is_valid(raise_exception=True) + updated_collection = authoring_api.update_collection(pk, **update_serializer.validated_data) + serializer = self.get_serializer(updated_collection) + return Response(serializer.data) + + def destroy(self, request, lib_key_str, pk=None): + """ + Deletes a Collection that belongs to a Content Library + + Note: (currently not allowed) + """ + return Response(None, status=HTTP_405_METHOD_NOT_ALLOWED) diff --git a/openedx/core/djangoapps/content_libraries/serializers.py b/openedx/core/djangoapps/content_libraries/serializers.py index e714718a77b5..ed24c00c8955 100644 --- a/openedx/core/djangoapps/content_libraries/serializers.py +++ b/openedx/core/djangoapps/content_libraries/serializers.py @@ -5,6 +5,8 @@ from django.core.validators import validate_unicode_slug from rest_framework import serializers + +from openedx_learning.api.authoring_models import Collection from openedx.core.djangoapps.content_libraries.constants import ( LIBRARY_TYPES, COMPLEX, @@ -234,3 +236,22 @@ class ContentLibraryBlockImportTaskCreateSerializer(serializers.Serializer): """ course_key = CourseKeyField() + + +class ContentLibraryCollectionSerializer(serializers.ModelSerializer): + """ + Serializer for a Content Library Collection + """ + + class Meta: + model = Collection + fields = '__all__' + + +class ContentLibraryCollectionCreateOrUpdateSerializer(serializers.Serializer): + """ + Serializer for add/update a Collection in a Content Library + """ + + title = serializers.CharField() + description = serializers.CharField() diff --git a/openedx/core/djangoapps/content_libraries/urls.py b/openedx/core/djangoapps/content_libraries/urls.py index 022c288e363f..d1fab5298125 100644 --- a/openedx/core/djangoapps/content_libraries/urls.py +++ b/openedx/core/djangoapps/content_libraries/urls.py @@ -7,6 +7,7 @@ from rest_framework import routers from . import views +from .collections.rest_api.v1 import views as collection_views # Django application name. @@ -18,6 +19,9 @@ import_blocks_router = routers.DefaultRouter() import_blocks_router.register(r'tasks', views.LibraryImportTaskViewSet, basename='import-block-task') +library_collections_router = routers.DefaultRouter() +library_collections_router.register(r'collections', collection_views.LibraryCollectionsView, basename="library-collections") + # These URLs are only used in Studio. The LMS already provides all the # API endpoints needed to serve XBlocks from content libraries using the # standard XBlock REST API (see openedx.core.django_apps.xblock.rest_api.urls) @@ -43,6 +47,8 @@ path('team/group//', views.LibraryTeamGroupView.as_view()), # Import blocks into this library. path('import_blocks/', include(import_blocks_router.urls)), + # Library Collections + path('', include(library_collections_router.urls)), ])), path('blocks//', include([ # Get metadata about a specific XBlock in this library, or delete the block: