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: Add Library Collections REST endpoints (temp) #671

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
5 changes: 0 additions & 5 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
from common.djangoapps.util.string_utils import _has_non_ascii_characters
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from openedx.core.djangoapps.discussions.tasks import update_discussions_settings_from_course
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangolib.js_utils import dump_js_escaped_json
Expand Down Expand Up @@ -303,10 +302,6 @@ def course_handler(request, course_key_string=None):
else:
return HttpResponseBadRequest()
elif request.method == 'GET': # assume html
# Update course discussion settings, sometimes the course discussion settings are not updated
# when the course is created, so we need to update them here.
course_key = CourseKey.from_string(course_key_string)
update_discussions_settings_from_course(course_key)
if course_key_string is None:
return redirect(reverse('home'))
else:
Expand Down
4 changes: 2 additions & 2 deletions cms/djangoapps/contentstore/views/tests/test_course_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,8 @@ def test_number_of_calls_to_db(self):
"""
Test to check number of queries made to mysql and mongo
"""
with self.assertNumQueries(32, table_ignorelist=WAFFLE_TABLES):
with check_mongo_calls(5):
with self.assertNumQueries(29, table_ignorelist=WAFFLE_TABLES):
with check_mongo_calls(3):
self.client.get_html(reverse_course_url('course_handler', self.course.id))


Expand Down
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,7 @@
'openedx_events',

# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.apps.authoring.collections",
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
Expand Down
1 change: 1 addition & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3394,6 +3394,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
'openedx_events',

# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.apps.authoring.collections",
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
Expand Down
6 changes: 5 additions & 1 deletion openedx/core/djangoapps/content_libraries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
]
Expand Down Expand Up @@ -408,6 +410,7 @@ def get_library(library_key):
license=ref.license,
created=learning_package.created,
updated=learning_package.updated,
learning_package=learning_package
)


Expand Down Expand Up @@ -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
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""
Tests Library Collections REST API views
"""

from __future__ import annotations

from openedx_learning.api.authoring_models import Collection

from openedx.core.djangolib.testing.utils import skip_unless_cms
from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest
from openedx.core.djangoapps.content_libraries.models import ContentLibrary
from common.djangoapps.student.tests.factories import UserFactory

URL_PREFIX = '/api/libraries/v2/{lib_key}/'
URL_LIB_COLLECTIONS = URL_PREFIX + 'collections/'
URL_LIB_COLLECTION = URL_LIB_COLLECTIONS + '{collection_id}/'


@skip_unless_cms # Content Library Collections REST API is only available in Studio
class ContentLibraryCollectionsViewsTest(ContentLibrariesRestApiTest):
"""
Tests for Content Library Collection REST API Views
"""

def setUp(self):
super().setUp()

# Create Content Libraries
self._create_library("test-lib-col-1", "Test Library 1")
self._create_library("test-lib-col-2", "Test Library 2")
self.lib1 = ContentLibrary.objects.get(slug="test-lib-col-1")
self.lib2 = ContentLibrary.objects.get(slug="test-lib-col-2")

print("self.lib1", self.lib1)
print("self.lib2", self.lib2)

# Create Content Library Collections
self.col1 = Collection.objects.create(
learning_package_id=self.lib1.learning_package.id,
title="Collection 1",
description="Description for Collection 1",
created_by=self.user,
)
self.col2 = Collection.objects.create(
learning_package_id=self.lib1.learning_package.id,
title="Collection 2",
description="Description for Collection 2",
created_by=self.user,
)
self.col3 = Collection.objects.create(
learning_package_id=self.lib2.learning_package.id,
title="Collection 3",
description="Description for Collection 3",
created_by=self.user,
)

def test_get_library_collection(self):
"""
Test retrieving a Content Library Collection
"""
resp = self.client.get(
URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_id=self.col3.id)
)

# Check that correct Content Library Collection data retrieved
expected_collection = {
"title": "Collection 3",
"description": "Description for Collection 3",
}
assert resp.status_code == 200
self.assertDictContainsEntries(resp.data, expected_collection)

# Check that a random user without permissions cannot access Content Library Collection
random_user = UserFactory.create(username="Random", email="[email protected]")
with self.as_user(random_user):
resp = self.client.get(
URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_id=self.col3.id)
)
assert resp.status_code == 403

def test_list_library_collections(self):
"""
Test listing Content Library Collections
"""
resp = self.client.get(URL_LIB_COLLECTIONS.format(lib_key=self.lib1.library_key))

# Check that the correct collections are listed
assert resp.status_code == 200
assert len(resp.data) == 2
expected_collections = [
{"title": "Collection 1", "description": "Description for Collection 1"},
{"title": "Collection 2", "description": "Description for Collection 2"},
]
for collection, expected in zip(resp.data, expected_collections):
self.assertDictContainsEntries(collection, expected)

# Check that a random user without permissions cannot access Content Library Collections
random_user = UserFactory.create(username="Random", email="[email protected]")
with self.as_user(random_user):
resp = self.client.get(URL_LIB_COLLECTIONS.format(lib_key=self.lib1.library_key))
assert resp.status_code == 403

def test_create_library_collection(self):
"""
Test creating a Content Library Collection
"""
post_data = {
"title": "Collection 4",
"description": "Description for Collection 4",
}
resp = self.client.post(
URL_LIB_COLLECTIONS.format(lib_key=self.lib1.library_key), post_data, format="json"
)

# Check that the new Content Library Collection is returned in response and created in DB
assert resp.status_code == 200
self.assertDictContainsEntries(resp.data, post_data)

created_collection = Collection.objects.get(id=resp.data["id"])
self.assertIsNotNone(created_collection)

# Check that user with read only access cannot create new Content Library Collection
reader = UserFactory.create(username="Reader", email="[email protected]")
self._add_user_by_email(self.lib1.library_key, reader.email, access_level="read")

with self.as_user(reader):
post_data = {
"title": "Collection 5",
"description": "Description for Collection 5",
}
resp = self.client.post(
URL_LIB_COLLECTIONS.format(lib_key=self.lib1.library_key), post_data, format="json"
)

assert resp.status_code == 403

def test_update_library_collection(self):
"""
Test updating a Content Library Collection
"""
patch_data = {
"title": "Collection 3 Updated",
}
resp = self.client.patch(
URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_id=self.col3.id),
patch_data,
format="json"
)

# Check that updated Content Library Collection is returned in response and updated in DB
assert resp.status_code == 200
self.assertDictContainsEntries(resp.data, patch_data)

created_collection = Collection.objects.get(id=resp.data["id"])
self.assertIsNotNone(created_collection)
self.assertEqual(created_collection.title, patch_data["title"])

# Check that user with read only access cannot update a Content Library Collection
reader = UserFactory.create(username="Reader", email="[email protected]")
self._add_user_by_email(self.lib1.library_key, reader.email, access_level="read")

with self.as_user(reader):
patch_data = {
"title": "Collection 3 should not update",
}
resp = self.client.patch(
URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_id=self.col3.id),
patch_data,
format="json"
)

assert resp.status_code == 403

def test_delete_library_collection(self):
"""
Test deleting a Content Library Collection

Note: Currently not implemented and should return a 405
"""
resp = self.client.delete(
URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_id=self.col3.id)
)

assert resp.status_code == 405
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading