Skip to content

Commit b0854a1

Browse files
committed
feat: container collections support
1 parent 0120179 commit b0854a1

13 files changed

Lines changed: 93 additions & 77 deletions

File tree

openedx/core/djangoapps/content_libraries/api/collections.py

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,21 @@
44
"""
55
from django.db import IntegrityError
66
from opaque_keys.edx.keys import BlockTypeKey, UsageKeyV2
7-
from opaque_keys.edx.locator import (
8-
LibraryLocatorV2,
9-
LibraryCollectionLocator,
10-
)
11-
7+
from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryLocatorV2
128
from openedx_events.content_authoring.data import LibraryCollectionData
139
from openedx_events.content_authoring.signals import LIBRARY_COLLECTION_UPDATED
14-
1510
from openedx_learning.api import authoring as authoring_api
16-
from openedx_learning.api.authoring_models import (
17-
Collection,
18-
Component,
19-
PublishableEntity,
20-
)
21-
22-
from .exceptions import (
23-
ContentLibraryBlockNotFound,
24-
ContentLibraryCollectionNotFound,
25-
LibraryCollectionAlreadyExists,
26-
)
11+
from openedx_learning.api.authoring_models import Collection, Component, PublishableEntity
12+
2713
from ..models import ContentLibrary
14+
from .exceptions import ContentLibraryBlockNotFound, ContentLibraryCollectionNotFound, LibraryCollectionAlreadyExists
2815

2916
# The public API is only the following symbols:
3017
__all__ = [
3118
"create_library_collection",
3219
"update_library_collection",
3320
"update_library_collection_components",
34-
"set_library_component_collections",
21+
"set_library_item_collections",
3522
"get_library_collection_usage_key",
3623
"get_library_collection_from_usage_key",
3724
]
@@ -173,27 +160,27 @@ def update_library_collection_components(
173160
return collection
174161

175162

176-
def set_library_component_collections(
163+
def set_library_item_collections(
177164
library_key: LibraryLocatorV2,
178-
component: Component,
165+
publishable_entity: PublishableEntity,
179166
*,
180167
collection_keys: list[str],
181168
created_by: int | None = None,
182169
# As an optimization, callers may pass in a pre-fetched ContentLibrary instance
183170
content_library: ContentLibrary | None = None,
184-
) -> Component:
171+
) -> PublishableEntity:
185172
"""
186-
It Associates the component with collections for the given collection keys.
173+
It Associates the publishable_entity with collections for the given collection keys.
187174
188-
Only collections in queryset are associated with component, all previous component-collections
175+
Only collections in queryset are associated with publishable_entity, all previous publishable_entity-collections
189176
associations are removed.
190177
191178
If you've already fetched the ContentLibrary, pass it in to avoid refetching.
192179
193180
Raises:
194181
* ContentLibraryCollectionNotFound if any of the given collection_keys don't match Collections in the given library.
195182
196-
Returns the updated Component.
183+
Returns the updated PublishableEntity.
197184
"""
198185
if not content_library:
199186
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
@@ -208,7 +195,7 @@ def set_library_component_collections(
208195

209196
affected_collections = authoring_api.set_collections(
210197
content_library.learning_package_id,
211-
component,
198+
publishable_entity,
212199
collection_qs,
213200
created_by=created_by,
214201
)
@@ -224,7 +211,7 @@ def set_library_component_collections(
224211
)
225212
)
226213

227-
return component
214+
return publishable_entity
228215

229216

230217
def get_library_collection_usage_key(

openedx/core/djangoapps/content_libraries/api/containers.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"ContainerMetadata",
3838
"ContainerType",
3939
# API methods
40+
"get_container_from_key",
4041
"get_container",
4142
"create_container",
4243
"get_container_children",
@@ -121,7 +122,7 @@ def library_container_locator(
121122
)
122123

123124

124-
def _get_container(container_key: LibraryContainerLocator) -> Container:
125+
def get_container_from_key(container_key: LibraryContainerLocator) -> Container:
125126
"""
126127
Internal method to fetch the Container object from its LibraryContainerLocator
127128
@@ -144,7 +145,7 @@ def get_container(container_key: LibraryContainerLocator) -> ContainerMetadata:
144145
"""
145146
Get a container (a Section, Subsection, or Unit).
146147
"""
147-
container = _get_container(container_key)
148+
container = get_container_from_key(container_key)
148149
container_meta = ContainerMetadata.from_container(container_key.library_key, container)
149150
assert container_meta.container_type.value == container_key.container_type
150151
return container_meta
@@ -205,7 +206,7 @@ def update_container(
205206
"""
206207
Update a container (e.g. a Unit) title.
207208
"""
208-
container = _get_container(container_key)
209+
container = get_container_from_key(container_key)
209210
library_key = container_key.library_key
210211

211212
assert container.unit
@@ -235,7 +236,7 @@ def delete_container(
235236
No-op if container doesn't exist or has already been soft-deleted.
236237
"""
237238
try:
238-
container = _get_container(container_key)
239+
container = get_container_from_key(container_key)
239240
except ContentLibraryContainerNotFound:
240241
return
241242

@@ -258,7 +259,7 @@ def get_container_children(
258259
"""
259260
Get the entities contained in the given container (e.g. the components/xblocks in a unit)
260261
"""
261-
container = _get_container(container_key)
262+
container = get_container_from_key(container_key)
262263
if container_key.container_type == ContainerType.Unit.value:
263264
child_components = authoring_api.get_components_in_unit(container.unit, published=published)
264265
return [LibraryXBlockMetadata.from_component(
@@ -280,7 +281,7 @@ def get_container_children_count(
280281
"""
281282
Get the count of entities contained in the given container (e.g. the components/xblocks in a unit)
282283
"""
283-
container = _get_container(container_key)
284+
container = get_container_from_key(container_key)
284285
return authoring_api.get_container_children_count(container, published=published)
285286

286287

@@ -295,7 +296,7 @@ def update_container_children(
295296
"""
296297
library_key = container_key.library_key
297298
container_type = container_key.container_type
298-
container = _get_container(container_key)
299+
container = get_container_from_key(container_key)
299300
match container_type:
300301
case ContainerType.Unit.value:
301302
components = [get_component_from_usage_key(key) for key in children_ids] # type: ignore[arg-type]

openedx/core/djangoapps/content_libraries/rest_api/blocks.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
"""
22
Content Library REST APIs related to XBlocks/Components and their static assets
33
"""
4+
import edx_api_doc_tools as apidocs
45
from django.core.exceptions import ObjectDoesNotExist
56
from django.db.transaction import non_atomic_requests
67
from django.http import Http404, HttpResponse, StreamingHttpResponse
78
from django.urls import reverse
89
from django.utils.decorators import method_decorator
910
from drf_yasg.utils import swagger_auto_schema
11+
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
12+
from openedx_learning.api import authoring as authoring_api
1013
from rest_framework import status
1114
from rest_framework.exceptions import NotFound, ValidationError
1215
from rest_framework.generics import GenericAPIView
1316
from rest_framework.parsers import MultiPartParser
1417
from rest_framework.response import Response
1518
from rest_framework.views import APIView
1619

17-
import edx_api_doc_tools as apidocs
18-
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
19-
from openedx_learning.api import authoring as authoring_api
20-
2120
from openedx.core.djangoapps.content_libraries import api, permissions
2221
from openedx.core.djangoapps.content_libraries.rest_api.serializers import (
23-
ContentLibraryComponentCollectionsUpdateSerializer,
22+
ContentLibraryItemCollectionsUpdateSerializer,
2423
LibraryXBlockCreationSerializer,
2524
LibraryXBlockMetadataSerializer,
2625
LibraryXBlockOlxSerializer,
2726
LibraryXBlockStaticFileSerializer,
28-
LibraryXBlockStaticFilesSerializer,
27+
LibraryXBlockStaticFilesSerializer
2928
)
29+
from openedx.core.djangoapps.xblock import api as xblock_api
3030
from openedx.core.lib.api.view_utils import view_auth_classes
3131
from openedx.core.types.http import RestRequest
32-
from openedx.core.djangoapps.xblock import api as xblock_api
3332

3433
from .libraries import LibraryApiPaginationDocs
3534
from .utils import convert_exceptions
@@ -259,13 +258,13 @@ def patch(self, request: RestRequest, usage_key_str) -> Response:
259258
permissions.CAN_EDIT_THIS_CONTENT_LIBRARY
260259
)
261260
component = api.get_component_from_usage_key(key)
262-
serializer = ContentLibraryComponentCollectionsUpdateSerializer(data=request.data)
261+
serializer = ContentLibraryItemCollectionsUpdateSerializer(data=request.data)
263262
serializer.is_valid(raise_exception=True)
264263

265264
collection_keys = serializer.validated_data['collection_keys']
266-
api.set_library_component_collections(
265+
api.set_library_item_collections(
267266
library_key=key.lib_key,
268-
component=component,
267+
publishable_entity=component.publishable_entity,
269268
collection_keys=collection_keys,
270269
created_by=request.user.id,
271270
content_library=content_library,

openedx/core/djangoapps/content_libraries/rest_api/containers.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from openedx.core.djangoapps.content_libraries import api, permissions
2020
from openedx.core.lib.api.view_utils import view_auth_classes
21+
from openedx.core.types.http import RestRequest
2122
from . import serializers
2223
from .utils import convert_exceptions
2324

@@ -273,3 +274,38 @@ def patch(self, request, container_key: LibraryContainerLocator):
273274
container_key,
274275
action=authoring_api.ChildrenEntitiesAction.REPLACE,
275276
)
277+
278+
279+
@method_decorator(non_atomic_requests, name="dispatch")
280+
@view_auth_classes()
281+
class LibraryContainerCollectionsView(GenericAPIView):
282+
"""
283+
View to set collections for a container.
284+
"""
285+
@convert_exceptions
286+
def patch(self, request: RestRequest, container_key_str: str) -> Response:
287+
"""
288+
Sets Collections for a Component.
289+
290+
Collection and Components must all be part of the given library/learning package.
291+
"""
292+
key = LibraryContainerLocator.from_string(container_key_str)
293+
content_library = api.require_permission_for_library_key(
294+
key.library_key,
295+
request.user,
296+
permissions.CAN_EDIT_THIS_CONTENT_LIBRARY
297+
)
298+
container = api.get_container_from_key(key)
299+
serializer = serializers.ContentLibraryItemCollectionsUpdateSerializer(data=request.data)
300+
serializer.is_valid(raise_exception=True)
301+
302+
collection_keys = serializer.validated_data['collection_keys']
303+
api.set_library_item_collections(
304+
library_key=key.library_key,
305+
publishable_entity=container.publishable_entity,
306+
collection_keys=collection_keys,
307+
created_by=request.user.id,
308+
content_library=content_library,
309+
)
310+
311+
return Response({'count': len(collection_keys)})

openedx/core/djangoapps/content_libraries/rest_api/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@ class ContentLibraryComponentKeysSerializer(serializers.Serializer):
357357
usage_keys = serializers.ListField(child=UsageKeyV2Serializer(), allow_empty=False)
358358

359359

360-
class ContentLibraryComponentCollectionsUpdateSerializer(serializers.Serializer):
360+
class ContentLibraryItemCollectionsUpdateSerializer(serializers.Serializer):
361361
"""
362-
Serializer for adding/removing Collections to/from a Component.
362+
Serializer for adding/removing Collections to/from a Library Item (component, unit, etc..).
363363
"""
364364

365365
collection_keys = serializers.ListField(child=serializers.CharField(), allow_empty=True)

openedx/core/djangoapps/content_libraries/signal_handlers.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
LIBRARY_COLLECTION_DELETED,
2121
LIBRARY_COLLECTION_UPDATED,
2222
)
23-
from openedx_learning.api.authoring import get_component, get_components
24-
from openedx_learning.api.authoring_models import Collection, CollectionPublishableEntity, Component, PublishableEntity
23+
from openedx_learning.api.authoring import get_components, get_containers
24+
from openedx_learning.api.authoring_models import Collection, CollectionPublishableEntity, PublishableEntity
2525

2626
from lms.djangoapps.grades.api import signals as grades_signals
2727

28-
from .api import library_component_usage_key
2928
from .models import ContentLibrary, LtiGradedResource
3029

3130
log = logging.getLogger(__name__)
@@ -118,8 +117,8 @@ def library_collection_deleted(sender, instance, **kwargs):
118117
)
119118

