Repository navigation
feat: serve course custom pages and tabs from authoring v1 - #39305
taimoor-ahmed-1 wants to merge 11 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.
Summary
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 docstrings name each successor and #39128docs: 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; the DEPR entry is a comment on [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 until it is folded into the issue bodycustom_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 frozen apart from docstring-only markers: the syntax tree with docstrings stripped equals the base.deprecated: truethrough three exactSUPERSEDED_PATHSentries; no runtime header (frozen routes). DEPR: an entry comment on #39128, to be folded into the issue bodyCmsMarkSupersededPathsTest.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.pygains docstring text only; its syntax tree with docstrings stripped equals the base. 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)…test_a_change_without_is_hidden;CourseTabUpdateTest.test_a_body_without_is_hidden_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/v0/views/tabs.pycms/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, and the docstrings name each successor. The views are otherwise unchanged: no runtime header. Tracked on the FC-0118 umbrella DEPR ticket, #39128; this resource's entry is posted there with the PR. 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_hiddenchanges nothing.course_key,custom_pages,is_hidden).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. The test module has not been run locally, and CI is its first run.v0/views/tabs.pydocstrings,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 routesFollow-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.