Skip to content

Commit 5d9fc24

Browse files
authored
refactor: course container children api [FC-0097] (#37375)
* feat: course container children view * refactor: rename * refactor: include children info in upstream info of container children * fix: tests * fix: test * refactor: children check
1 parent 2b47814 commit 5d9fc24

8 files changed

Lines changed: 239 additions & 195 deletions

File tree

cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919
from .settings import CourseSettingsSerializer
2020
from .textbooks import CourseTextbooksSerializer
21-
from .vertical_block import ContainerHandlerSerializer, VerticalContainerSerializer
21+
from .vertical_block import ContainerHandlerSerializer, ContainerChildrenSerializer
2222
from .videos import (
2323
CourseVideosSerializer,
2424
VideoDownloadSerializer,

cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ def get_assets_url(self, obj):
105105
return None
106106

107107

108+
class UpstreamChildrenInfoSerializer(serializers.Serializer):
109+
"""
110+
Serializer holding the information about the children of an xblock that is syncing.
111+
"""
112+
name = serializers.CharField()
113+
upstream = serializers.CharField(allow_null=True)
114+
id = serializers.CharField()
115+
116+
108117
class UpstreamLinkSerializer(serializers.Serializer):
109118
"""
110119
Serializer holding info for syncing a block with its upstream (eg, a library block).
@@ -115,9 +124,12 @@ class UpstreamLinkSerializer(serializers.Serializer):
115124
version_declined = serializers.IntegerField(allow_null=True)
116125
error_message = serializers.CharField(allow_null=True)
117126
ready_to_sync = serializers.BooleanField()
127+
is_modified = serializers.BooleanField()
128+
has_top_level_parent = serializers.BooleanField()
129+
ready_to_sync_children = UpstreamChildrenInfoSerializer(many=True, required=False)
118130

119131

120-
class ChildVerticalContainerSerializer(serializers.Serializer):
132+
class ContainerChildSerializer(serializers.Serializer):
121133
"""
122134
Serializer for representing a xblock child of vertical container.
123135
"""
@@ -160,11 +172,11 @@ def get_actions(self, obj): # pylint: disable=unused-argument
160172
return actions
161173

162174

163-
class VerticalContainerSerializer(serializers.Serializer):
175+
class ContainerChildrenSerializer(serializers.Serializer):
164176
"""
165177
Serializer for representing a vertical container with state and children.
166178
"""
167179

168-
children = ChildVerticalContainerSerializer(many=True)
180+
children = ContainerChildSerializer(many=True)
169181
is_published = serializers.BooleanField()
170182
can_paste_component = serializers.BooleanField()

cms/djangoapps/contentstore/rest_api/v1/urls.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
""" Contenstore API v1 URLs. """
22

33
from django.conf import settings
4-
from django.urls import re_path, path
4+
from django.urls import path, re_path
55

66
from openedx.core.constants import COURSE_ID_PATTERN
77

88
from .views import (
9+
ContainerChildrenView,
910
ContainerHandlerView,
1011
CourseCertificatesView,
1112
CourseDetailsView,
12-
CourseTeamView,
13-
CourseTextbooksView,
14-
CourseIndexView,
1513
CourseGradingView,
1614
CourseGroupConfigurationsView,
15+
CourseIndexView,
1716
CourseRerunView,
1817
CourseSettingsView,
18+
CourseTeamView,
19+
CourseTextbooksView,
1920
CourseVideosView,
2021
CourseWaffleFlagsView,
21-
HomePageView,
22+
HelpUrlsView,
2223
HomePageCoursesView,
2324
HomePageLibrariesView,
25+
HomePageView,
2426
ProctoredExamSettingsView,
2527
ProctoringErrorsView,
26-
HelpUrlsView,
27-
VideoUsageView,
2828
VideoDownloadView,
29-
VerticalContainerView,
29+
VideoUsageView,
30+
vertical_container_children_redirect_view,
3031
)
3132

3233
app_name = 'v1'
@@ -127,11 +128,17 @@
127128
ContainerHandlerView.as_view(),
128129
name="container_handler"
129130
),
131+
# Deprecated url, please use `container_children` url below
130132
re_path(
131133
fr'^container/vertical/{settings.USAGE_KEY_PATTERN}/children$',
132-
VerticalContainerView.as_view(),
134+
vertical_container_children_redirect_view,
133135
name="container_vertical"
134136
),
137+
re_path(
138+
fr'^container/{settings.USAGE_KEY_PATTERN}/children$',
139+
ContainerChildrenView.as_view(),
140+
name="container_children"
141+
),
135142
re_path(
136143
fr'^course_waffle_flags(?:/{COURSE_ID_PATTERN})?$',
137144
CourseWaffleFlagsView.as_view(),

cms/djangoapps/contentstore/rest_api/v1/views/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
"""
44
from .certificates import CourseCertificatesView
55
from .course_details import CourseDetailsView
6-
from .course_index import CourseIndexView
6+
from .course_index import ContainerChildrenView, CourseIndexView
77
from .course_rerun import CourseRerunView
8-
from .course_waffle_flags import CourseWaffleFlagsView
98
from .course_team import CourseTeamView
9+
from .course_waffle_flags import CourseWaffleFlagsView
1010
from .grading import CourseGradingView
1111
from .group_configurations import CourseGroupConfigurationsView
1212
from .help_urls import HelpUrlsView
1313
from .home import HomePageCoursesView, HomePageLibrariesView, HomePageView
1414
from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView
1515
from .settings import CourseSettingsView
1616
from .textbooks import CourseTextbooksView
17-
from .vertical_block import ContainerHandlerView, VerticalContainerView
17+
from .vertical_block import ContainerHandlerView, vertical_container_children_redirect_view
1818
from .videos import (
1919
CourseVideosView,
2020
VideoDownloadView,

cms/djangoapps/contentstore/rest_api/v1/views/course_index.py

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""API Views for course index"""
22

3+
import logging
4+
35
import edx_api_doc_tools as apidocs
46
from django.conf import settings
57
from opaque_keys.edx.keys import CourseKey
@@ -8,10 +10,24 @@
810
from rest_framework.views import APIView
911

1012
from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
11-
from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseIndexSerializer
12-
from cms.djangoapps.contentstore.utils import get_course_index_context
13+
from cms.djangoapps.contentstore.rest_api.v1.mixins import ContainerHandlerMixin
14+
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
15+
CourseIndexSerializer,
16+
ContainerChildrenSerializer,
17+
)
18+
from cms.djangoapps.contentstore.utils import (
19+
get_course_index_context,
20+
get_user_partition_info,
21+
get_visibility_partition_info,
22+
get_xblock_render_error,
23+
get_xblock_validation_messages,
24+
)
25+
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock
26+
from cms.lib.xblock.upstream_sync import UpstreamLink
1327
from common.djangoapps.student.auth import has_studio_read_access
1428
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
29+
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
30+
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
1531

1632

1733
@view_auth_classes(is_authenticated=True)
@@ -98,3 +114,169 @@ def get(self, request: Request, course_id: str):
98114

99115
serializer = CourseIndexSerializer(course_index_context)
100116
return Response(serializer.data)
117+
118+
119+
@view_auth_classes(is_authenticated=True)
120+
class ContainerChildrenView(APIView, ContainerHandlerMixin):
121+
"""
122+
View for container xblock requests to get state and children data.
123+
"""
124+
125+
@apidocs.schema(
126+
parameters=[
127+
apidocs.string_parameter(
128+
"usage_key_string",
129+
apidocs.ParameterLocation.PATH,
130+
description="Container usage key",
131+
),
132+
],
133+
responses={
134+
200: ContainerChildrenSerializer,
135+
401: "The requester is not authenticated.",
136+
404: "The requested locator does not exist.",
137+
},
138+
)
139+
def get(self, request: Request, usage_key_string: str):
140+
"""
141+
Get an object containing vertical state with children data.
142+
143+
**Example Request**
144+
145+
GET /api/contentstore/v1/container/{usage_key_string}/children
146+
147+
**Response Values**
148+
149+
If the request is successful, an HTTP 200 "OK" response is returned.
150+
151+
The HTTP 200 response contains a single dict that contains keys that
152+
are the vertical's container children data.
153+
154+
**Example Response**
155+
156+
```json
157+
{
158+
"children": [
159+
{
160+
"name": "Drag and Drop",
161+
"block_id": "block-v1:org+101+101+type@drag-and-drop-v2+block@7599275ace6b46f5a482078a2954ca16",
162+
"block_type": "drag-and-drop-v2",
163+
"user_partition_info": {},
164+
"user_partitions": {}
165+
"upstream_link": null,
166+
"actions": {
167+
"can_copy": true,
168+
"can_duplicate": true,
169+
"can_move": true,
170+
"can_manage_access": true,
171+
"can_delete": true,
172+
"can_manage_tags": true,
173+
},
174+
"has_validation_error": false,
175+
"validation_errors": [],
176+
},
177+
{
178+
"name": "Video",
179+
"block_id": "block-v1:org+101+101+type@video+block@0e3d39b12d7c4345981bda6b3511a9bf",
180+
"block_type": "video",
181+
"user_partition_info": {},
182+
"user_partitions": {}
183+
"upstream_link": {
184+
"upstream_ref": "lb:org:mylib:video:404",
185+
"version_synced": 16
186+
"version_available": null,
187+
"error_message": "Linked library item not found: lb:org:mylib:video:404",
188+
"ready_to_sync": false,
189+
},
190+
"actions": {
191+
"can_copy": true,
192+
"can_duplicate": true,
193+
"can_move": true,
194+
"can_manage_access": true,
195+
"can_delete": true,
196+
"can_manage_tags": true,
197+
}
198+
"validation_messages": [],
199+
"render_error": "",
200+
},
201+
{
202+
"name": "Text",
203+
"block_id": "block-v1:org+101+101+type@html+block@3e3fa1f88adb4a108cd14e9002143690",
204+
"block_type": "html",
205+
"user_partition_info": {},
206+
"user_partitions": {},
207+
"upstream_link": {
208+
"upstream_ref": "lb:org:mylib:html:abcd",
209+
"version_synced": 43,
210+
"version_available": 49,
211+
"error_message": null,
212+
"ready_to_sync": true,
213+
},
214+
"actions": {
215+
"can_copy": true,
216+
"can_duplicate": true,
217+
"can_move": true,
218+
"can_manage_access": true,
219+
"can_delete": true,
220+
"can_manage_tags": true,
221+
},
222+
"validation_messages": [
223+
{
224+
"text": "This component's access settings contradict its parent's access settings.",
225+
"type": "error"
226+
}
227+
],
228+
"render_error": "Unterminated control keyword: 'if' in file '../problem.html'",
229+
},
230+
],
231+
"is_published": false,
232+
"can_paste_component": true,
233+
}
234+
```
235+
"""
236+
usage_key = self.get_object(usage_key_string)
237+
current_xblock = get_xblock(usage_key, request.user)
238+
is_course = current_xblock.scope_ids.usage_id.context_key.is_course
239+
240+
with modulestore().bulk_operations(usage_key.course_key):
241+
# load course once to reuse it for user_partitions query
242+
course = modulestore().get_course(current_xblock.location.course_key)
243+
children = []
244+
if current_xblock.has_children:
245+
for child in current_xblock.children:
246+
child_info = modulestore().get_item(child)
247+
user_partition_info = get_visibility_partition_info(child_info, course=course)
248+
user_partitions = get_user_partition_info(child_info, course=course)
249+
upstream_link = UpstreamLink.try_get_for_block(child_info, log_error=False)
250+
validation_messages = get_xblock_validation_messages(child_info)
251+
render_error = get_xblock_render_error(request, child_info)
252+
253+
children.append({
254+
"xblock": child_info,
255+
"name": child_info.display_name_with_default,
256+
"block_id": child_info.location,
257+
"block_type": child_info.location.block_type,
258+
"user_partition_info": user_partition_info,
259+
"user_partitions": user_partitions,
260+
"upstream_link": (
261+
# If the block isn't linked to an upstream (which is by far the most common case) then just
262+
# make this field null, which communicates the same info, but with less noise.
263+
upstream_link.to_json(include_child_info=True) if upstream_link.upstream_ref
264+
else None
265+
),
266+
"validation_messages": validation_messages,
267+
"render_error": render_error,
268+
})
269+
270+
is_published = False
271+
try:
272+
is_published = not modulestore().has_changes(current_xblock)
273+
except ItemNotFoundError:
274+
logging.error('Could not find any changes for block [%s]', usage_key)
275+
276+
container_data = {
277+
"children": children,
278+
"is_published": is_published,
279+
"can_paste_component": is_course,
280+
}
281+
serializer = ContainerChildrenSerializer(container_data)
282+
return Response(serializer.data)

cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
195195
Unit tests for the ContainerVerticalViewTest.
196196
"""
197197

198-
view_name = "container_vertical"
198+
view_name = "container_children"
199199

200200
def test_success_response(self):
201201
"""
@@ -279,6 +279,8 @@ def test_children_content(self):
279279
"version_declined": None,
280280
"error_message": "Linked upstream library block was not found in the system",
281281
"ready_to_sync": False,
282+
"has_top_level_parent": False,
283+
"is_modified": False,
282284
},
283285
"user_partition_info": expected_user_partition_info,
284286
"user_partitions": expected_user_partitions,

0 commit comments

Comments
 (0)