120119

121-
def _library_collection_component_changed(
122-
component: Component,
120+
def _library_collection_entity_changed(
121+
publishable_entity: PublishableEntity,
123122
library_key: LibraryLocatorV2 | None = None,
124123
) -> None:
125124
"""
@@ -128,23 +127,19 @@ def _library_collection_component_changed(
128127
if not library_key:
129128
try:
130129
library = ContentLibrary.objects.get(
131-
learning_package_id=component.learning_package_id,
130+
learning_package_id=publishable_entity.learning_package_id,
132131
)
133132
except ContentLibrary.DoesNotExist:
134-
log.error("{component} is not associated with a content library.")
133+
log.error("{publishable_entity} is not associated with a content library.")
135134
return
136135

137136
library_key = library.library_key
138137

139138
assert library_key
140139

141-
usage_key = library_component_usage_key(
142-
library_key,
143-
component,
144-
)
145140
CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event(
146141
content_object=ContentObjectChangedData(
147-
object_id=str(usage_key),
142+
object_id=str(publishable_entity.key),
148143
changes=["collections"],
149144
),
150145
)
@@ -156,9 +151,7 @@ def library_collection_entity_saved(sender, instance, created, **kwargs):
156151
Sends a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event for components added to a collection.
157152
"""
158153
if created:
159-
# Component.pk matches its entity.pk
160-
component = get_component(instance.entity_id)
161-
_library_collection_component_changed(component)
154+
_library_collection_entity_changed(instance.entity)
162155

163156

164157
@receiver(post_delete, sender=CollectionPublishableEntity, dispatch_uid="library_collection_entity_deleted")
@@ -168,9 +161,7 @@ def library_collection_entity_deleted(sender, instance, **kwargs):
168161
"""
169162
# Only trigger component updates if CollectionPublishableEntity was cascade deleted due to deletion of a collection.
170163
if isinstance(kwargs.get('origin'), Collection):
171-
# Component.pk matches its entity.pk
172-
component = get_component(instance.entity_id)
173-
_library_collection_component_changed(component)
164+
_library_collection_entity_changed(instance.entity)
174165

175166

176167
@receiver(m2m_changed, sender=CollectionPublishableEntity, dispatch_uid="library_collection_entities_changed")
@@ -190,15 +181,17 @@ def library_collection_entities_changed(sender, instance, action, pk_set, **kwar
190181
return
191182

192183
if isinstance(instance, PublishableEntity):
193-
_library_collection_component_changed(instance.component, library.library_key)
184+
_library_collection_entity_changed(instance, library.library_key)
194185
return
195186

196187
# When action=="post_clear", pk_set==None
197188
# Since the collection instance now has an empty entities set,
198189
# we don't know which ones were removed, so we need to update associations for all library components.
199190
components = get_components(instance.learning_package_id)
191+
containers = get_containers(instance.learning_package_id)
200192
if pk_set:
201193
components = components.filter(pk__in=pk_set)
194+
containers = containers.filter(pk__in=pk_set)
202195

203-
for component in components.all():
204-
_library_collection_component_changed(component, library.library_key)
196+
for entity in [components.all(), containers.all()]:
197+
_library_collection_entity_changed(entity.publishable_entity, library.library_key)

openedx/core/djangoapps/content_libraries/tests/test_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,9 @@ def test_set_library_component_collections(self):
520520
assert not list(self.col2.entities.all())
521521
component = api.get_component_from_usage_key(UsageKey.from_string(self.lib2_problem_block["id"]))
522522

523-
api.set_library_component_collections(
523+
api.set_library_item_collections(
524524
self.lib2.library_key,
525-
component,
525+
component.publishable_entity,
526526
collection_keys=[self.col2.key, self.col3.key],
527527
)
528528

0 commit comments

Comments
 (0)