Skip to content

Commit 7886f40

Browse files
committed
feat: adds python and REST APIs to update a container's display_name
1 parent 0370a37 commit 7886f40

6 files changed

Lines changed: 109 additions & 3 deletions

File tree

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
from openedx_events.content_authoring.data import LibraryContainerData
1717
from openedx_events.content_authoring.signals import (
1818
LIBRARY_CONTAINER_CREATED,
19+
LIBRARY_CONTAINER_UPDATED,
1920
)
2021
from openedx_learning.api import authoring as authoring_api
2122
from openedx_learning.api import authoring_models
2223

2324
from ..models import ContentLibrary
2425
from .libraries import PublishableItem
2526

27+
2628
# The public API is only the following symbols:
2729
__all__ = [
2830
"ContainerMetadata",
@@ -31,6 +33,7 @@
3133
"create_container",
3234
"get_container_children",
3335
"library_container_locator",
36+
"update_container",
3437
]
3538

3639

@@ -170,6 +173,42 @@ def create_container(
170173
return ContainerMetadata.from_container(library_key, container)
171174

172175

176+
def update_container(
177+
container_key: LibraryContainerLocator,
178+
display_name: str,
179+
user_id: int | None,
180+
) -> ContainerMetadata:
181+
"""
182+
Update a container (e.g. a Unit) title.
183+
"""
184+
assert isinstance(container_key, LibraryContainerLocator)
185+
library_key = container_key.library_key
186+
content_library = ContentLibrary.objects.get_by_key(library_key)
187+
learning_package = content_library.learning_package
188+
assert learning_package is not None
189+
container = authoring_api.get_container_by_key(
190+
learning_package.id,
191+
key=container_key.container_id,
192+
)
193+
194+
assert container.unit
195+
unit_version = authoring_api.create_next_unit_version(
196+
container.unit,
197+
title=display_name,
198+
created=datetime.now(),
199+
created_by=user_id,
200+
)
201+
202+
LIBRARY_CONTAINER_UPDATED.send_event(
203+
library_container=LibraryContainerData(
204+
library_key=library_key,
205+
container_key=str(container_key),
206+
)
207+
)
208+
209+
return ContainerMetadata.from_container(library_key, unit_version.container)
210+
211+
173212
def get_container_children(
174213
container_key: LibraryContainerLocator,
175214
published=False,

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def post(self, request, lib_key_str):
6262
@view_auth_classes()
6363
class LibraryContainerView(GenericAPIView):
6464
"""
65-
View to get data about a specific container (a section, subsection, or unit)
65+
View to retrieve or update data about a specific container (a section, subsection, or unit)
6666
"""
6767
serializer_class = serializers.LibraryContainerMetadataSerializer
6868

@@ -81,3 +81,28 @@ def get(self, request, container_key: LibraryContainerLocator):
8181
)
8282
container = api.get_container(container_key)
8383
return Response(serializers.LibraryContainerMetadataSerializer(container).data)
84+
85+
@convert_exceptions
86+
@swagger_auto_schema(
87+
request_body=serializers.LibraryContainerUpdateSerializer,
88+
responses={200: serializers.LibraryContainerMetadataSerializer}
89+
)
90+
def patch(self, request, container_key: LibraryContainerLocator):
91+
"""
92+
Update a Container.
93+
"""
94+
api.require_permission_for_library_key(
95+
container_key.library_key,
96+
request.user,
97+
permissions.CAN_EDIT_THIS_CONTENT_LIBRARY,
98+
)
99+
serializer = serializers.LibraryContainerUpdateSerializer(data=request.data)
100+
serializer.is_valid(raise_exception=True)
101+
102+
container = api.update_container(
103+
container_key,
104+
display_name=serializer.validated_data['display_name'],
105+
user_id=request.user.id,
106+
)
107+
108+
return Response(serializers.LibraryContainerMetadataSerializer(container).data)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ def to_internal_value(self, data):
270270
return result
271271

272272

273+
class LibraryContainerUpdateSerializer(serializers.Serializer):
274+
"""
275+
Serializer for updating metadata for Containers like Sections, Subsections, Units
276+
"""
277+
display_name = serializers.CharField()
278+
279+
273280
class ContentLibraryBlockImportTaskSerializer(serializers.ModelSerializer):
274281
"""
275282
Serializer for a Content Library block import task.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,8 @@ def _create_container(self, lib_key, container_type, slug: str | None, display_n
363363
def _get_container(self, container_key: str, expect_response=200):
364364
""" Get a container (unit etc.) """
365365
return self._api('get', URL_LIB_CONTAINER.format(container_key=container_key), None, expect_response)
366+
367+
def _update_container(self, container_key: str, display_name: str, expect_response=200):
368+
""" Update a container (unit etc.) """
369+
data = {"display_name": display_name}
370+
return self._api('patch', URL_LIB_CONTAINER.format(container_key=container_key), data, expect_response)

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from opaque_keys.edx.locator import LibraryLocatorV2
1111
from openedx_events.content_authoring.data import LibraryContainerData
12-
from openedx_events.content_authoring.signals import LIBRARY_CONTAINER_CREATED
12+
from openedx_events.content_authoring.signals import LIBRARY_CONTAINER_CREATED, LIBRARY_CONTAINER_UPDATED
1313
from openedx_events.tests.utils import OpenEdxEventsTestMixin
1414

1515
from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest
@@ -38,6 +38,7 @@ class ContainersTestCase(OpenEdxEventsTestMixin, ContentLibrariesRestApiTest):
3838
"""
3939
ENABLED_OPENEDX_EVENTS = [
4040
LIBRARY_CONTAINER_CREATED.event_type,
41+
LIBRARY_CONTAINER_UPDATED.event_type,
4142
]
4243

4344
def test_unit_crud(self):
@@ -50,6 +51,9 @@ def test_unit_crud(self):
5051
create_receiver = mock.Mock()
5152
LIBRARY_CONTAINER_CREATED.connect(create_receiver)
5253

54+
update_receiver = mock.Mock()
55+
LIBRARY_CONTAINER_UPDATED.connect(update_receiver)
56+
5357
# Create a unit:
5458
create_date = datetime(2024, 9, 8, 7, 6, 5, tzinfo=timezone.utc)
5559
with freeze_time(create_date):
@@ -87,6 +91,32 @@ def test_unit_crud(self):
8791
# make sure it contains the same data when we read it back:
8892
self.assertDictContainsEntries(unit_as_read, expected_data)
8993

94+
# Update the unit:
95+
modified_date = datetime(2024, 10, 9, 8, 7, 6, tzinfo=timezone.utc)
96+
with freeze_time(modified_date):
97+
container_data = self._update_container("lct:CL-TEST:containers:unit:u1", display_name="Unit ABC")
98+
expected_data['last_draft_created'] = expected_data['modified'] = '2024-10-09T08:07:06Z'
99+
expected_data['display_name'] = 'Unit ABC'
100+
self.assertDictContainsEntries(container_data, expected_data)
101+
102+
assert update_receiver.call_count == 1
103+
self.assertDictContainsSubset(
104+
{
105+
"signal": LIBRARY_CONTAINER_UPDATED,
106+
"sender": None,
107+
"library_container": LibraryContainerData(
108+
lib_key,
109+
container_key="lct:CL-TEST:containers:unit:u1",
110+
),
111+
},
112+
update_receiver.call_args_list[0].kwargs,
113+
)
114+
115+
# Re-fetch the unit
116+
unit_as_re_read = self._get_container(container_data["container_key"])
117+
# make sure it contains the same data when we read it back:
118+
self.assertDictContainsEntries(unit_as_re_read, expected_data)
119+
90120
# TODO: test that a regular user with read-only permissions on the library cannot create units
91121

92122
def test_unit_gets_auto_slugs(self):

openedx/core/djangoapps/content_libraries/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
])),
7979
# Containers are Sections, Subsections, and Units
8080
path('containers/<lib_container_key:container_key>/', include([
81-
# Get metadata about a specific container in this library, or delete the container:
81+
# Get metadata about a specific container in this library, update or delete the container:
8282
path('', containers.LibraryContainerView.as_view()),
8383
# Update collections for a given container
8484
# path('collections/', views.LibraryContainerCollectionsView.as_view(), name='update-collections-ct'),

0 commit comments

Comments
 (0)