Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: REST API for create Library Collection [FC-0062] #35447

Closed
Closed
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
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/content_libraries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class ContentLibraryMetadata:
key = attr.ib(type=LibraryLocatorV2)
title = attr.ib("")
description = attr.ib("")
learning_package_id = attr.ib(default=None, type=int)
num_blocks = attr.ib(0)
version = attr.ib(0)
type = attr.ib(default=COMPLEX)
Expand Down Expand Up @@ -392,6 +393,7 @@ def get_library(library_key):
return ContentLibraryMetadata(
key=library_key,
title=learning_package.title,
learning_package_id=learning_package.id,
type=ref.type,
description=ref.learning_package.description,
num_blocks=num_blocks,
Expand Down
22 changes: 22 additions & 0 deletions openedx/core/djangoapps/content_libraries/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,25 @@ class ContentLibraryBlockImportTaskCreateSerializer(serializers.Serializer):
"""

course_key = CourseKeyField()


class LibraryCollectionCreationSerializer(serializers.Serializer):
"""
Serializer to create a new library collection.
"""

title = serializers.CharField()
description = serializers.CharField(allow_blank=True)


class LibraryCollectionMetadataSerializer(serializers.Serializer):
"""
Serializer for Library Collection Metadata.
"""

# TODO Set this "id" with the LibraryCollectionKey
id = serializers.CharField(read_only=True)
# Rename collection.key to "slug" because "key" is a reserved prop name in React
slug = serializers.CharField(source="key", read_only=True)
title = serializers.CharField()
description = serializers.CharField(allow_blank=True)
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from openedx_events.tests.utils import OpenEdxEventsTestMixin
from openedx.core.djangoapps.content_libraries.tests.base import (
ContentLibrariesRestApiTest,
URL_LIB_DETAIL,
URL_BLOCK_METADATA_URL,
URL_BLOCK_RENDER_VIEW,
URL_BLOCK_GET_HANDLER_URL,
Expand Down Expand Up @@ -1063,3 +1064,55 @@ def test_not_found_fails_correctly(self):
self.assertEqual(response.json(), {
'detail': f"XBlock {valid_not_found_key} does not exist, or you don't have permission to view it.",
})


class LibraryCollectionsTestCase(ContentLibrariesRestApiTest):

def test_create_collection(self):
"""
Test collection creation

Tests with some non-ASCII chars in title, description.
"""
url = URL_LIB_DETAIL + 'collections/'
lib = self._create_library(
slug="téstlꜟط", title="A Tést Lꜟطrary", description="Just Téstꜟng", license_type=CC_4_BY,
)
collection = self._api("post", url.format(lib_key=lib["id"]), {
'title': 'A Tést Lꜟطrary Collection',
'description': 'Just Téstꜟng',
}, 200)
expected_data = {
'id': '1',
'slug': 'a-test-lrary-collection',
'title': 'A Tést Lꜟطrary Collection',
'description': 'Just Téstꜟng'
}
self.assertDictContainsEntries(collection, expected_data)

def test_create_collection_same_key(self):
"""
Test collection creation with same key
"""
url = URL_LIB_DETAIL + 'collections/'
lib = self._create_library(
slug="téstlꜟط", title="A Tést Lꜟطrary", description="Just Téstꜟng", license_type=CC_4_BY,
)

self._api("post", url.format(lib_key=lib["id"]), {
'title': 'Test Collection',
'description': 'Just a Test',
}, 200)

for i in range(0, 100):
collection = self._api("post", url.format(lib_key=lib["id"]), {
'title': 'Test Collection',
'description': 'Just a Test',
}, 200)
expected_data = {
'id': collection['id'],
'slug': f'test-collection-{i + 1}',
'title': 'Test Collection',
'description': 'Just a Test'
}
self.assertDictContainsEntries(collection, expected_data)
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/content_libraries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
path('import_blocks/', include(import_blocks_router.urls)),
# Paste contents of clipboard into library
path('paste_clipboard/', views.LibraryPasteClipboardView.as_view()),
# list of library collections / create a library collection
path('collections/', views.LibraryCollectionsRootView.as_view()),
])),
path('blocks/<str:usage_key_str>/', include([
# Get metadata about a specific XBlock in this library, or delete the block:
Expand Down
65 changes: 65 additions & 0 deletions openedx/core/djangoapps/content_libraries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
from django.contrib.auth import authenticate, get_user_model, login
from django.contrib.auth.models import Group
from django.db.transaction import atomic, non_atomic_requests
from django.db.utils import IntegrityError
from django.http import Http404, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
Expand All @@ -80,6 +81,7 @@
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateResponseMixin, View
from django.utils.text import slugify
from pylti1p3.contrib.django import DjangoCacheDataStorage, DjangoDbToolConf, DjangoMessageLaunch, DjangoOIDCLogin
from pylti1p3.exception import LtiException, OIDCException

Expand All @@ -88,13 +90,16 @@
from organizations.api import ensure_organization
from organizations.exceptions import InvalidOrganizationException
from organizations.models import Organization
from openedx_learning.api import authoring as authoring_api
from rest_framework import status
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.generics import GenericAPIView
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet
from openedx_events.content_authoring.data import LibraryCollectionData
from openedx_events.content_authoring.signals import LIBRARY_COLLECTION_CREATED

from openedx.core.djangoapps.content_libraries import api, permissions
from openedx.core.djangoapps.content_libraries.serializers import (
Expand All @@ -113,6 +118,8 @@
LibraryXBlockStaticFilesSerializer,
ContentLibraryAddPermissionByEmailSerializer,
LibraryPasteClipboardSerializer,
LibraryCollectionCreationSerializer,
LibraryCollectionMetadataSerializer,
)
import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers
from openedx.core.lib.api.view_utils import view_auth_classes
Expand Down Expand Up @@ -154,6 +161,10 @@ def wrapped_fn(*args, **kwargs):
return wrapped_fn


# Library Views
# =============


class LibraryApiPaginationDocs:
"""
API docs for query params related to paginating ContentLibraryMetadata objects.
Expand Down Expand Up @@ -829,6 +840,60 @@ def retrieve(self, request, lib_key_str, pk=None):
return Response(ContentLibraryBlockImportTaskSerializer(import_task).data)


# Library Collections Views
# =============

@method_decorator(non_atomic_requests, name="dispatch")
@view_auth_classes()
class LibraryCollectionsRootView(GenericAPIView):
"""
Views to list and create library collections.
"""

# TODO Implement list collections

def post(self, request, lib_key_str):
"""
Create a new collection 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)
serializer = LibraryCollectionCreationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

library = api.get_library(library_key)
title = serializer.validated_data['title']

key = slugify(title)

attempt = 0
result = None

# It's possible that the key is not unique in the database
# So to avoid that, we add a correlative number in the key
while not result:
modified_key = key if attempt == 0 else key + '-' + str(attempt)
try:
result = authoring_api.create_collection(
learning_package_id=library.learning_package_id,
key=modified_key,
title=title,
description=serializer.validated_data['description'],
created_by=request.user.id,
)
except IntegrityError:
attempt += 1

LIBRARY_COLLECTION_CREATED.send_event(
library_collection=LibraryCollectionData(
library_key=library_key,
collection_key=result.id,
)
)

return Response(LibraryCollectionMetadataSerializer(result).data)


# LTI 1.3 Views
# =============

Expand Down
2 changes: 1 addition & 1 deletion requirements/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ libsass==0.10.0
click==8.1.6

# pinning this version to avoid updates while the library is being developed
openedx-learning==0.11.2
openedx-learning==0.11.4

# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
openai<=0.28.1
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ openedx-django-require==2.1.0
# via -r requirements/edx/kernel.in
openedx-django-wiki==2.1.0
# via -r requirements/edx/kernel.in
openedx-events==9.12.0
openedx-events==9.14.0
# via
# -r requirements/edx/kernel.in
# edx-enterprise
Expand All @@ -824,7 +824,7 @@ openedx-filters==1.9.0
# -r requirements/edx/kernel.in
# lti-consumer-xblock
# ora2
openedx-learning==0.11.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@jill/collection-key
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@ openedx-django-wiki==2.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
openedx-events==9.12.0
openedx-events==9.14.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
Expand All @@ -1373,7 +1373,7 @@ openedx-filters==1.9.0
# -r requirements/edx/testing.txt
# lti-consumer-xblock
# ora2
openedx-learning==0.11.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@jill/collection-key
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ openedx-django-require==2.1.0
# via -r requirements/edx/base.txt
openedx-django-wiki==2.1.0
# via -r requirements/edx/base.txt
openedx-events==9.12.0
openedx-events==9.14.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
Expand All @@ -983,7 +983,7 @@ openedx-filters==1.9.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
openedx-learning==0.11.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@jill/collection-key
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/kernel.in
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ olxcleaner
openedx-atlas # CLI tool to manage translations
openedx-calc # Library supporting mathematical calculations for Open edX
openedx-django-require
openedx-events # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-events>=9.14.0 # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)
openedx-learning # Open edX Learning core (experimental)
git+https://github.com/open-craft/openedx-learning.git@jill/collection-key#egg=openedx-learning # Open edX Learning core (experimental)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Bump version

openedx-mongodbproxy
openedx-django-wiki
path
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,7 @@ openedx-django-require==2.1.0
# via -r requirements/edx/base.txt
openedx-django-wiki==2.1.0
# via -r requirements/edx/base.txt
openedx-events==9.12.0
openedx-events==9.14.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
Expand All @@ -1034,7 +1034,7 @@ openedx-filters==1.9.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
openedx-learning==0.11.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@jill/collection-key
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
Expand Down
Loading