|
1 | 1 | """API Views for course index""" |
2 | 2 |
|
| 3 | +import logging |
| 4 | + |
3 | 5 | import edx_api_doc_tools as apidocs |
4 | 6 | from django.conf import settings |
5 | 7 | from opaque_keys.edx.keys import CourseKey |
|
8 | 10 | from rest_framework.views import APIView |
9 | 11 |
|
10 | 12 | 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 |
13 | 27 | from common.djangoapps.student.auth import has_studio_read_access |
14 | 28 | 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 |
15 | 31 |
|
16 | 32 |
|
17 | 33 | @view_auth_classes(is_authenticated=True) |
@@ -98,3 +114,169 @@ def get(self, request: Request, course_id: str): |
98 | 114 |
|
99 | 115 | serializer = CourseIndexSerializer(course_index_context) |
100 | 116 | 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) |
0 commit comments