Repository navigation
feat: serve course custom pages and tabs from authoring v1 - #39305
taimoor-ahmed-1 wants to merge 13 commits into
Conversation
The v0 advanced settings view checks course existence with the @verify_course_exists decorator and access with an inline check_course_advanced_settings_access call in each handler. The conforming views need these as permission classes. IsExistingCourse answers 404 for a course key that names no course run: an unknown course, a key that names only a version, or a library key. CanViewAdvancedSettings and CanManageAdvancedSettings make the same check_course_advanced_settings_access(read|write) call the v0 view makes, so the policy-permission and legacy Studio-access branches are unchanged. Existing classes in the module are untouched.
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id} answers
errors with developer_message bodies and some failures with a 500,
accepts Bearer tokens, returns every settings field by default, and
reports a refused value with the field parser's own text, which can be
Python exception text.
GET/PATCH /api/authoring/v1/courses/{course_key}/advanced_settings/ is
its successor. Errors use the standard envelope; a refused value is
reported under its setting name with one fixed message and the original
text is logged. The default read is the set of settings course authors
edit (v0 with fetch_all=0); view=full returns every field and fields
narrows either view. Authentication keeps the session class that admits
inactive users, as the legacy endpoint did; Bearer is dropped. Access is
IsAuthenticated, IsExistingCourse and a per-action class making v0's
read or write check, and an action without a declared permission is
never served. Parity tests compare v1 with v0 through the test client;
the declared differences are the error envelope and the fixed message.
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id} now has a
conforming successor at /api/authoring/v1/courses/{course_key}/advanced_settings/.
Both v0 operations are marked deprecated in the CMS schema through an
exact SUPERSEDED_PATHS entry, and the view's docstrings name the
successor, how each v0 read maps to a v1 read, and the deprecation
ticket. The view itself is unchanged. The schema hook test that used
this path as its example of an unrelated contentstore path now uses the
heartbeat, which is not deprecated.
Importing cms.djangoapps.contentstore.rest_api.v1.authoring_urls at module level builds its routes before cms/urls.py registers the course_key path converter, so collecting the module failed with an ImproperlyConfigured error. The URL-name test now imports it when it runs, as the youtube transcript tests do. Also wraps a line pylint reported as too long.
The first CI run of the module showed three expectations that did not match the platform: - The legacy role checks consult the policy engine, which admits superusers, so a superuser who is not global staff is served by v0 and v1 alike. The test now expects that. - The proctoring provider field keeps its default unless proctored exams are enabled, so the proctoring tests enable them. - The JWT middleware appends NotJwtRestrictedApplication to the permissions of views that accept JWTs, so the wiring test compares only the classes the view declares.
The side-effect recorder, the body comparison helpers, the schema helpers and the client helpers in the advanced settings tests are not specific to advanced settings. They move unchanged, under public names, into v1/views/tests/helpers.py so the other authoring v1 endpoint tests reuse one copy instead of each carrying its own.
edx-drf-extensions' DefaultPagination returns seven envelope keys but publishes a schema with four, so generated clients drop num_pages, current_page and start. DocumentedDefaultPagination keeps the library's pagination and publishes all seven keys, each with an example. It lives in an unversioned module for every authoring list to reuse, and is to be deleted once the library documents the envelope itself.
The v0 tab views check courses.manage_pages_and_resources inline with a legacy write-access fallback before reordering custom pages or changing a tab. CanManagePagesAndResources makes the same call, beside the existing CanViewPagesAndResources. Existing classes are unchanged.
The v0 tabs endpoints return the custom pages as a bare array, reorder
them through a POST verb that answers a missing page with 500 and stores
a repeated page twice, change a tab's visibility through a POST verb that
echoes its input in errors, and accept Bearer tokens.
GET /api/authoring/v1/custom_pages/?course_key=... lists a course's
custom pages a page at a time in course order; a custom page is
addressed by its own usage key, so the list is top level with the course
in the query. PUT /api/authoring/v1/courses/{course_key}/custom_page_order/
replaces their order, with pages left out kept after the listed ones.
PATCH /api/authoring/v1/courses/{course_key}/tabs/{tab_id}/ shows or
hides a built-in tab and returns it. Errors use the standard envelope
with fixed messages. Access makes v0's view and manage checks. Parity
tests compare the stored tabs and each refusal with v0.
The three v0 tab operations now have conforming successors under /api/authoring/v1/. They are marked deprecated in the CMS schema through exact SUPERSEDED_PATHS entries, and the views' docstrings name the successors and the deprecation ticket. The views themselves are unchanged.
|
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. |
The authentication comment narrated where the classes came from; it now says what an inactive session gets: staff status admits it, and its roles count only on courses where policy-based permissions are on. The superuser test claimed to cover a superuser who is not global staff. Saving a superuser sets is_staff, so the factory user is global staff; the test is renamed and its docstring gives that reason.
Faraz32123
left a comment
There was a problem hiding this comment.
Reviewed locally (this PR's 5 commits): 429 passed, schema diff clean (0 breaking). Auth, course existence, list/reorder/settings permissions, rows and write paths match v0. Looks good overall, a few points:
- Deprecation notes in the v0 docstrings (inline).
- Minor: error
detailis the Python repr of the errors dict (declared follow-up for edx-drf-extensions). pylint cmsis red on two C0116 inv1/views/tests/test_custom_pages.py(514, 1195).- PR body still lists tests/schema as SKIPPED.
- Inherited, FYI only: courses that already have a duplicated custom page (v0 reorder can store one) list it twice on v1 too.
All findings were compared with the previous version's behaviour; anything inherited from it is marked as such.
| Deprecated: use ``GET /api/authoring/v1/custom_pages/?course_key={course_key}`` instead. | ||
| See https://github.com/openedx/openedx-platform/issues/39128. |
There was a problem hiding this comment.
Can we drop this deprecation note? We agreed on #39100 not to add DEPR notes in docstrings. This docstring is the published description of the v0 operation, so it changes the legacy schema text and puts an issue URL into it (which also makes the OEP-69 tick untrue). SUPERSEDED_PATHS plus the #39128 entry already carry the deprecation. Same for the other notes in this file.
| """A new order for a course's custom pages.""" | ||
|
|
||
| custom_pages = serializers.ListField( | ||
| child=_CustomPageItemField(allow_null=True, help_text="Usage key of one of the course's custom pages."), |
There was a problem hiding this comment.
allow_null=True publishes the item as string | null, but a null item is always refused with 400. Generated SDKs will accept a value the server rejects. Could the list validator refuse null instead?
There was a problem hiding this comment.
| if "is_hidden" not in changes: | ||
| return tab |
There was a problem hiding this comment.
Declared, but worth a second look: a misspelled body like {"is_hiden": true} returns 200 and changes nothing, where v0 gave 400. Maybe refuse a body with no known member?
There was a problem hiding this comment.
Changed my mind on this one: a body naming no member a tab accepts ({}, {"is_hiden": true}) is now 400 with a fixed message under non_field_errors, as v0 was 400 (8d66706). That removes the "change without is_hidden" row from the declared differences; the case is a parity error pair now. One knock-on: the body is read before the tab is looked up, so an empty body to an unknown tab is 400, not 404, which v0 also did.
| class Meta: | ||
| ref_name = "CustomPageListQuery.v1" | ||
|
|
||
| def validate_course_key(self, value): |
There was a problem hiding this comment.
Since the course key moved from path to query, an unencoded + decodes to spaces and gives 400. Could the parameter description / migration notes say the value must be percent-encoded?
There was a problem hiding this comment.
The parameter description now says to percent-encode the value, with course-v1%3AedX%2BDemoX%2B2026 as the example and the reason (an unencoded + reads as a space), and a test sends the unencoded form and checks the 400 (8d66706). The migration notes in the description say the same.
A view docstring is the published description of the v0 operation, so the notes changed the legacy schema text and the OPTIONS body. The deprecation is carried by SUPERSEDED_PATHS and the openedx#39128 entry; the v0 file is back to its base content.
A null item in a custom page order was published as nullable but always refused; the list now refuses it before the items are read, with the same fixed message as any other unusable item, and the schema says string. A tab change without is_hidden, such as a misspelt member, was accepted as no change where v0 answered 400; it is refused with a fixed message under non_field_errors. The course_key query parameter says the value must be percent-encoded. Two test helpers gain docstrings.
Summary
Part of #39059
Standardizes the custom pages and tabs resources of the Course Management area (#39059, part of #38137). This is PR C of the eight resource-group PRs for the area; #39303 (advanced settings) is PR A.
Three conforming operations replace the three v0 tab endpoints:
deprecated: true)authoring_v1:)GET /api/contentstore/v0/tabs/{course_id}GET /api/authoring/v1/custom_pages/?course_key={course_key}custom_page_listPOST /api/contentstore/v0/tabs/{course_id}/reorderPUT /api/authoring/v1/courses/{course_key}/custom_page_order/course_custom_page_orderPOST /api/contentstore/v0/tabs/{course_id}/settings?tab_id=PATCH /api/authoring/v1/courses/{course_key}/tabs/{tab_id}/course_tab_detailIt applies ADRs 0025–0038 and OEP-69.
Two resources, not one. v0's "tabs" endpoints act on two different things. The list and the reorder only ever see custom pages (static tabs): v0's GET keeps only
static_tabentries, and its reorder only moves static tabs. The settings endpoint only ever succeeds on built-in tabs (progress, dates, wiki and the like), because a static tab is never hideable (StaticTabkeepsCourseTab.is_hideable = False). So v1 names them separately:static_tabblock with its own usage key: itsidin the list, the item of the reorder body, and/api/authoring/v1/xblocks/{usage_key}/all address it by that key. ADR 0038 rule 8 puts such a resource at the top level with its parent in the query (the ADR's ownunits/?course_key=example).course_keyis required.courses/{course_key}/custom_page_order/, replaced withPUT. v0'sreorderverb becomes the noun (rule 10).tab_id(progress,dates,wiki), so they stay under the course:courses/{course_key}/tabs/{tab_id}/, withPATCH {"is_hidden": …}returning the tab. Only the member is routed: listing every tab is not something v0 offered.RISK, stated in the schema: a custom page is also one of the course's tabs (
tab_idstatic_tab_<url_slug>), and the list shows thattab_id.PATCH …/tabs/static_tab_<url_slug>/still answers 400 onis_hidden, as v0 does, because custom pages cannot be hidden. The PATCH description says so.Why a new version and not a fix in place. Each of these changes the contract for v0's callers (the Pages & Resources custom pages screen calls the list and the reorder), so they ride v1 (ADR 0037):
PUTwith a new body, and the tab change becomesPATCHon the member returning the tab;Changes
feat: add a shared paginator that documents all seven envelope keysDefaultPaginationpaging unchanged (page,page_size, 10 by default, at most 100)cms/djangoapps/contentstore/rest_api/pagination.py:DocumentedDefaultPagination, which publishes all seven envelope keys, each with an example. For this PR and the later Course Management listsfeat: add the pages and resources management permission classCanViewPagesAndResourcesunchangedCanManagePagesAndResources: the sameuser_has_course_permission(courses.manage_pages_and_resources, WRITE)call v0's reorder and settings views make inlinefeat: serve course custom pages and tabs from authoring v1views/tabs.py(get_course_tabs,reorder_tabs_handler,edit_tab_handler), which are not editeddocs: mark the v0 tabs endpoints deprecated in favour of authoring v1deprecated: truein the schema (three exactSUPERSEDED_PATHSentries); the docstring notes it added are reverted belowdocs: remove the deprecation notes from the v0 tabs docstringsSUPERSEDED_PATHSand the #39128 entry carry the deprecationfix: refuse a null page item and a tab change that names no memberstring; a tab change naming no accepted member ({}, a misspeltis_hidden) is 400 with a fixed message undernon_field_errors, as v0 was 400; thecourse_keyquery parameter says the value must be percent-encodeddocs: describe what an inactive session and a superuser are allowedis_staff)ADRs applied
CourseTab.v1,CustomPage.v1), request (CustomPageOrder.v1,CourseTabUpdate.v1) and query (CustomPageListQuery.v1) serializers,help_texton every field, explicit.v1component namesIsAuthenticated, the course key,IsExistingCourse, then the view or manage permission v0 checked inline; the list refuses any action it was not written for; deny matrix with policy permissions off and on@extend_schemaon all three with summary, description (every 400 message named), parameters, examples and every 4xx asErrorResponse; all three taggedopenedx-platform-sdkViewSets over a service module, mounted withpath(...)like every route inauthoring_urls.py, because a router's regex routes cannot use the shared path convertersinternaland a malformed course key on the nested routes gets the CMS HTML 404 (library and routing gaps, pinned by tests);detailstill carries the library'sErrorDetail(...)text for validation errors whileerrorsis cleanPolicyCacheControlrow on the first permission check against a database that lacks it, as on v0page,page_size10 by default, at most 100) through the new sharedDocumentedDefaultPagination, and the schema publishes all seven keys(JwtAuthentication, SessionAuthenticationAllowInactiveUser)declared with its reason, as ADR 0034 requires for an endpoint that usedSessionAuthenticationAllowInactiveUser; Bearer droppedsettingsmap each) and a single tabdeprecated: truein the schema throughSUPERSEDED_PATHS; the DEPR entry on #39128 is posted; the removal follows [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128's window rather than a named releasecustom_pages/?course_key=(rule 8),courses/{course_key}/custom_page_order/(rule 10),courses/{course_key}/tabs/{tab_id}/; shared converter, trailing slash, version-free URL names; v0 names keptget_queryset(); the layering is shown in the matrixCompliance matrix
CourseTabSerializer(CourseTab.v1:type,title(null for custom pages),is_hideable,is_hidden,is_movable,course_staff_only,name,tab_id,settings) andCustomPageSerializer(CustomPage.v1, adds the usage keyid) give the same item v0's list returns. Request serializers for the order and the tab change; a query serializer forcourse_key. Explicitref_names, becauseCourseTabalready collides platform-wideCustomPagesSchemaTest(test_the_tab_and_page_components,test_every_member_is_described,test_the_reorder_takes_json_naming_the_pages_and_answers_without_a_body,test_the_tab_change_takes_an_optional_is_hidden_and_returns_the_tab);CustomPagesListTest.test_the_body_matches_the_published_schema; hygiene H9course_keyfrom the query after the login check and before the course checks, so the order is 401, 400 (missing or invalid key), 404, 403.IsExistingCourseis unchanged: a permission step sets the view'scourse_keyargument it reads. The list refuses any action other thanlistand OPTIONS with a server error. The order and tab viewsets check the manage permission for every actionCustomPagesAccessLegacyPermissionsTest,CustomPagesAccessAuthzTest(each access case asserts v0's answer too);CustomPagesPermissionWiringTest(test_the_listing_refuses_any_other_action,test_a_routed_action_the_listing_does_not_check_is_never_served);CustomPagesCourseKeyQueryTest(test_the_caller_is_authenticated_before_the_course_key_is_read,test_the_course_key_is_read_before_the_course_is_looked_up_or_the_caller_checked)v1_custom_pages_list,v1_courses_custom_page_order_update,v1_courses_tabs_partial_update.course_keyis a required query parameter. The PUT description names all four 400 messages; the PATCH description says custom pages cannot be hidden there. All three are taggedopenedx-platform-sdk; the deprecated v0 operations lose the tag throughuntag_deprecated_operationsCustomPagesSchemaTest(test_the_three_operations_are_published_for_the_sdk,test_the_list_parameters,test_the_reorder_documents_each_refusal_message,test_the_tab_change_says_custom_pages_cannot_be_hidden_there,test_the_examples_are_valid_bodies,test_the_v0_operations_are_deprecated_and_left_out_of_the_sdk)CourseCustomPagesViewSet,CourseCustomPageOrderViewSet,CourseTabsViewSet(eachStandardizedErrorMixinfirst, thenviewsets.ViewSet) overv1/custom_pages_service.py. They are plain ViewSets because the data is the course's tab list in the modulestore. Deviation: mounted withpath(), not a routerCustomPagesRoutingTest.test_the_addresses;CustomPagesListingCost{LegacyPermissions,Authz}TestStandardizedErrorMixinfirst. 401authn, 403authz, 404not-found(unknown, version-only or library course; a course the modulestore does not return; atab_idthe course does not have; a page out of range), 400validationwitherrorskeyed by member. Every refusal uses a fixed message that never echoes the request:course_key; the four reorder messages (not a usage key, not a custom page of this course, listed twice, could not be saved);is_hiddenon a tab that cannot be hidden. When the reorder helper fails, its own text is logged with the course key and not returned. Gaps pinned by tests that flip when fixed: malformed JSON (400), 405, 406 and 415 are typedinternal(edx-drf-extensions 10.9.0 does not catalogue them); a malformed key on the nested routes gets the CMS HTML 404 at routing;detailrenders the validation detail with the library'sErrorDetail(...)text, whileerrorscarries the plain messagesCustomPageOrderTest(test_an_item_that_is_not_a_string_holding_a_usage_key_is_refused_with_the_same_message,test_a_failure_saving_the_order_is_reported_without_its_text);CourseTabUpdateTest(test_a_tab_that_cannot_be_hidden_is_refused,test_a_tab_the_course_does_not_have_is_404);CustomPagesCourseKeyQueryTest.test_a_value_that_is_not_a_course_key_is_refused_without_echoing_it;CustomPagesRoutingTest(test_a_malformed_json_body_is_a_400_typed_internal,test_an_unsupported_media_type_is_a_415_typed_internal,test_other_methods_are_not_allowed,test_unsupported_accept_is_refused_with_406,test_an_unparsable_or_slash_form_key_is_an_html_404)PolicyCacheControlrow, which inserts it (one INSERT, itspre_save/post_save). That write belongs to the permission library, happens on v0 too, and is allow-listed by name. The recorder's own self-tests live with the shared helpers in #39303CustomPagesListingSideEffects{LegacyPermissions,Authz}Test(test_listings_have_no_side_effects,test_the_v0_listing_makes_the_same_first_lookup_write)IterablePaginationMixinwithDocumentedDefaultPagination. Why a local subclass: edx-drf-extensions 10.9.0'sDefaultPaginationreturns seven keys but publishes a response schema with four, so generated clients would dropnum_pages,current_pageandstart. The subclass only addsget_paginated_response_schema, with an example per key so the generator can wrap a list example. It lives in an unversioned module so the later Course Management lists reuse it. The library fix is item L3 (to be filed); v2 textbooks'CourseTextbookPaginationis a second copy, in a frozen fileCustomPagesListTest(test_the_envelope_has_the_seven_keys,test_page_size_splits_the_pages,test_the_default_page_size_is_ten,test_the_page_size_is_at_most_one_hundred,test_a_page_out_of_range_is_404);SharedPageEnvelopeTest;CustomPagesSchemaTest.test_the_list_publishes_the_seven_envelope_keys; hygiene H3 WARN (explained below)CustomPagesListTest(test_only_the_custom_pages_are_listed_in_course_order,test_other_query_parameters_are_ignored)(JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented at the declaration: an inactive account's browser session stays authenticated, so the course permission decides: staff status admits it, and its roles count only on courses where policy-based permissions are on. Bearer is dropped. Same as #39303CustomPagesAuthenticationTest(test_a_jwt_caller_may_list_reorder_and_change_tabs,test_a_session_caller_may_list_reorder_and_change_tabs,test_an_inactive_session_is_authenticated,test_a_bearer_token_is_refused); hygiene H4v1/. v0 not edited: the file is identical to the base.deprecated: truethrough three exactSUPERSEDED_PATHSentries; no runtime header (frozen routes). DEPR: entry on #39128; the removal follows #39128's windowCmsMarkSupersededPathsTest.test_the_tab_operations_are_marked_and_their_successors_are_not;CustomPagesSchemaTest(test_the_v0_operations_are_deprecated_and_left_out_of_the_sdk,test_only_superseded_or_migrated_addresses_are_deprecated)tab_idas the tab's natural key; sharedcourse_keyconverter on the nested routes; trailing slash; version-free URL namesCustomPagesRoutingTest(test_the_addresses,test_the_v0_addresses_are_unchanged,test_the_url_names_are_unique_in_the_namespace,test_the_course_is_no_longer_part_of_the_list_address,test_the_list_address_without_its_trailing_slash_redirects_to_it,test_v0_still_takes_a_slash_separated_key_that_v1_does_not);CustomPagesCourseKeyQueryTest; url_snapshot rules check on a hand-built snapshot (the urls gate itself was skipped)get_course_tabs, keeping custom pages only, as on v0; (3) user selection: none beyondpage/page_size, which cannot widen what is listedCustomPagesCourseKeyQueryTest.test_each_course_lists_its_own_pagesBackward compatibility
v0 is unchanged.
v0/views/tabs.pyis not in the diff. Its routes, views and URL names are untouched, and v0's own tests are not edited.Parity. v1 is compared with v0 through the Django test client, with policy permissions off and on:
The only declared body differences, verbatim from the test module:
The comparison fails both on an undeclared difference and on a declared difference that never occurs (
_ParityMixin.test_every_declared_difference_occurs).Status and request differences from v0. Each is listed in the test module and observed against v0's own answer:
As a table, with the request-shape changes that carry no status difference:
GET v0/tabs/{course_id}, bare arrayGET v1/custom_pages/?course_key=, 7-key envelope, 10 per page by defaultCustomPagesResponseDifferencesTest.test_the_list_is_paginatederrors.course_key…test_a_slash_separated_course_keyPOST …/reorderwith[{tab_id}|{tab_locator}]PUT …/custom_page_order/with{"custom_pages": [<usage key>, …]}; same stored tabs_ParityMixin.test_reorders_store_the_same_tabs…test_pages_left_out_of_a_reorderurl_slug…test_a_page_of_another_course_with_the_same_url_slug…test_a_reorder_naming_no_page…test_a_reorder_naming_a_page_twicePOST …/settings?tab_id=→ 204, no bodyPATCH …/tabs/{tab_id}/→ 200 with the tab…test_a_visibility_change_returns_the_tab…test_an_unknown_tabis_hidden({}, or a misspelled member)is_hiddennon_field_errors…test_a_change_naming_no_known_member_is_refused_by_both;CourseTabUpdateTest.test_a_body_without_is_hidden_is_refused_and_changes_nothingis_hidden_ParityMixin.test_a_refused_visibility_change_names_is_hidden_with_a_fixed_message…test_a_course_the_modulestore_returns_nothing_for…test_options…test_a_bearer_tokenCustomPagesRoutingTest.test_the_list_address_without_its_trailing_slash_redirects_to_itThe
tab_locatorform of v0's settings endpoint has no successor. It can only name a custom page, which can never be hidden.Query counts. A listing runs no more SQL queries or Mongo reads than v0's, measured in the same test on both sides of the pages-and-resources flag. Its SQL count does not grow with the number of pages. No literal baseline was measured, because no platform environment was available.
Schema. Three new operations, and
deprecated: trueon the three v0 operations.docs/cms-openapi.yamlis not edited; thegenerate_openapi_schemasworkflow regenerates it.In-place edits
cms/djangoapps/contentstore/rest_api/v1/authoring_urls.pyCustomPagesRoutingTestcms/djangoapps/contentstore/views/permissions.py(shared, unversioned)openedx_authz.constants.permissionsimport gains one name; existing classes unchangedCustomPagesPermissionWiringTestand the access testscms/djangoapps/contentstore/rest_api/pagination.py(new, unversioned)SharedPageEnvelopeTest,CustomPagesSchemaTest.test_the_list_publishes_the_seven_envelope_keyscms/lib/spectacular.py,cms/lib/tests/test_spectacular.pySUPERSEDED_PATHSentries; the exact-set test updated and a test for the tab paths addedCmsMarkSupersededPathsTestNew files inside the pre-existing
v1/:custom_pages_service.py,serializers/custom_pages.py,views/custom_pages.py,views/tests/test_custom_pages.py. They import nothing fromrest_api/v0/.Error-format decision
Versioned. v0 keeps its
developer_message/error_codebodies, its 500s and the helpers' text unchanged. v1 uses the envelope with fixed messages. The custom pages screen in frontend-app-authoring calls the v0 list and reorder (src/custom-pages/data/api.ts); the list's pagination already forces a new version, and the error format rides with it.Deprecations
The three v0 operations are superseded by the three v1 operations in the Summary. They are
deprecated: truein the CMS schema; the v0 views are not edited and send no runtime header. Tracked on the FC-0118 umbrella DEPR ticket, #39128; this resource's entry is posted there. Window per #39128: removed one named release after the release this ships in.Migration notes:
GET /api/authoring/v1/custom_pages/?course_key={course_key}. It is paginated: readresults, and follownextor passpage/page_size(at most 100) to get every page. Items are unchanged, includingid.PUT /api/authoring/v1/courses/{course_key}/custom_page_order/with{"custom_pages": [<usage key>, …]}, theids from the list (v0'stab_locatorvalues). Pages left out follow the listed ones in their current order. Answers 204.PATCH /api/authoring/v1/courses/{course_key}/tabs/{tab_id}/with{"is_hidden": true|false}. Answers 200 with the tab. A body withoutis_hiddenis refused (400, fixed message undernon_field_errors).course_key,custom_pages,is_hidden, ornon_field_errorsfor a tab change naming no member).course_keyis a query parameter now, so it must be percent-encoded: an unencoded+reads as a space and is refused (sendcourse-v1%3AedX%2BDemoX%2B2026).Authorization
@view_auth_classes(is_authenticated=True)on all three viewsIsAuthenticated, before the course key is readtest_anonymous_caller_is_refused_with_401(both permission modes), incl. on an unknown course;CustomPagesCourseKeyQueryTest.test_the_caller_is_authenticated_before_the_course_key_is_read@verify_course_exists()on all three methodsIsExistingCourse(also 404 for version-only and library keys); for the list, after the query'scourse_keyis validatedtest_unknown_course_is_404_before_permission_is_checked,test_a_version_only_course_key_is_404,test_a_library_is_not_a_course,CustomPagesCourseKeyQueryTest.test_a_version_only_or_library_key_is_an_unknown_course)user_has_course_permission(courses.view_pages_and_resources, READ)in the listCanViewPagesAndResources(existing), same calluser_has_course_permission(courses.manage_pages_and_resources, WRITE)in reorder and settingsCanManagePagesAndResources(new), same calltest_a_role_that_may_only_view_may_list_but_not_reorder_or_change_tabs)Order: 401, then 400 for a missing or invalid
course_key(list only), then 404 for a missing course, then 403. Allowed: course staff and instructor, org staff and instructor, global staff, and superusers, as on v0: saving a superuser also setsis_staff, so every superuser is global staff (test_a_superuser_is_allowed). With policy permissions on, the roles that grant viewing (list) or managing (reorder, tab change) pages and resources are allowed: auditor views; editor, staff and admin manage.Gate report
Run on the host against
9b4d60ea0d(the head of #39303, which this branch stacks on). No platform environment was available, so four gates were skipped locally. The test module runs in CI.v1/authoring_urls.py), 4 ADDED, nothing protected touched (23 shared legacy modules protected, includingviews/tabs.py, whose helpers v1 reuses)DocumentedDefaultPagination, a local subclass of a library paginator. It is kept on purpose: it adds the seven-key response schema the library lacks (see 0032) and says so in its docstringurl_snapshot.py check) on a hand-built snapshot of the new routesVideoVolumeControlARIA test); the two pylint C0116 hits are fixed in 8d66706Follow-ups
openedx-platform-sdk; the next openedx-platform-sdk regeneration gains them.src/custom-pages/data/api.ts) can move to the v1 list (requestpage_size=100or follownext) and thePUTorder. No MFE caller uses the v0 settings endpoint today.DefaultPagination.get_paginated_response_schemawith all seven keys, after whichDocumentedDefaultPaginationcan be deleted. Issue to be filed.ParseError,MethodNotAllowed,NotAcceptableandUnsupportedMediaType.detailwithout theErrorDetail(...)text.CourseTextbookPagination(frozen inv2/views/textbooks.py) duplicates the shared paginator's schema; point the next version of that resource atrest_api/pagination.py.PolicyCacheControl: the first permission check on a fresh database inserts the cache row on a read; seeding it by migration would make reads write-free. Issue to be filed./api/authoring/: v1 here, feat: serve course advanced settings from authoring v1 #39303 and the feat: apply FC-0118 ADRs to course content APIs #39157 v2 views keepSessionAuthenticationAllowInactiveUser; feat: serve the course outline from authoring v2 #39229 and feat: serve course containers from authoring v2 #39274 use the default. One policy for new versions of such endpoints should be agreed.views/permissions.py,v1/authoring_urls.py,cms/lib/spectacular.pyand its test. The conflicts are additive, resolved by rebasing in merge order. When both land, the mergedopenedx_authz.constants.permissionsimport must be wrapped to stay within 120 characters.