Skip to content

Commit 8ec46fe

Browse files
feat(adr-0027): add @extend_schema to Xblock v1 viewset (#38834)
Follow-up from PR #38773 review: Xblock v1 previously had no OpenAPI schema decoration, which was flagged as a pre-existing gap. This adds drf-spectacular @extend_schema to all five viewset actions (create, retrieve, update, partial_update, destroy) so the endpoints show up with proper request/response bodies, path/query parameters, and error codes in the generated OpenAPI 3.x schema. Notes: - Documents the ADR 0036 ``?view=minimal`` query parameter in the schema (enum=["minimal"]) so consumers can discover it. - Documents the legacy ``?fields=`` selector (graderType / ancestorInfo / customReadToken) as ``deprecated=True`` — its semantics are pass-through / type-of-response, not the ADR 0036 CSV subset convention. - Shared parameter and response building blocks are declared at module scope to keep the per-action decorators readable. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f3cabe0 commit 8ec46fe

1 file changed

Lines changed: 143 additions & 1 deletion

File tree

  • cms/djangoapps/contentstore/rest_api/v1/views

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

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import logging
3434

3535
from django.http import JsonResponse
36-
from drf_spectacular.utils import extend_schema
36+
from drf_spectacular.utils import OpenApiParameter, OpenApiRequest, OpenApiResponse, extend_schema
3737
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
3838
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
3939
from opaque_keys import InvalidKeyError
@@ -55,6 +55,71 @@
5555

5656
log = logging.getLogger(__name__)
5757

58+
59+
# ---------------------------------------------------------------------------
60+
# ADR 0027 — shared OpenAPI parameter and response building blocks
61+
# ---------------------------------------------------------------------------
62+
_USAGE_KEY_PATH_PARAMETER = OpenApiParameter(
63+
name="usage_key_string",
64+
description=(
65+
"Usage key identifying the xblock (e.g. "
66+
"``block-v1:edX+DemoX+Demo_Course+type@vertical+block@abcd``). Also "
67+
"accepts legacy ``i4x://`` locators."
68+
),
69+
required=True,
70+
type=str,
71+
location=OpenApiParameter.PATH,
72+
)
73+
74+
# ADR 0036 — declare the ``?view=minimal`` preset in the OpenAPI schema so
75+
# consumers (Swagger UI, generated SDK clients) can discover it. Only the
76+
# ``retrieve`` action honours this parameter today.
77+
_VIEW_QUERY_PARAMETER = OpenApiParameter(
78+
name="view",
79+
description=(
80+
"ADR 0036 response preset. ``minimal`` drops heavy/contextual xblock "
81+
"fields (``data``, ``metadata``, ``fields``, ``student_view_data``, "
82+
"``edited_on``, ``published`` …) and keeps only the structural fields "
83+
"(``id``, ``display_name``, ``category``, ``children``, "
84+
"``has_children``, ``studio_url``). Omit the parameter to receive "
85+
"the full xblock response."
86+
),
87+
required=False,
88+
type=str,
89+
location=OpenApiParameter.QUERY,
90+
enum=["minimal"],
91+
)
92+
93+
# ADR 0036 — the underlying ``retrieve_xblock_response`` accepts a legacy
94+
# ``?fields=`` selector with **type-of-response** semantics (not the ADR 0036
95+
# CSV-subset semantics). Documented here as a deprecated parameter so callers
96+
# can see it in Swagger UI and know it's a legacy pass-through.
97+
_LEGACY_FIELDS_QUERY_PARAMETER = OpenApiParameter(
98+
name="fields",
99+
description=(
100+
"**Legacy pass-through** (v0/v1 semantics). Selects a *type* of "
101+
"response rather than a subset of top-level keys:\n"
102+
" * ``fields=graderType`` — return the grader-type value directly\n"
103+
" * ``fields=ancestorInfo`` — return concise ancestor info\n"
104+
" * ``fields=customReadToken`` — include parent + children on the "
105+
"response\n"
106+
"Note: this is **not** the ADR 0036 ``?fields=`` CSV subset "
107+
"convention. New callers should use ``?view=minimal`` instead."
108+
),
109+
required=False,
110+
type=str,
111+
location=OpenApiParameter.QUERY,
112+
deprecated=True,
113+
)
114+
115+
_COMMON_ERROR_RESPONSES = {
116+
401: OpenApiResponse(description="The requester is not authenticated."),
117+
403: OpenApiResponse(description="The requester does not have permission for this xblock's course."),
118+
404: OpenApiResponse(description="The requested xblock does not exist."),
119+
406: OpenApiResponse(description="Requested representation is not available (e.g. non-JSON ``Accept``)."),
120+
}
121+
122+
58123
# ADR 0036 — top-level keys kept when ``?view=minimal`` is requested. Chosen so
59124
# the response is structurally complete (callers can navigate the tree by id
60125
# and fetch full nodes on demand) without any heavy/contextual fields
@@ -152,12 +217,49 @@ def initial(self, request, *args, **kwargs):
152217
self.course_key = None
153218
super().initial(request, *args, **kwargs)
154219

220+
@extend_schema(
221+
summary="Create an xblock",
222+
description=(
223+
"Create a new xblock under a parent block. The ``parent_locator`` "
224+
"field on the request body identifies the parent and (implicitly) "
225+
"the course."
226+
),
227+
request=OpenApiRequest(XblockSerializer),
228+
responses={
229+
200: OpenApiResponse(
230+
response=XblockSerializer,
231+
description="The xblock was created successfully.",
232+
),
233+
400: OpenApiResponse(description="Request body failed validation."),
234+
**_COMMON_ERROR_RESPONSES,
235+
},
236+
)
155237
@expect_json_in_class_view
156238
@validate_request_with_serializer
157239
def create(self, request):
158240
"""Create a new xblock under the given parent."""
159241
return create_xblock_response(request)
160242

243+
@extend_schema(
244+
summary="Retrieve an xblock",
245+
description=(
246+
"Retrieve an xblock (and, by default, its nested tree) by usage "
247+
"key. Supports ADR 0036 ``?view=minimal`` to strip contextual "
248+
"fields, plus the legacy ``?fields=`` type-of-response selector."
249+
),
250+
parameters=[
251+
_USAGE_KEY_PATH_PARAMETER,
252+
_VIEW_QUERY_PARAMETER,
253+
_LEGACY_FIELDS_QUERY_PARAMETER,
254+
],
255+
responses={
256+
200: OpenApiResponse(
257+
response=XblockSerializer,
258+
description="The xblock representation.",
259+
),
260+
**_COMMON_ERROR_RESPONSES,
261+
},
262+
)
161263
@expect_json_in_class_view
162264
def retrieve(self, request, usage_key_string=None):
163265
"""
@@ -172,18 +274,58 @@ def retrieve(self, request, usage_key_string=None):
172274
response = _apply_minimal_view(response)
173275
return response
174276

277+
@extend_schema(
278+
summary="Update an xblock",
279+
description="Fully update an xblock identified by its usage key.",
280+
parameters=[_USAGE_KEY_PATH_PARAMETER],
281+
request=OpenApiRequest(XblockSerializer),
282+
responses={
283+
200: OpenApiResponse(
284+
response=XblockSerializer,
285+
description="The xblock was updated successfully.",
286+
),
287+
400: OpenApiResponse(description="Request body failed validation."),
288+
**_COMMON_ERROR_RESPONSES,
289+
},
290+
)
175291
@expect_json_in_class_view
176292
@validate_request_with_serializer
177293
def update(self, request, usage_key_string=None):
178294
"""Fully update an xblock."""
179295
return update_xblock_response(request, usage_key_string)
180296

297+
@extend_schema(
298+
summary="Partially update an xblock",
299+
description=(
300+
"Partially update an xblock identified by its usage key. Only the "
301+
"fields present in the request body are updated."
302+
),
303+
parameters=[_USAGE_KEY_PATH_PARAMETER],
304+
request=OpenApiRequest(XblockSerializer),
305+
responses={
306+
200: OpenApiResponse(
307+
response=XblockSerializer,
308+
description="The xblock was updated successfully.",
309+
),
310+
400: OpenApiResponse(description="Request body failed validation."),
311+
**_COMMON_ERROR_RESPONSES,
312+
},
313+
)
181314
@expect_json_in_class_view
182315
@validate_request_with_serializer
183316
def partial_update(self, request, usage_key_string=None):
184317
"""Partially update an xblock."""
185318
return update_xblock_response(request, usage_key_string)
186319

320+
@extend_schema(
321+
summary="Delete an xblock",
322+
description="Delete an xblock identified by its usage key.",
323+
parameters=[_USAGE_KEY_PATH_PARAMETER],
324+
responses={
325+
200: OpenApiResponse(description="The xblock was deleted successfully."),
326+
**_COMMON_ERROR_RESPONSES,
327+
},
328+
)
187329
@expect_json_in_class_view
188330
def destroy(self, request, usage_key_string=None):
189331
"""Delete an xblock."""

0 commit comments

Comments
 (0)