Repository navigation
feat: serve container children from authoring v2 - #39317
Draft
Faraz32123 wants to merge 1 commit into
Draft
Faraz32123 wants to merge 1 commit into
Faraz32123 wants to merge 1 commit into
Conversation
This was referenced Oct 27, 2026
Faraz32123
force-pushed
the
feat/standardize-container-children-api
branch
from
October 27, 2026 12:57
9216513 to
6ad329a
Compare
Contributor
Author
|
Note: This PR is rebased onto
Once those PRs merge, this branch will be rebased to use their shared definitions and the local copies removed. CI may flag missing imports until then. |
Faraz32123
force-pushed
the
feat/standardize-container-children-api
branch
7 times, most recently
from
October 28, 2026 06:31
8f083e9 to
bed8ede
Compare
Adds GET /api/authoring/v2/containers/{usage_key}/children/,
the ADR-conforming replacement for the v1 container children
endpoint.
Part of: #39063 — [API] Containers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Faraz32123
force-pushed
the
feat/standardize-container-children-api
branch
from
October 28, 2026 09:01
bed8ede to
72eb701
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Standardizes the container children read from the
[API] Containerstracking issue (#39063, part of the FC-0118 umbrella #38137):GET /api/contentstore/v1/container/{usage_key_string}/childrenis superseded byGET /api/authoring/v2/containers/{usage_key}/children/. This is the second half of the container pair: the container itself is #39274 (/api/authoring/v2/containers/{usage_key}/), and its PR body assigns this address to this PR.Stacked. This branch sits on #39274 (which sits on #39229) and uses their
CanViewCourse, the usage-key converter route and theviewconstants. The diff againstorigin/pr/39274is what is described below.Why a new version. The contract changes for callers, so it cannot be done in place:
IsAuthenticatedand nothing else: no course-access check anywhere on its path (course_index.py,get_xblock,ContainerHandlerMixin). v2 requires permission to view the course, the checkcontainer_handlerand the course outline already make. This is a deliberate status change for callers without a role on the course (200 becomes 403).get_upstream_infoanswer is a bare list.page,page_size, default 10, maximum 100) and shaped byview:minimal(default) is five fields per child;view=fullis v1's child object with v1's field names.200in v1 ({children: []}and the sections); they are404in v2 (the outline serves the course root).render_errorcarries one fixed message instead of the exception text raised by the block's own render code; the text is logged with the child's key.course_infoblock when asked for the children of one that did not exist.What leaves this response:
is_published,can_paste_component,display_nameandupstream_ready_to_sync_children_info(with theget_upstream_infoparameter that fed the last).display_nameis on the container detail;can_paste_componentis always true for what v2 serves;is_publishedand the container's own upstream readiness have no v2 home yet (see Follow-ups).Change table
GET /api/authoring/v2/containers/{usage_key}/children/(namecontainer_child_list)container/vertical/.../childrenstill 301s to itview=minimal|full,(IsAuthenticated, CanViewCourse), default authentication, standard error envelopecms/lib/spectacular.pydeprecated: trueby one exactSUPERSEDED_PATHSentrytest_containers.py,test_spectacular.pyADRs applied
CourseContainerChild*serializers withhelp_texton every field,allow_nullwhere real data is null,actionstyped as an object, aMeta.ref_nameon each(IsAuthenticated, CanViewCourse); the one legacy check (IsAuthenticated) survives and one is added@extend_schemawith summary, description, examples and the error responses; SDK tag; the seven-key page envelope is publishedViewSetwithlist, mounted withpath()and the shared usage-key converter instead of a router (router routes are regexes and cannot use the converter)CanViewCourseproduce is the envelope; a malformed ori4x://key fails at URL resolution and gets the service's HTML 404, and 405/406 are typedinternalbecause edx-drf-extensions 10.9.0 omits them (both pinned by tests that flip when fixed)course_infois no longer createdcontainer_handleris a different resourceviewselects a representationauthentication_classesview=fullopt-in/api/authoring/v2/containers/{usage_key}/children/, trailing slash, shared converterauthoring, plural collection, nesting of one level, per-resource versionCompliance matrix
v2/serializers/container_children.py; nullable fields checked against real data (a broken library link, an unversioned link, a block with no display type)ContainerChildrenFullViewTest::test_a_link_to_a_library_that_cannot_be_followed_reports_why_and_nothing_else,ContainerChildrenSchemaTest::test_the_two_item_shapes,::test_every_field_of_the_read_is_describedContainerChildrenAccessLegacyPermissionsTest,ContainerChildrenAccessAuthzTestContainerChildrenSchemaTest(8 tests);schema_diff.py compare: 0 BREAKINGViewSet(the data is the modulestore, not rows);path()rather than a router for the converterContainerChildrenRoutingTest::test_the_addressassert_error_envelopeon every 400, 401, 403, 404 in the access, membership and page tests;ContainerChildrenRoutingTest::test_unparsable_or_deprecated_key_is_404pins the gapget_xblock; reads only. Rendering a child forview=fullcan store state for the previewing user in the Django session, as v1 does; that row is the only write besides the permission library's first-lookup policy row, and the default view never touches the sessionContainerChildren*SideEffects*Test(GET and HEAD, both views, AuthZ off and on);ContainerChildrenMembershipTest::test_a_course_info_block_that_does_not_exist_is_not_createdIterablePaginationMixin.paginate_iterable;CourseContainerChildPaginationoverrides onlyget_paginated_response_schema, which the library publishes with four of the seven keysContainerChildrenDefaultViewTest(page size, maximum, links, out of range);ContainerChildrenSchemaTest::test_the_page_has_all_seven_keys_around_the_two_item_shapesauthentication_classeslinetest_bearer_token_is_refused,test_an_inactive_session_is_not_authenticated,test_an_inactive_jwt_caller_*view=minimal(default) /view=full, one level, no recursionContainerChildrenDefaultViewTest,ContainerChildrenParityTestreverse("authoring_v2:container_child_list")is/api/authoring/v2/containers/<key>/children/; shared converter;routed_actions == {"get": "list"}ContainerChildrenRoutingTest;url_snapshot.py check(IsAuthenticated, CanViewCourse). 2 record visibility: anyone who may view the course sees every child, including staff-only and group-restricted ones, exactly as v1 returned them. 3 user filtering: none;view,pageandpage_sizeshape the output and cannot widen visibilityBackward compatibility
Nothing changes for any v1 caller.
v1/is not edited; every v1 address, URL name, body and status stays, including the 301 aliascontainer/vertical/{key}/children.Versions gate (
check_old_versions_untouched.sh --base origin/pr/39274): PASS. Two allowed edits (listed below), four added files.URL gate (
url_snapshot.py check): 2166 routes at head, 2165 at base, no address and no URL name lost, one new API route checked: PASS.Schema diff: 0 BREAKING. Added: operation
GET /api/authoring/v2/containers/{usage_key}/children/, operationIdv2_containers_children_list, 8 components (CourseContainerChild,CourseContainerChildActions,CourseContainerChildFull,CourseContainerChildMessage,CourseContainerChildRead,CourseContainerChildUpstreamChild,CourseContainerChildUpstreamLink,PaginatedCourseContainerChildReadList). Zero operationIds and zerocomponents.schemasnames removed. The v1 operation is newly markeddeprecated: true. The two WARN lines aredrf-spectacularfailing to resolve the platform's default authenticator classes, which every default-authentication view in this schema triggers.Parity (
ContainerChildrenParityTest,ContainerChildrenLibraryLinkedParityTest): every child of a section, subsection, unit, split test and group, with a library link to a block and to a container, a broken link, restricted groups, a validation error, tagging on and off, and a block that fails to render, is compared leaf by leaf with the v1 body.INTENTIONAL_DIFFERENCES, verbatim from the test:is_published|can_paste_component|display_name|upstream_ready_to_sync_children_info(\[\d+\].*)?— v1 also describes the container itself; the page lists only its childrencount|next|previous|num_pages|current_page|start— the page enveloperesults\[\d+\]\.render_error— the cause of a render failure is logged and a fixed message returnedThe comparison fails on an undeclared difference and on a declared one that never occurs. It was mutation-checked: changing
can_copy, returning the exception text, and always returning the fixed message each turn it red.Legacy tests (
v1/views/tests/test_vertical_block.py, includingContainerVerticalViewTest) are not edited and pass.v1's measured behaviour (Django test client, same course): a missing block answers an HTML 500; a signed-in user with no role on the course gets the full children payload (200).
Query counts, a unit with N html children read by course staff (SQL statements / Mongo reads, second identical request):
view=full(page of 10)With units in a subsection v1 is 19 / 10, 23 / 10, 38 / 10; v2 default 11 / 2; v2 full 22 / 6, 26 / 6, 31 / 6.
view=fullcosts what the page holds, not what the container holds (page_size=5over 20 children costs what 5 children cost), and its extra statements beyond v1 are the permission check's own (test_the_full_view_costs_no_more_than_v1_beyond_the_permission_check). Both with policy-based authoring permissions off and on.In-place edits
cms/djangoapps/contentstore/rest_api/v2/authoring_urls.pypath()url_snapshot.py check: no address and no URL name lost; the existing v2 route tests passcms/djangoapps/contentstore/rest_api/v2/views/tests/test_containers.pyV1_CHILDREN,test_the_v1_read_is_deprecated_and_the_children_read_is_notbecomestest_the_v1_read_is_deprecated); nothing else changesOutside any version directory:
cms/lib/spectacular.pygains one exact entry inSUPERSEDED_PATHS(no new constant, no redefined hook);cms/lib/tests/test_spectacular.pygains cases that include other resources' paths and lookalike paths and flips the matching #39274 pin. The superseded view module (v1/views/course_index.py) andv1/urls.pyare not in the diff.Error-format decision
Versioned. v1's error bodies on this route are not parseable shapes any client can rely on (an HTML 500 for a missing block, a bare list for an invalid parameter), and the permission and pagination changes already require a new version. The only readers found read
childrenfrom a success body (the Authoring MFE's unit page and the end-to-end suite); neither reads an error body. Two known gaps, pinned by tests that flip when fixed: keys that fail to parse or arei4x://get the service's HTML 404 at URL resolution (closing it needs a catch-all undercontainers/, which #39274 declined for its route; best done for both together), and 405 and 406 are typedinternalby edx-drf-extensions 10.9.0.Deprecations
GET /api/contentstore/v1/container/{usage_key_string}/childrenis superseded byGET /api/authoring/v2/containers/{usage_key}/children/?view=fulland markeddeprecated: truein the published schema, by one exactSUPERSEDED_PATHSentry. The superseded view is unchanged: no response header, no deprecation notes in code. The 301 aliascontainer/vertical/{usage_key_string}/childrenis not a DRF view, has no schema operation, and is named on the DEPR entry.Tracked on the FC-0118 umbrella DEPR ticket, #39128, which removes them one named release after the release this ships in. This area's entry is posted there once the PR is open.
Gap named, per the deprecation decision for this area: four things leave the response and two of them have no v2 home yet:
is_publishedand the container's ownupstream_ready_to_sync_children_info. The Authoring MFE's compare-containers widget reads the second. #39274 is asked to carry both on the container detail before it merges; until then the window could end before one exists, and the DEPR entry says so.Migration notes for callers: call the v2 address with the trailing slash and
?view=full&page_size=100for the Unit page; readresultsand follownext; errors use the envelope; Bearer is refused; a leaf or course-root key is 404; the caller must be able to view the course;render_erroris a fixed message.Authorization
view_auth_classes(is_authenticated=True):IsAuthenticatedIsAuthenticatedCanViewCourse: 404 for a missing course, a library key or a version-only key before the check; thencourses.view_course/ READ, with AuthZ roles when enabledDeny-path tests, each with GET and HEAD, on both views, with policy-based authoring permissions off (
ContainerChildrenAccessLegacyPermissionsTest) and on (ContainerChildrenAccessAuthzTest): anonymous (401,WWW-Authenticate), signed in with no role (403), enrolled learner (403), limited staff (403), a role on another course (403, and 200 on its own course), unknown course (404 before 403; anonymous 401 before 404), custom course key (403 for everyone), org and course staff / instructor and global staff (200), AuthZ auditor / editor / staff / admin (200), other AuthZ roles (403), legacy staff without a policy role under AuthZ (403), inactive session (401), inactive JWT (200 for a user with a role), Bearer (401), POST/PUT/PATCH/DELETE (405 after authentication).test_v1_lists_the_children_for_a_user_without_a_rolerecords the status change: v1 200, v2 403.Gate report
Run against
origin/pr/39274(stack base, SHA3291339058). Gates run individually due to worktree environment (nouv/.venv; Tutor dev).check_old_versions_untouched.sh)check_code_hygiene.py)CourseContainerChildPaginationsubclass kept for the seven-key schema; documentedschema_diff.py compare)deprecated: true; WARNs are the platform's default-authenticator resolutionurl_snapshot.py check)pytest v1/…/test_vertical_block.py)pytest v2/…/test_container_children.py)test_spectacular.py25 cases PASS; full affected set 606 passed, 3 failed (pre-existing in #39274'stest_containers.py, fail identically at base)ruff,pylint)Re-run against
origin/masterafter rebase (plan §12.4).Follow-ups
is_publishedand the container's own upstream readiness (ready_to_sync_children) on the container detail (feat: serve course containers from authoring v2 #39274), so the compare-containers widget has a v2 source; recorded in the DEPR entry.containers/so keys that fail to parse answer with the envelope instead of the HTML 404 (both container routes together).user_partition_infoanduser_partitionsobjects are candidates for a typed shape in a next version; v1's declared shapes are kept as they are.UpstreamLinkfieldsupstream_name,downstream_key,upstream_linkandis_ready_to_sync_individuallystay dropped, as in v1.CourseContainerChildPaginationis deleted forDefaultPaginationonce edx-drf-extensions documents all seven keys.docs/cms-openapi.yamlis regenerated by the schema workflow, not edited here; the SDK gains a method for the new operation.containerChildrenPathcalls the 301 alias; it moves to the v2 address in a separate change.🤖 Generated with Claude Code