Merge pull request #23233 from open-craft/blockstore-library-links
REST API for managing Content Library bundle links
This commit is contained in:
@@ -1,8 +1,39 @@
|
||||
"""
|
||||
Python API for content libraries.
|
||||
|
||||
Unless otherwise specified, all APIs in this file deal with the DRAFT version
|
||||
of the content library.
|
||||
Via 'views.py', most of these API methods are also exposed as a REST API.
|
||||
|
||||
The API methods in this file are focused on authoring and specific to content
|
||||
libraries; they wouldn't necessarily apply or work in other learning contexts
|
||||
such as courses, blogs, "pathways," etc.
|
||||
|
||||
** As this is an authoring-focused API, all API methods in this file deal with
|
||||
the DRAFT version of the content library. **
|
||||
|
||||
Some of these methods will work and may be used from the LMS if needed (mostly
|
||||
for test setup; other use is discouraged), but some of the implementation
|
||||
details rely on Studio so other methods will raise errors if called from the
|
||||
LMS. (The REST API is not available at all from the LMS.)
|
||||
|
||||
Any APIs that use/affect content libraries but are generic enough to work in
|
||||
other learning contexts too are in the core XBlock python/REST API at
|
||||
openedx.core.djangoapps.xblock.api/rest_api
|
||||
|
||||
For example, to render a content library XBlock as HTML, one can use the generic
|
||||
render_block_view(block, view_name, user)
|
||||
API in openedx.core.djangoapps.xblock.api (use it from Studio for the draft
|
||||
version, from the LMS for published version).
|
||||
|
||||
There are one or two methods in this file that have some overlap with the core
|
||||
XBlock API; for example, this content library API provides a get_library_block()
|
||||
which returns metadata about an XBlock; it's in this API because it also returns
|
||||
data about whether or not the XBlock has unpublished edits, which is an
|
||||
authoring-only concern. Likewise, APIs for getting/setting an individual
|
||||
XBlock's OLX directly seem more appropriate for small, reusable components in
|
||||
content libraries and may not be appropriate for other learning contexts so they
|
||||
are implemented here in the library API only. In the future, if we find a need
|
||||
for these in most other learning contexts then those methods could be promoted
|
||||
to the core XBlock API and made generic.
|
||||
"""
|
||||
from uuid import UUID
|
||||
import logging
|
||||
@@ -13,6 +44,7 @@ from django.core.exceptions import PermissionDenied
|
||||
from django.core.validators import validate_unicode_slug
|
||||
from django.db import IntegrityError
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.keys import LearningContextKey
|
||||
from opaque_keys.edx.locator import BundleDefinitionLocator, LibraryLocatorV2, LibraryUsageLocatorV2
|
||||
from organizations.models import Organization
|
||||
import six
|
||||
@@ -34,6 +66,7 @@ from openedx.core.lib.blockstore_api import (
|
||||
update_bundle,
|
||||
delete_bundle,
|
||||
write_draft_file,
|
||||
set_draft_link,
|
||||
commit_draft,
|
||||
delete_draft,
|
||||
)
|
||||
@@ -69,7 +102,7 @@ class InvalidNameError(ValueError):
|
||||
# Models:
|
||||
|
||||
@attr.s
|
||||
class ContentLibraryMetadata(object):
|
||||
class ContentLibraryMetadata:
|
||||
"""
|
||||
Class that represents the metadata about a content library.
|
||||
"""
|
||||
@@ -90,7 +123,7 @@ class ContentLibraryMetadata(object):
|
||||
allow_public_read = attr.ib(False)
|
||||
|
||||
|
||||
class AccessLevel(object):
|
||||
class AccessLevel:
|
||||
""" Enum defining library access levels/permissions """
|
||||
ADMIN_LEVEL = ContentLibraryPermission.ADMIN_LEVEL
|
||||
AUTHOR_LEVEL = ContentLibraryPermission.AUTHOR_LEVEL
|
||||
@@ -99,7 +132,7 @@ class AccessLevel(object):
|
||||
|
||||
|
||||
@attr.s
|
||||
class ContentLibraryPermissionEntry(object):
|
||||
class ContentLibraryPermissionEntry:
|
||||
"""
|
||||
A user or group granted permission to use a content library.
|
||||
"""
|
||||
@@ -109,7 +142,7 @@ class ContentLibraryPermissionEntry(object):
|
||||
|
||||
|
||||
@attr.s
|
||||
class LibraryXBlockMetadata(object):
|
||||
class LibraryXBlockMetadata:
|
||||
"""
|
||||
Class that represents the metadata about an XBlock in a content library.
|
||||
"""
|
||||
@@ -120,7 +153,7 @@ class LibraryXBlockMetadata(object):
|
||||
|
||||
|
||||
@attr.s
|
||||
class LibraryXBlockStaticFile(object):
|
||||
class LibraryXBlockStaticFile:
|
||||
"""
|
||||
Class that represents a static file in a content library, associated with
|
||||
a particular XBlock.
|
||||
@@ -135,7 +168,7 @@ class LibraryXBlockStaticFile(object):
|
||||
|
||||
|
||||
@attr.s
|
||||
class LibraryXBlockType(object):
|
||||
class LibraryXBlockType:
|
||||
"""
|
||||
An XBlock type that can be added to a content library
|
||||
"""
|
||||
@@ -143,6 +176,33 @@ class LibraryXBlockType(object):
|
||||
display_name = attr.ib("")
|
||||
|
||||
|
||||
@attr.s
|
||||
class LibraryBundleLink:
|
||||
"""
|
||||
A link from a content library blockstore bundle to another blockstore bundle
|
||||
"""
|
||||
# Bundle that is linked to
|
||||
bundle_uuid = attr.ib(type=UUID)
|
||||
# Link name (slug)
|
||||
id = attr.ib("")
|
||||
# What version of this bundle we are currently linking to.
|
||||
version = attr.ib(0)
|
||||
# What the latest version of the linked bundle is:
|
||||
# (if latest_version > version), the link can be "updated" to the latest version.
|
||||
latest_version = attr.ib(0)
|
||||
# Opaque key: If the linked bundle is a library or other learning context whose opaque key we can deduce, then this
|
||||
# is the key. If we don't know what type of blockstore bundle this link is pointing to, then this is blank.
|
||||
opaque_key = attr.ib(type=LearningContextKey, default=None)
|
||||
|
||||
|
||||
class AccessLevel:
|
||||
""" Enum defining library access levels/permissions """
|
||||
ADMIN_LEVEL = ContentLibraryPermission.ADMIN_LEVEL
|
||||
AUTHOR_LEVEL = ContentLibraryPermission.AUTHOR_LEVEL
|
||||
READ_LEVEL = ContentLibraryPermission.READ_LEVEL
|
||||
NO_ACCESS = None
|
||||
|
||||
|
||||
def list_libraries_for_user(user):
|
||||
"""
|
||||
Lists up to 50 content libraries that the user has permission to view.
|
||||
@@ -643,6 +703,93 @@ def get_allowed_block_types(library_key): # pylint: disable=unused-argument
|
||||
return info
|
||||
|
||||
|
||||
def get_bundle_links(library_key):
|
||||
"""
|
||||
Get the list of bundles/libraries linked to this content library.
|
||||
|
||||
Returns LibraryBundleLink objects (defined above).
|
||||
|
||||
Because every content library is a blockstore bundle, it can have "links" to
|
||||
other bundles, which may or may not be content libraries. This allows using
|
||||
XBlocks (or perhaps even static assets etc.) from another bundle without
|
||||
needing to duplicate/copy the data.
|
||||
|
||||
Links always point to a specific published version of the target bundle.
|
||||
Links are identified by a slug-like ID, e.g. "link1"
|
||||
"""
|
||||
ref = ContentLibrary.objects.get_by_key(library_key)
|
||||
links = blockstore_cache.get_bundle_draft_direct_links_cached(ref.bundle_uuid, DRAFT_NAME)
|
||||
results = []
|
||||
# To be able to quickly get the library ID from the bundle ID for links which point to other libraries, build a map:
|
||||
bundle_uuids = set(link_data.bundle_uuid for link_data in links.values())
|
||||
libraries_linked = {
|
||||
lib.bundle_uuid: lib
|
||||
for lib in ContentLibrary.objects.select_related('org').filter(bundle_uuid__in=bundle_uuids)
|
||||
}
|
||||
for link_name, link_data in links.items():
|
||||
# Is this linked bundle a content library?
|
||||
try:
|
||||
opaque_key = libraries_linked[link_data.bundle_uuid].library_key
|
||||
except KeyError:
|
||||
opaque_key = None
|
||||
# Append the link information:
|
||||
results.append(LibraryBundleLink(
|
||||
id=link_name,
|
||||
bundle_uuid=link_data.bundle_uuid,
|
||||
version=link_data.version,
|
||||
latest_version=blockstore_cache.get_bundle_version_number(link_data.bundle_uuid),
|
||||
opaque_key=opaque_key,
|
||||
))
|
||||
return results
|
||||
|
||||
|
||||
def create_bundle_link(library_key, link_id, target_opaque_key, version=None):
|
||||
"""
|
||||
Create a new link to the resource with the specified opaque key.
|
||||
|
||||
For now, only LibraryLocatorV2 opaque keys are supported.
|
||||
"""
|
||||
ref = ContentLibrary.objects.get_by_key(library_key)
|
||||
# Make sure this link ID/name is not already in use:
|
||||
links = blockstore_cache.get_bundle_draft_direct_links_cached(ref.bundle_uuid, DRAFT_NAME)
|
||||
if link_id in links:
|
||||
raise InvalidNameError("That link ID is already in use.")
|
||||
# Determine the target:
|
||||
if not isinstance(target_opaque_key, LibraryLocatorV2):
|
||||
raise TypeError("For now, only LibraryLocatorV2 opaque keys are supported by create_bundle_link")
|
||||
target_bundle_uuid = ContentLibrary.objects.get_by_key(target_opaque_key).bundle_uuid
|
||||
if version is None:
|
||||
version = get_bundle(target_bundle_uuid).latest_version
|
||||
# Create the new link:
|
||||
draft = get_or_create_bundle_draft(ref.bundle_uuid, DRAFT_NAME)
|
||||
set_draft_link(draft.uuid, link_id, target_bundle_uuid, version)
|
||||
# Clear the cache:
|
||||
LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
|
||||
|
||||
|
||||
def update_bundle_link(library_key, link_id, version=None, delete=False):
|
||||
"""
|
||||
Update a bundle's link to point to the specified version of its target
|
||||
bundle. Use version=None to automatically point to the latest version.
|
||||
Use delete=True to delete the link.
|
||||
"""
|
||||
ref = ContentLibrary.objects.get_by_key(library_key)
|
||||
draft = get_or_create_bundle_draft(ref.bundle_uuid, DRAFT_NAME)
|
||||
if delete:
|
||||
set_draft_link(draft.uuid, link_id, None, None)
|
||||
else:
|
||||
links = blockstore_cache.get_bundle_draft_direct_links_cached(ref.bundle_uuid, DRAFT_NAME)
|
||||
try:
|
||||
link = links[link_id]
|
||||
except KeyError:
|
||||
raise InvalidNameError("That link does not exist.")
|
||||
if version is None:
|
||||
version = get_bundle(link.bundle_uuid).latest_version
|
||||
set_draft_link(draft.uuid, link_id, link.bundle_uuid, version)
|
||||
# Clear the cache:
|
||||
LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
|
||||
|
||||
|
||||
def publish_changes(library_key):
|
||||
"""
|
||||
Publish all pending changes to the specified library.
|
||||
|
||||
@@ -18,6 +18,7 @@ from openedx.core.djangoapps.xblock.runtime.olx_parsing import (
|
||||
)
|
||||
from openedx.core.djangolib.blockstore_cache import (
|
||||
BundleCache,
|
||||
get_bundle_direct_links_with_cache,
|
||||
get_bundle_files_cached,
|
||||
get_bundle_file_metadata_with_cache,
|
||||
get_bundle_version_number,
|
||||
@@ -299,6 +300,12 @@ class LibraryBundle(object):
|
||||
has_unpublished_changes = True
|
||||
break
|
||||
|
||||
if not has_unpublished_changes:
|
||||
# Check if any links have changed:
|
||||
old_links = set(get_bundle_direct_links_with_cache(self.bundle_uuid).items())
|
||||
new_links = set(get_bundle_direct_links_with_cache(self.bundle_uuid, draft_name=self.draft_name).items())
|
||||
has_unpublished_changes = new_links != old_links
|
||||
|
||||
published_file_paths = set(f.path for f in get_bundle_files_cached(self.bundle_uuid))
|
||||
draft_file_paths = set(f.path for f in draft_files)
|
||||
for file_path in published_file_paths:
|
||||
|
||||
@@ -84,6 +84,32 @@ class LibraryXBlockTypeSerializer(serializers.Serializer):
|
||||
display_name = serializers.CharField()
|
||||
|
||||
|
||||
class LibraryBundleLinkSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a link from a content library blockstore bundle to another
|
||||
blockstore bundle.
|
||||
"""
|
||||
id = serializers.SlugField() # Link name
|
||||
bundle_uuid = serializers.UUIDField(format='hex_verbose', read_only=True)
|
||||
# What version of this bundle we are currently linking to.
|
||||
# This is never NULL but can optionally be set to null when creating a new link, which means "use latest version."
|
||||
version = serializers.IntegerField(allow_null=True)
|
||||
# What the latest version of the linked bundle is:
|
||||
# (if latest_version > version), the link can be "updated" to the latest version.
|
||||
latest_version = serializers.IntegerField(read_only=True)
|
||||
# Opaque key: If the linked bundle is a library or other learning context whose opaque key we can deduce, then this
|
||||
# is the key. If we don't know what type of blockstore bundle this link is pointing to, then this is blank.
|
||||
opaque_key = serializers.CharField()
|
||||
|
||||
|
||||
class LibraryBundleLinkUpdateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for updating an existing link in a content library blockstore
|
||||
bundle.
|
||||
"""
|
||||
version = serializers.IntegerField(allow_null=True)
|
||||
|
||||
|
||||
class LibraryXBlockCreationSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for adding a new XBlock to a content library
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
Tests for Blockstore-based Content Libraries
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from organizations.models import Organization
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
import six
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_cms
|
||||
@@ -20,6 +20,7 @@ URL_PREFIX = '/api/libraries/v2/'
|
||||
URL_LIB_CREATE = URL_PREFIX
|
||||
URL_LIB_DETAIL = URL_PREFIX + '{lib_key}/' # Get data about a library, update or delete library
|
||||
URL_LIB_BLOCK_TYPES = URL_LIB_DETAIL + 'block_types/' # Get the list of XBlock types that can be added to this library
|
||||
URL_LIB_LINKS = URL_LIB_DETAIL + 'links/' # Get the list of links in this library, or add a new one
|
||||
URL_LIB_COMMIT = URL_LIB_DETAIL + 'commit/' # Commit (POST) or revert (DELETE) all pending changes to this library
|
||||
URL_LIB_BLOCKS = URL_LIB_DETAIL + 'blocks/' # Get the list of XBlocks in this library, or add a new one
|
||||
URL_LIB_TEAM = URL_LIB_DETAIL + 'team/' # Get the list of users/groups authorized to use this library
|
||||
@@ -67,7 +68,7 @@ class ContentLibrariesRestApiTest(APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(ContentLibrariesRestApiTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
# Create a collection using Blockstore API directly only because there
|
||||
# is not yet any Studio REST API for doing so:
|
||||
@@ -79,7 +80,7 @@ class ContentLibrariesRestApiTest(APITestCase):
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(ContentLibrariesRestApiTest, self).setUp()
|
||||
super().setUp()
|
||||
self.clients_by_user = {}
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
|
||||
@@ -143,6 +144,23 @@ class ContentLibrariesRestApiTest(APITestCase):
|
||||
""" Delete an existing library """
|
||||
return self._api('delete', URL_LIB_DETAIL.format(lib_key=lib_key), None, expect_response)
|
||||
|
||||
def _get_library_links(self, lib_key):
|
||||
""" Get the links of the specified content library """
|
||||
return self._api('get', URL_LIB_LINKS.format(lib_key=lib_key), None, expect_response=200)
|
||||
|
||||
def _link_to_library(self, lib_key, link_id, other_library_key, version=None):
|
||||
"""
|
||||
Modify the library identified by lib_key to create a named link to
|
||||
other_library_key. This allows you to use XBlocks from other_library in
|
||||
lib. Optionally specify a version to link to.
|
||||
"""
|
||||
data = {
|
||||
"id": link_id,
|
||||
"opaque_key": other_library_key,
|
||||
"version": version,
|
||||
}
|
||||
return self._api('post', URL_LIB_LINKS.format(lib_key=lib_key), data, expect_response=200)
|
||||
|
||||
def _commit_library_changes(self, lib_key, expect_response=200):
|
||||
""" Commit changes to an existing library """
|
||||
return self._api('post', URL_LIB_COMMIT.format(lib_key=lib_key), None, expect_response)
|
||||
@@ -221,8 +239,8 @@ class ContentLibrariesRestApiTest(APITestCase):
|
||||
|
||||
content should be a binary string.
|
||||
"""
|
||||
assert isinstance(content, six.binary_type)
|
||||
file_handle = six.BytesIO(content)
|
||||
assert isinstance(content, bytes)
|
||||
file_handle = BytesIO(content)
|
||||
url = URL_LIB_BLOCK_ASSET_FILE.format(block_key=block_key, file_name=file_name)
|
||||
response = self.client.put(url, data={"content": file_handle})
|
||||
self.assertEqual(
|
||||
|
||||
@@ -355,3 +355,91 @@ class ContentLibrariesTest(ContentLibrariesRestApiTest):
|
||||
self._delete_library_block(block3_key)
|
||||
self._commit_library_changes(lib_id)
|
||||
self._revert_library_changes(lib_id) # This is a no-op after the commit, but should still have 200 response
|
||||
|
||||
def test_library_blocks_with_links(self):
|
||||
"""
|
||||
Test that libraries can link to XBlocks in other content libraries
|
||||
"""
|
||||
# Create a problem bank:
|
||||
bank_lib = self._create_library(slug="problem_bank", title="Problem Bank")
|
||||
bank_lib_id = bank_lib["id"]
|
||||
# Add problem1 to the problem bank:
|
||||
p1 = self._add_block_to_library(bank_lib_id, "problem", "problem1")
|
||||
self._set_library_block_olx(p1["id"], """
|
||||
<problem><multiplechoiceresponse>
|
||||
<p>What is an even number?</p>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">3</choice>
|
||||
<choice correct="true">2</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse></problem>
|
||||
""")
|
||||
# Commit the changes, creating version 1:
|
||||
self._commit_library_changes(bank_lib_id)
|
||||
# Now update problem 1 and create a new problem 2:
|
||||
self._set_library_block_olx(p1["id"], """
|
||||
<problem><multiplechoiceresponse>
|
||||
<p>What is an odd number?</p>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="true">3</choice>
|
||||
<choice correct="false">2</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse></problem>
|
||||
""")
|
||||
p2 = self._add_block_to_library(bank_lib_id, "problem", "problem2")
|
||||
self._set_library_block_olx(p2["id"], """
|
||||
<problem><multiplechoiceresponse>
|
||||
<p>What holds this XBlock?</p>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">A course</choice>
|
||||
<choice correct="true">A problem bank</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse></problem>
|
||||
""")
|
||||
# Commit the changes, creating version 2:
|
||||
self._commit_library_changes(bank_lib_id)
|
||||
# At this point, bank_lib contains two problems and has two versions.
|
||||
# In version 1, problem1 is "What is an event number", and in version 2 it's "What is an odd number".
|
||||
# Problem2 exists only in version 2 and asks "What holds this XBlock?"
|
||||
|
||||
lib = self._create_library(slug="links_test_lib", title="Link Test Library")
|
||||
lib_id = lib["id"]
|
||||
# Link to the problem bank:
|
||||
self._link_to_library(lib_id, "problem_bank", bank_lib_id)
|
||||
self._link_to_library(lib_id, "problem_bank_v1", bank_lib_id, version=1)
|
||||
|
||||
# Add a 'unit' XBlock to the library:
|
||||
unit_block = self._add_block_to_library(lib_id, "unit", "unit1")
|
||||
self._set_library_block_olx(unit_block["id"], """
|
||||
<unit>
|
||||
<!-- version 2 link to "What is an odd number?" -->
|
||||
<xblock-include source="problem_bank" definition="problem/problem1"/>
|
||||
<!-- version 1 link to "What is an even number?" -->
|
||||
<xblock-include source="problem_bank_v1" definition="problem/problem1" usage="p1v1" />
|
||||
<!-- link to "What holds this XBlock?" -->
|
||||
<xblock-include source="problem_bank" definition="problem/problem2"/>
|
||||
</unit>
|
||||
""")
|
||||
|
||||
# The unit can see and render its children:
|
||||
fragment = self._render_block_view(unit_block["id"], "student_view")
|
||||
self.assertIn("What is an odd number?", fragment["content"])
|
||||
self.assertIn("What is an even number?", fragment["content"])
|
||||
self.assertIn("What holds this XBlock?", fragment["content"])
|
||||
|
||||
# Also check the API for retrieving links:
|
||||
links_created = self._get_library_links(lib_id)
|
||||
links_created.sort(key=lambda link: link["id"])
|
||||
self.assertEqual(len(links_created), 2)
|
||||
|
||||
self.assertEqual(links_created[0]["id"], "problem_bank")
|
||||
self.assertEqual(links_created[0]["bundle_uuid"], bank_lib["bundle_uuid"])
|
||||
self.assertEqual(links_created[0]["version"], 2)
|
||||
self.assertEqual(links_created[0]["latest_version"], 2)
|
||||
self.assertEqual(links_created[0]["opaque_key"], bank_lib_id)
|
||||
|
||||
self.assertEqual(links_created[1]["id"], "problem_bank_v1")
|
||||
self.assertEqual(links_created[1]["bundle_uuid"], bank_lib["bundle_uuid"])
|
||||
self.assertEqual(links_created[1]["version"], 1)
|
||||
self.assertEqual(links_created[1]["latest_version"], 2)
|
||||
self.assertEqual(links_created[1]["opaque_key"], bank_lib_id)
|
||||
|
||||
@@ -20,6 +20,10 @@ urlpatterns = [
|
||||
url(r'^$', views.LibraryDetailsView.as_view()),
|
||||
# Get the list of XBlock types that can be added to this library
|
||||
url(r'^block_types/$', views.LibraryBlockTypesView.as_view()),
|
||||
# Get the list of Blockstore Bundle Links for this library, or add a new one:
|
||||
url(r'^links/$', views.LibraryLinksView.as_view()),
|
||||
# Update or delete a link:
|
||||
url(r'^links/(?P<link_id>[^/]+)/$', views.LibraryLinkDetailView.as_view()),
|
||||
# Get the list of XBlocks in this library, or add a new one:
|
||||
url(r'^blocks/$', views.LibraryBlocksView.as_view()),
|
||||
# Commit (POST) or revert (DELETE) all pending changes to this library:
|
||||
|
||||
@@ -24,6 +24,8 @@ from openedx.core.djangoapps.content_libraries.serializers import (
|
||||
LibraryXBlockCreationSerializer,
|
||||
LibraryXBlockMetadataSerializer,
|
||||
LibraryXBlockTypeSerializer,
|
||||
LibraryBundleLinkSerializer,
|
||||
LibraryBundleLinkUpdateSerializer,
|
||||
LibraryXBlockOlxSerializer,
|
||||
LibraryXBlockStaticFileSerializer,
|
||||
LibraryXBlockStaticFilesSerializer,
|
||||
@@ -241,6 +243,80 @@ class LibraryBlockTypesView(APIView):
|
||||
return Response(LibraryXBlockTypeSerializer(result, many=True).data)
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class LibraryLinksView(APIView):
|
||||
"""
|
||||
View to get the list of bundles/libraries linked to this content library.
|
||||
|
||||
Because every content library is a blockstore bundle, it can have "links" to
|
||||
other bundles, which may or may not be content libraries. This allows using
|
||||
XBlocks (or perhaps even static assets etc.) from another bundle without
|
||||
needing to duplicate/copy the data.
|
||||
|
||||
Links always point to a specific published version of the target bundle.
|
||||
Links are identified by a slug-like ID, e.g. "link1"
|
||||
"""
|
||||
@convert_exceptions
|
||||
def get(self, request, lib_key_str):
|
||||
"""
|
||||
Get the list of bundles that this library links to, if any
|
||||
"""
|
||||
key = LibraryLocatorV2.from_string(lib_key_str)
|
||||
api.require_permission_for_library_key(key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
|
||||
result = api.get_bundle_links(key)
|
||||
return Response(LibraryBundleLinkSerializer(result, many=True).data)
|
||||
|
||||
@convert_exceptions
|
||||
def post(self, request, lib_key_str):
|
||||
"""
|
||||
Create a new link in this library.
|
||||
"""
|
||||
key = LibraryLocatorV2.from_string(lib_key_str)
|
||||
api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
|
||||
serializer = LibraryBundleLinkSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
target_key = LibraryLocatorV2.from_string(serializer.validated_data['opaque_key'])
|
||||
api.create_bundle_link(
|
||||
library_key=key,
|
||||
link_id=serializer.validated_data['id'],
|
||||
target_opaque_key=target_key,
|
||||
version=serializer.validated_data['version'], # a number, or None for "use latest version"
|
||||
)
|
||||
return Response({})
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class LibraryLinkDetailView(APIView):
|
||||
"""
|
||||
View to update/delete an existing library link
|
||||
"""
|
||||
@convert_exceptions
|
||||
def patch(self, request, lib_key_str, link_id):
|
||||
"""
|
||||
Update the specified link to point to a different version of its
|
||||
target bundle.
|
||||
|
||||
Pass e.g. {"version": 40} or pass {"version": None} to update to the
|
||||
latest published version.
|
||||
"""
|
||||
key = LibraryLocatorV2.from_string(lib_key_str)
|
||||
api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
|
||||
serializer = LibraryBundleLinkUpdateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
api.update_bundle_link(key, link_id, version=serializer.validated_data['version'])
|
||||
return Response({})
|
||||
|
||||
@convert_exceptions
|
||||
def delete(self, request, lib_key_str, link_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Delete a link from this library.
|
||||
"""
|
||||
key = LibraryLocatorV2.from_string(lib_key_str)
|
||||
api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
|
||||
api.update_bundle_link(key, link_id, delete=True)
|
||||
return Response({})
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class LibraryCommitView(APIView):
|
||||
"""
|
||||
|
||||
@@ -264,7 +264,7 @@ def get_bundle_version_links(bundle_uuid, version_number):
|
||||
Get a dictionary of the links in the specified bundle version
|
||||
"""
|
||||
if version_number == 0:
|
||||
return []
|
||||
return {}
|
||||
version_url = api_url('bundle_versions', str(bundle_uuid) + ',' + str(version_number))
|
||||
version_info = api_request('get', version_url)
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user