Repository navigation
feat: serve the course outline from authoring v2 - #39229
taimoor-ahmed-1 wants to merge 3 commits into
Conversation
The course index existed only at v1, with Bearer auth, a legacy error
body, a slashless address, the whole screen payload as its only shape,
and writes on GET (pre-1900 start dates re-saved, a course overview
built for the clipboard title).
v2 serves GET /api/authoring/v2/courses/{course_key}/outline/. By
default it returns a thin outline tree two levels deep, where every cut
node carries a followable children_url. ?view=full returns v1's screen
payload under v1's names, without the clipboard. Reads write nothing.
Authentication is the platform default, the course-view permission and
its legacy fallback are kept in CanViewCourse, and errors use the
standardized envelope. The v1 course index is marked deprecated in the
CMS schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docstring-only markers naming the v2 successor and DEPR openedx#39128. With docstrings stripped the module matches the base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the pull request, @taimoor-ahmed-1! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Update the status of your PRYour PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate. Where can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
mypy checks rest_api/v2/views, and pytz ships no type stubs, so the import failed make check-types. The standard library's UTC gives the same timezone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This PR standardizes the Course Index functional area (#39066, umbrella #38137) as a new resource,
GET /api/authoring/v2/courses/{course_key}/outline/, following the API standardization ADRsdocs/decisions/0025to0038and OEP-69.GET /api/contentstore/v1/course_index/{course_id}stays mounted and unchanged, and is marked deprecated.Why a new version. The course index exists only at v1, and each change the ADRs require would break v1 callers:
So v1 stays frozen, and v2 is added under the authoring mount #39078 created. The precedent is #39157, which moved textbooks and video settings from v1 to authoring v2.
Change table
GET /api/authoring/v2/courses/{course_key}/outline/{id, category, display_name, children[] | children_url}, two levels deep by default (depth1 to 3). A node cut at the depth limit carrieschildren_url, which re-roots the same resource with?usage_key=<block>&depth=1.usage_keyaccepts any container in the course outline, so everychildren_urlcan be followed.?view=full: v1's screen payload under v1's field names, minusinitial_user_clipboard, which has its own endpoint.showsets the initial expanded state, as in v1.GET /api/contentstore/v1/course_index/{course_id}deprecated: truein the CMS schema, plus a docstring naming the successor.ADRs applied
help_texton every field (including v2-local proctoring-error serializers); nullable and required fields match real responsesIsAuthenticated+CanViewCourse(courses.view_coursewith the legacy fallback); 401, then 404, then 403; thereindex_linkgate kept, with authz off and on@extend_schemawith machine-readable query parameter limits, examples of both read shapes, 4xx referencingErrorResponse,openedx-platform-sdktagViewSetover a service module; query counts pinned, flat with course sizeinternaluntil edx-drf-extensions catalogues them.XBLOCK_UPDATEDorCourseOverviewsave happens on GET. v1 still does both, pinned.authentication_classesdeclaredchildren_urlto go deeper; the screen payload is opt-incourses/{course_key}/outline/, URL namecourse_outline, mounted inv2/authoring_urls.pyCompliance matrix
serializers/course_outline.py:CourseOutlineQuery,CourseOutline(recursive node),CourseOutlineFull,CourseOutlineInitialState,CourseOutlineDeprecatedBlocks,CourseOutlineProctoringError/Setting(byte-identical to the shared ones, documented). Six nullable fields.OutlineSchemaTest(descriptions on every reachable property, nullable and required sets); every test response validated against the published schemaviews/permissions.pyCanViewCourse, reindex gate per fieldOutlineAccessLegacyPermissionsTest,OutlineAccessAuthzTest(each case compared with v1, including a role on another course),OutlineFullViewReindexLinkAuthzTestminimum/maximum/default,viewenum and default; two examples; SDK tagOutlineSchemaTestcourse_outline_service.py, onebulk_operationswalkOutlineQueryCountTest,OutlineQueryCountAuthzTest,OutlineRerootQueryCountTestStandardizedErrorMixin; fixed messages that echo no keysOutlineDefaultViewTest; open items pinned inOutlineRoutingTestOutlineReadSideEffectsLegacyPermissionsTest,OutlineReadSideEffectsAuthzTest(SQL and Mongo writes, signals, first request on a fresh database, HEAD, both views, role-holder reader)OutlineDefaultViewTest,OutlineOtherContainersTest(follows everychildren_url, includingconditional,library_contentandsplit_test)SUPERSEDED_PATHS; DEPR entry on #39128cms/lib/tests/test_spectacular.py; DEPR entryOutlineRoutingTest; urls gateBackward compatibility
course_index.pychanges only in docstring lines; with docstrings stripped, its AST equals master.v2/authoring_urls.pygains one route. Protected, untouched:utils.py,views/course.py,xblock_storage_handlers/view_handlers.py(the shared outline builder),rest_api/serializers/common.py, the content-staging API and models,view_utils.py, the authz decorators, the CourseOverview model,docs/cms-openapi.yaml.deprecated: true. Warnings:ContainerChildrenView's existing warnings, moved by the docstring lines.?view=fullvs v1, through the test client). The comparison is typed, keeps empty containers, and fails on a difference that is not declared or a declared one that does not occur. Declared differences:initial_user_clipboard: always absent in v2.course_release_date,start,release_date,released_to_studentsandvisibility_state(v1 repairs these dates on read and reports "Set Date"/unscheduled; v2 reports them as stored).course_structurestays a free-form object, as in v1. The sharedcreate_xblock_infobuilds it, andOVERRIDE_CREATE_XBLOCK_INFOplugins can extend it. Its node keys are documented in the schema.OutlineFullViewNodeKeysTestpins the key set per block role, so a change to the shared builder fails loudly.In-place edits
rest_api/v1/views/course_index.pyrest_api/v2/authoring_urls.pyrest_api/v2/course_outline_service.py,serializers/course_outline.py,views/course_outline.py,views/tests/test_course_outline.pyviews/permissions.py(shared, unversioned)CanViewCourseandCOURSE_NOT_FOUND_MESSAGEappended; existing classes byte-identical; the textbooks, video_settings and certificates tests stay greencms/lib/spectacular.py,cms/lib/tests/test_spectacular.pySUPERSEDED_PATHSentry; an existing unmarked-paths test is rewritten as a loop with its assertion keptMigration (v1 to v2)
/api/authoring/v2/courses/{course_key}/outline/?view=full. Note the trailing slash; a slashless GET is redirected (301), other methods are not.view=fullbody keeps v1's top-level keys and per-node shape,tag_counts_by_blockincluded, minusinitial_user_clipboard. Read the clipboard from/api/content-staging/v1/clipboard/.initial_state,lms_link,notification_dismiss_url,reindex_link,rerun_notification_id,created_on.?format=concisehas no v2 equivalent. v1's concise form also listed the components inside units; both v2 views stop at units.Error-format decision
Versioned: v2 uses the standardized envelope, and v1 keeps its legacy
developer_message/error_codebody.Deprecations
GET /api/contentstore/v1/course_index/{course_id}isdeprecated: truein the CMS schema, and its docstrings name the v2 successor. No runtime header (frozen route). The DEPR record is an entry on #39128: this comment. Folding this entry, and the ones for #39102, #39186 and #39227, into the issue body is pending. Window, per #39128: the marking ships in the next named release after Verawood, and v1 stays mounted until one named release after that.Authorization
IsAuthenticatedIsAuthenticated, firstverify_course_existscourse_keyconverter (malformed keys), thenCanViewCourse: version-only key or unknown course gives 404 before any role checkuser_has_course_permission(courses.view_course, READ)CanViewCourse, the same calluser_can_reindex_courseforreindex_linkcourses.publish_course_content)Nobody gains access. Bearer callers and inactive browser sessions lose it.
Gate report
run_gates.sh --base origin/master --service cms --prefix /api/authoring/v2/, run in a container with the branch's pinned requirements (Python 3.12, edx-drf-extensions 10.9.0, openedx-authz 1.24.0). No gate was skipped.Query counts (warm request; SQL / Mongo reads / Mongo writes)
view=fullThese counts are the same for a 1-unit and a 36-unit course.
Coordination
v2/authoring_urls.pyandviews/permissions.py. It addsHasFullCourseKeyfor version-only keys, whichCanViewCoursealso handles. Whichever lands second should rebase onto the other and compose the classes.cms/lib/spectacular.py(SUPERSEDED_PATHS).view=fullcosts no more than v1.Follow-ups
CourseOverviewrow.?view=full.🤖 Generated with Claude Code