Repository navigation
feat: apply FC-0118 ADRs to legacy libraries API - #39238
Draft
Faraz32123 wants to merge 2 commits into
Draft
Faraz32123 wants to merge 2 commits into
Faraz32123 wants to merge 2 commits into
Conversation
Studio's legacy library list is served unpaginated at v1 and v3, writes a CourseCreator row on GET, ignores its documented `org` filter, and lives under a screen-named address. Fixing any of these in place would break current callers, so the operation gets its next version at /api/authoring/v4/legacy_libraries/: paginated, ordered deterministically, `org` actually filtering, standardized errors, no writes on GET, and the same per-library access checks reused unchanged. Part of #39069. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the CMS schema The v4 list supersedes GET /api/contentstore/v1/home/libraries and GET /api/authoring/v3/home/libraries/. List them as exact paths in SUPERSEDED_PATHS so the published schema flags them, while /api/authoring/v3/home/ and its courses route stay current. The views themselves are unchanged. Tracked on #39128. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 Libraries functional area (#39069, part of #38137). This area has one operation: "list the legacy (v1, modulestore) libraries the requester can read". Today it is served at three addresses across two versions:
GET /api/contentstore/v1/home/librariesGET /api/contentstore/v3/home/libraries/GET /api/authoring/v3/home/libraries/This PR adds its next version at
GET /api/authoring/v4/legacy_libraries/and marks the v1 and/api/authoring/v3/addressesdeprecated: truein the CMS schema. The v3 contentstore address already carried the mark. The v1 and v3 code is not changed.Applies ADRs 0025–0038 and the OEP-69 conventions.
Why a new version rather than fixing v3 in place. v3 already has the error envelope, a ViewSet and the
/api/authoring/mount. What it is missing would change its contract for current callers:librariesarray. Paginating it would change the body shape every caller parses.CourseCreatorrow and firespost_save. Removing that changes what the endpoint does.org. v1 and v3 document anorgfilter, but no code reads it. Making it work changes results for callers who already send it.home/librariesnames a screen, not a resource.So these changes go in v4, the next number after the resource's highest version. The new collection is called
legacy_librariesso that it cannot be confused with v2 content libraries, which live at/api/libraries/v2/.Changes
feat: add a paginated v4 legacy library listGET /api/authoring/v4/legacy_libraries/(new)has_studio_read_access, same helper), same row fields and names,can_editfromhas_studio_write_access,is_migratedfilter, empty list while legacy libraries are disabled, inactive-session callers still servedpage_sizeup to 100); fixed order: display name ignoring case, then library key;orgfilters by the library key's organization; empty filter values give 400; standardized error envelope; no Bearer; noCourseCreatorwrite or signal on GETfeat: mark the v1 and authoring v3 legacy library lists deprecated in the CMS schemaGET /api/contentstore/v1/home/libraries,GET /api/authoring/v3/home/libraries/deprecated: truein the published schema (exact paths inSUPERSEDED_PATHS;/api/authoring/v3/home/and/api/authoring/v3/home/courses/stay current)What each commit carries:
feat: add a paginated v4 legacy library list:rest_api/v4/authoring_urls.py,v4/legacy_libraries_service.py,v4/serializers/legacy_libraries.py,v4/views/legacy_libraries.py, andv4/views/tests/test_legacy_libraries.pywithout two schema tests:LegacyLibraryListSchemaTest::test_the_superseded_addresses_are_deprecatedand…::test_the_other_home_operations_stay_current.feat: mark the v1 and authoring v3 legacy library lists deprecated in the CMS schema:cms/lib/spectacular.py,cms/lib/tests/test_spectacular.py(the exact-set test renamed and extended, and two new hook tests), and the two schema tests above.Each commit is green on its own. With
cms/lib/spectacular.pyand its test module put back to the base, the v4 module and the hook module give1 failed, 76 passed. The one failure istest_the_superseded_addresses_are_deprecated, which is why it moves to the second commit.test_the_other_home_operations_stay_currentpasses in the first commit, but it guards the hook against marking too much, so it goes with the hook.ADRs applied
LegacyLibrarySerializer(output,help_texton every field, nullable collection fields) andLegacyLibraryListQuerySerializer(parameters)IsAuthenticatedon the view, with per-library read/write checks reused unchanged; 13 allow/deny tests@extend_schemawith summary, description, parameters and 200/400/401/404, and theopenedx-platform-sdktagpath(..., as_view({"get": "list"})), not registered on a DRF Router, because a router names the routelegacy_library-listand ADR 0038 rule 11 asks for a snake_case name (legacy_library_list, like its siblingcourse_listin the same URLconf); merged feat: apply ADR 0038 (URL structure standardization) across 6 standardized APIs #39078, feat: apply FC-0118 ADRs to youtube transcripts APIs #39100 and feat: apply FC-0118 ADRs to course content APIs #39157 mount their ViewSets the same way. Otherwise met: plainViewSet(modulestore data path) with a service module; bounded query count; see the matrixStandardizedErrorMixin; 400/401/404 documented withErrorResponse. Three known gaps lie outside this view's code and are recorded under Error-format decision: 405 and 406 enveloped asinternal, a 500 logged without its traceback, and an HTML 404 for addresses below the collectionPolicyCacheControlrow, once per database) are classified in the matrix. The course-creator write is not moved to a v4 endpoint; what still depends on it is in the migration notesDefaultPaginationenvelope viapaginate_iterable, with a seven-key schema and apage_sizedescription that states the default and the caporgandis_migratedvalidated by a serializer (non-ORM list); deterministic orderDeprecated <link>marker is allowed now that [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 is Plan Accepted, and is deliberately not added. See the matrix/api/authoring/v4/legacy_libraries/, nameauthoring_v4:legacy_library_list; legacy names keptget_queryset()forScopedQuerysetMixinto wrap; the three layers are implemented in the service and tested (see the matrix)Compliance matrix
help_texton all 11 fields;migrated_to_*optional; collection key and titleallow_null(the migrator returnsNonewithout a collection); parameters through their own serializer;serializer_class+get_serializer. No drf-yasg name collision.LegacyLibraryListSchemaTest::test_the_migration_fields_are_optional_and_the_collection_ones_nullable(exact description of all 11 fields),LegacyLibraryListContentTest::test_migration_fields_appear_only_on_migrated_libraries,LegacyLibraryListParityTest::test_every_field_of_every_row_matches; hygiene H9permission_classes = (IsAuthenticated,). Record visibility is_accessible_libraries_iter(has_studio_read_access), andcan_editisformat_library_for_view(has_studio_write_access), both called unchangedLegacyLibraryListAccessTest(13 tests; see Authorization)@extend_schemaonlist;tags=["openedx-platform-sdk"]; operationIdv4_legacy_libraries_list; notx-internal. Parameter descriptions say thatorgis an exact, case-sensitive match and thatis_migratedcounts only migrations that forwardLegacyLibraryListSchemaTest(7 tests, including…::test_the_query_parameters_are_published_with_their_descriptions, which asserts all four descriptions exactly); schema gate (check-presentfor/api/authoring/v4/legacy_libraries/)LegacyLibraryViewSet(StandardizedErrorMixin, IterablePaginationMixin, viewsets.ViewSet);legacy_libraries_service.py. Deviation: routed withpath(..., as_view({"get": "list"}))rather than a DRF Router, because a router would name the routelegacy_library-listagainst ADR 0038 rule 11 (snake_case names); the same form as the siblingcourse_listroute and merged #39078, #39100, #39157LegacyLibraryListUrlTest::test_the_address_resolves_to_the_list_action;LegacyLibraryListQueryCountTest(4 tests)validationwitherrorskeyed by parameter; 401authn; 404not-foundfor a page past the end or apagethat is not a number. Known gaps, none of them raised by this view's code: 405 and 406 are enveloped asinternal(the library catalogs neitherMethodNotAllowednorNotAcceptable); an unexpected exception gets the 500 envelope but no logged traceback; an address below the collection gets the CMS's HTML 404. Details under Error-format decision; follow-ups belowLegacyLibraryListErrorTest(4, including the fullerrorsvalue for both 400s andtest_writes_are_not_allowedfor the 405),LegacyLibraryListAccessTest::test_anonymous_is_refused_with_a_challenge,LegacyLibraryListParityTest::test_the_unauthenticated_envelopes_match. The 406, unlogged-500 and HTML-404 gaps were reproduced in review; no test pins them_get_course_creator_status: noCourseCreatorrow, nopost_save. Writes outside the handler, classified and kept: (1) JWT user provisioning at authentication (edx-drf-extensions, on every JWT endpoint); (2) waffle'scache.add, a cache-backend write; (3) openedx-authz's cache-version row,PolicyCacheControl.get()→get_or_create(pk=1). On a database without that row, the first role check inserts it (one INSERT,pre_save/post_save); after that, it never writes again. That write belongs to the platform authorization layer, which v1, v3 and every Studio role check share. The test fixture creates the row insetUp, so the measurement isolates the handler. The course-creator write is dropped from this resource, not moved to a new v4 endpoint (no POST is added). Migration notes name the reads that still create it andPOST /request_course_creator, which depends on itLegacyLibraryListReadOnlyTest::test_get_and_head_write_nothing_and_send_no_signal(GET and HEAD × three service branches, zero INSERT/UPDATE/DELETE, no signal); control…::test_the_legacy_list_does_write_for_the_same_caller;…::test_a_jwt_request_writes_nothing(first request measured, noCourseCreatorrow). Hygiene H8 cannot see a service module and is not the evidence.LegacyLibraryPagination+paginate_iterable; 10 default, 100 max. The subclass overridesget_paginated_response_schema, so the schema documents all seven keys withnext/previousnullable. It also setspage_size_query_descriptionto state the default and the cap. Its docstring says to delete it once the library does both. It is the second local copy of the schema fix, afterCourseTextbookPagination; that copy is accepted for this PR, and the library fix is a follow-upLegacyLibraryListPagesTest(4);LegacyLibraryListSchemaTest::test_the_success_body_is_the_seven_key_page_of_legacy_libraries,…::test_the_query_parameters_are_published_with_their_descriptions;LegacyLibraryListErrorTest::test_a_page_past_the_end_or_not_a_number_is_not_foundquery_params.dict()(an absent boolean stays absent);orgexact match; empty values give 400; order(display_name casefolded, library_key); noorderingparameter (the legacy offers none)LegacyLibraryListContentTest::test_org_*,…::test_is_migrated_*,LegacyLibraryListErrorTest::test_an_*_is_a_validation_error_on_that_parameter,LegacyLibraryListPagesTest::test_the_order_is_the_same_whatever_the_page_size,LegacyLibraryListContentTest::test_libraries_sharing_a_display_name_are_ordered_by_key(key tie-breaker)(JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented. The v1 and authoring v3 library lists it replaces accept inactive users' sessions, so this one keeps that access unchanged; inactive users fail every library role check, so only inactive global staff are shown any library, and inactive non-staff get an empty page. Bearer is not accepted on v4. Frozen v1 (HomePageLibrariesView, through the shared@view_auth_classesdecorator,openedx/core/lib/api/view_utils.py:122-126) still accepts Bearer until it is removed: this PR does not edit it, and taking Bearer out of that shared decorator is not this area's changeLegacyLibraryListAccessTest::test_a_bearer_token_is_not_accepted,…::test_a_jwt_caller_gets_the_same_page_as_a_session_caller,…::test_an_inactive_author_is_authenticated_but_shown_nothing,…::test_inactive_staff_are_authenticated_and_shown_every_library; hygiene H4?view=or?fields=v4/; v0–v4 otherwise untouched; v1 and/api/authoring/v3/home/libraries/deprecated in the schema. Deviation: no docstring marker on the superseded views. The basis is the FC-0118 review direction on #39100 and #39102 ("lets not add depr note in comments"). OEP-21 timing is not the basis: #39128 is already Plan Accepted, so OEP-21's one-lineDeprecated <link>marker would be allowed. It is deliberately not added; if a reviewer asks for a marker, that single line is the formLegacyLibraryListSchemaTest::test_the_superseded_addresses_are_deprecatedapi/authoring/v4/+legacy_libraries/; trailing slash; no nesting; snake_case, version-free name; legacy names unchangedLegacyLibraryListUrlTest(5); urls gateget_queryset(). Layer 1IsAuthenticated; layer 2 the shared visibility iterator; layer 3org/is_migrated, which only narrow layer 2. A later index-level scope would pass kwargs throughmodulestore().get_library_summaries(**kwargs), called from_accessible_libraries_iterLegacyLibraryListAccessTest::test_filters_only_narrow_what_the_caller_can_read,…::test_another_users_library_never_appears_under_any_filtercheck_code_hygiene.py --base origin/masteron the host checkout:PASS (0 FAIL, 1 WARN)Backward compatibility
The gate numbers below are from the gate run in the Gate report section.
Versions gate:
1 ALLOWED(v4/authoring_urls.py),4 ADDED(the newv4/files), no VIOLATION. Summary line:files in new version dirs: 0 other files: 10 added in old dirs: 4 allowed: 1/RESULT: PASS.cms/lib/spectacular.pyandcms/lib/tests/test_spectacular.py.cms/envs/tutor/andlms/envs/tutor/(__init__.py,development.py,production.pyandtest.pyin each). Tutor bind-mounts them into the container where the gates ran, so git there lists them as untracked. On the host they are empty mount points, and they are not in the diff.--protect:cms/djangoapps/contentstore/utils.py,views/course.py,views/library.py,toggles.py,modulestore_migrator/api/read_api.py,student/auth.py,student/roles.py,cms/urls.pyanddocs/cms-openapi.yaml. Run on the host checkout with those flags and--allow v4/authoring_urls.py, the same check givesother files: 2and PASS. None of the nine modules is in the diff.URLs:
head routes: 2163, base routes: 2162, new api routes checked: 1/RESULT: PASS (0 FAIL, 0 WARN). Every base route keeps its pattern, name and view.reverse("cms.djangoapps.contentstore:v1:libraries"),…:v3:home-librariesandauthoring_v3:home_librarieskeep their literals and views (LegacyLibraryListUrlTest::test_the_legacy_addresses_keep_their_names_and_views).Schema:
base: 69 paths, head: 70 paths/RESULT: PASS (0 BREAKING, 2 WARN, 3 INFO).GET /api/authoring/v4/legacy_libraries/added;GET /api/contentstore/v1/home/librariesandGET /api/authoring/v3/home/libraries/newly marked deprecated.JwtAuthenticationandSessionAuthenticationAllowInactiveUseron the new view. Base schema generation already emits the same warning for other views that use those classes, for exampleHomeViewSetandAuthoringGradingViewSet.Parity (
LegacyLibraryListParityTest): every page of v4 is compared field for field against/api/authoring/v3/home/libraries/, with the legacy list sorted into the new order.is_migrated=trueandis_migrated=false. 20 rows are compared, and the count is asserted.maybe,'',null) is refused by both. The only differences are those declared inINTENTIONAL_DIFFERENCES, verbatim:The harness has been seen to fail. A test flips one field and asserts that the diff reports exactly that path. Each guarded behaviour was also mutated in turn and the suite went red each time: visibility, sort, case-folding, the key tie-breaker,
org, the disabled gate, lazy migration reads, the course-creator write (also on the JWT path alone),.dict(), migration-key dropping,allow_null, the paginator schema, nullablenextandprevious, thepage_sizeandorgdescriptions, a field'shelp_text, emptied validation messages, Bearer, and the two hook entries (23 mutations, rerun against the final tree).Query count (global staff, five libraries, warm caches): v4 issues 14, asserted exactly; the v3 list issued 17 when measured, and the test asserts only that v4 costs no more, so a change to the shared home-screen helpers cannot turn a v4 test red. With
page_size=1, adding a library does not change the count. Withis_migrated=true, it adds exactly one query (one forwarding lookup per visible library; the migrator has no bulk read).Legacy tests unmodified and green: old-tests gate:
v1/views/tests/test_home.py(5),v3/tests/test_home.py(11),v3/views/tests/test_home.py(8). Tests gate, as neighbours of the new module:v4/tests/test_home.py(4),v4/views/tests/test_home.py(28).No code moved, so no re-exports are needed. The new service imports
_accessible_libraries_iterandformat_library_for_viewfromcms/djangoapps/contentstore/views/course.py, asutils.get_library_contextalready does.In-place edits
cms/djangoapps/contentstore/rest_api/v4/authoring_urls.pypath()for a new resource. Thecourses/route (authoring_v4:course_list) is unchanged.LegacyLibraryListUrlTest::test_the_neighbouring_course_list_is_unchanged; v4 home tests unmodified and greencms/lib/spectacular.pySUPERSEDED_PATHS; the comment above it updated to stay accurateCmsMarkSupersededPathsTest::test_the_legacy_library_lists_are_marked_and_their_neighbours_are_not(seven neighbouring paths, including/api/authoring/v3/home/,/home/courses/,/api/authoring/v4/courses/and the new path, left unchanged);…::test_a_superseded_home_path_keeps_its_internal_mark_and_the_home_screen_stays_currentcms/lib/tests/test_spectacular.pytest_the_superseded_paths_are_exactly_these(renamed fromtest_both_spellings_of_each_superseded_path_are_listed, whose name no longer fits the set) asserts the exact set ofSUPERSEDED_PATHS, so it gains the two paths; two tests addedNew files in the pre-existing
v4/directory (the gate reports them ADDED):legacy_libraries_service.py,serializers/legacy_libraries.py,views/legacy_libraries.py,views/tests/test_legacy_libraries.py. They are a new resource module beside the unrelatedcoursesresource.v4/views/__init__.pyandv4/serializers/__init__.pyare not edited.The superseded views (
v1/views/home.py,v3/views/home.py) are not in the diff.Error-format decision
The new version uses the standardized error envelope. v3 already returned it, so v3 callers see no change. v1 callers received DRF's default (
{"detail": …}) or, for a badis_migrated, a bare list; they get the envelope only by moving to v4. v1 is untouched.Known gaps. None comes from this view's code. Each is recorded here and listed under Follow-ups rather than fixed in this PR:
internal. edx-drf-extensions 10.9.0 catalogs neitherMethodNotAllowednorNotAcceptable(_BUILTIN_ERROR_TYPES,edx_rest_framework_extensions/errors.py:87-95). APOSTtherefore gets a 405 envelope with type…/errors/internaland title "Internal Server Error". So does aGETwithAccept: application/xmlortext/html, as a 406. Registering the types from an area is ruled out; the fix belongs in the library.LegacyLibraryListErrorTest::test_writes_are_not_allowedpins the 405 as it is today. The 406 was reproduced in review.errors.py:228-237). The server log has only Django'sInternal Server Error: /api/authoring/v4/legacy_libraries/line, with no traceback. v1 re-raises, so Django logs its traceback. ADR 0029 asks that stack traces still reach the server log. Whether APM still captures the exception is the library default; it was not verified here. Reproduced in review./api/authoring/v4/legacy_libraries/<anything>/. Such a request falls through to the CMShandler404and gets atext/htmlpage, even withAccept: application/json. A catch-all route under the collection would fix it for this one address. This PR adds none, and no merged area has one. Reproduced in review.Deprecations
GET /api/contentstore/v1/home/libraries,GET /api/contentstore/v3/home/libraries/andGET /api/authoring/v3/home/libraries/are superseded byGET /api/authoring/v4/legacy_libraries/. They are markeddeprecated: truein the published CMS schema; the v3 contentstore address was already marked through its/api/contentstore/v3/home/prefix. The superseded views are unchanged: no response header, and no deprecation notes in code./api/authoring/v3/home/and/api/authoring/v3/home/courses/remain current.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.
Migration notes for callers:
{count, num_pages, current_page, start, next, previous, results}, 10 per page by default,page_sizeup to 100. Each item inresultshas the fields of an item in the oldlibrariesarray, under the same names.orgnow filters by the organization in the library key, as an exact, case-sensitive match; v1 and v3 documented it but ignored it.is_migratedbehaves as before. An empty value for either is a 400.{"detail": ...}or a bare list).ENABLE_CREATOR_GROUP, v1 and v3 created anunrequestedcourse-creator record (CourseCreator) as a side effect of this read, for a non-staff user who had none. Two other Studio reads still create it: the Studio home context (get_home_context,cms/djangoapps/contentstore/utils.py:1714, behind the v1 and v3homeendpoints) and the course rerun context (get_course_rerun_context,utils.py:1740).POST /request_course_creatordepends on that record (cms/djangoapps/contentstore/views/user.py:27-31). It callsuser_requested_access, which doesCourseCreator.objects.get(user=user)(cms/djangoapps/course_creators/views.py:89). On a site withENABLE_CREATOR_GROUP, a non-staff author whose client has loaded neither the Studio home nor a course rerun context gets a 500 from that POST. No v4 endpoint creates the record (neither this list nor/api/authoring/v4/courses/), and this PR adds no POST to do it. A client that offers course-creator requests must still load the Studio home for the user first. The fix is tracked with the Studio home standardization, [API] Home Page Data #39068./api/authoring/v3/home/libraries/was added by #39078, merged 2026-10-12 (cc @Abdul-Muqadim-Arbisoft). It is deprecated here because v4 is now this operation's current version.Authorization
IsAuthenticatedpermission_classes = (IsAuthenticated,)test_anonymous_is_refused_with_a_challenge(401authn)libraries_v1_enabled()ContentLibrariesDisabledTest,LegacyLibrariesDisabledTest(one fixture class per side; staff getcount == 0, as on v3)has_studio_read_accessper library (including the limited-staff hide and inactive-user refusal)_accessible_libraries_iter(user), unchangedtest_a_user_without_roles_gets_an_empty_page,test_limited_staff_are_hidden_even_when_they_hold_a_library_user_role(limited staff on a library who also hold its library-user role or the org library-user role are not shown it),test_another_users_library_never_appears_under_any_filter,test_filters_only_narrow_what_the_caller_can_read,test_an_inactive_author_is_authenticated_but_shown_nothinghas_studio_write_accessforcan_editformat_library_for_view, unchangedtest_a_library_user_sees_only_that_library_read_only,test_an_org_library_user_sees_that_organizations_libraries_read_only(can_edit False);test_library_staff_and_instructors_see_their_library_with_edit_rights,test_org_staff_and_instructors_see_that_organizations_libraries_with_edit_rights(can_edit True)Global staff, including inactive staff, see every legacy library (
test_inactive_staff_are_authenticated_and_shown_every_library). JWT callers get the same page as session callers. Bearer is refused with 401.Gate report
run_gates.shagainst baseorigin/master, run inside the Tutor dev container on the bind-mounted checkout, on the final tree. No gate was skipped.files in new version dirs: 0 other files: 10 added in old dirs: 4 allowed: 1/RESULT: PASS13 file(s) scanned/RESULT: PASS (0 FAIL, 5 WARN)base: 69 paths, head: 70 paths/RESULT: PASS (0 BREAKING, 2 WARN, 3 INFO)head routes: 2163, base routes: 2162, new api routes checked: 1/RESULT: PASS (0 FAIL, 0 WARN)24 passed, 70 warnings111 passed, 672 warningsThe container sees files that the PR does not contain. Tutor bind-mounts its settings modules into
cms/envs/tutor/andlms/envs/tutor/, and git inside the container lists them as untracked; on the host both directories are empty and nothing in them is in the diff. That explains two numbers above:other files: 10: two files from this PR (cms/lib/spectacular.pyand its test) plus eight Tutor settings modules.13 file(s)and5 WARN: the PR's seven files plus six Tutor modules. Four of the WARNs are H11 on line 184 ofcms/envs/tutor/development.py,cms/envs/tutor/production.py,lms/envs/tutor/development.pyandlms/envs/tutor/production.py, each a comment of the form# RemovedInDjango5xWarning: 'xxx' is deprecated. …, which Tutor generates. The fifth is H3 onLegacyLibraryPagination, the documented paginator subclass. On the host checkout the same hygiene check givesPASS (0 FAIL, 1 WARN), that H3.The tests gate does not turn warnings into errors. A separate run of the same 111 tests used
pytest -W error, filtering by message only the third-party and platform warnings that exist at the base. It gave111 passed, 1 warning; the one warning is webob's'cgi' is deprecated, raised at import and already present at the base.Follow-ups
SDK: the view carries the
openedx-platform-sdktag, so the next openedx-platform-sdk regeneration gainsv4_legacy_libraries_list. The v3 addresses arex-internaland drop out of generators that skip it.frontend-app-authoring:
/api/contentstore/v3/home/libraries/, which is deprecated. It should target v4 instead (to be noted on FAA#3127).Course-creator record, for [API] Home Page Data #39068: standardizing the Studio home endpoints under [API] Home Page Data #39068 would remove one of the two reads that still create the
unrequestedCourseCreatorrecord; the course rerun context is the other. Before that happens,POST /request_course_creatorneeds a write that creates the record: an explicit endpoint, oruser_requested_accesscreating it when it is missing. This PR records the dependency and adds no POST.edx-drf-extensions (library changes, not made here):
DefaultPagination.get_paginated_response_schemadocuments four of the seven keys, and the inheritedpage_sizedescription gives neither the default nor the cap. The library fix should cover both. This PR adds the second local subclass, afterCourseTextbookPagination. The two now differ, since only this one carries thepage_sizetext. Keeping a second copy is an accepted risk for this PR; both subclasses go once the library fix is released. No issue or PR exists for it (searched 2026-10-14).MethodNotAllowedandNotAcceptableare not cataloged, so a 405 or 406 is enveloped asinternal/ "Internal Server Error".ValidationErroris rendered intodetailas the Python repr of the whole error dict.Items 2 to 4 were not searched for existing issues.
CMS JSON 404: an address below
/api/authoring/v4/legacy_libraries/gets the CMS's HTML 404 page, even when the client asked for JSON. Fixing that for unmatched/api/addresses belongs to the CMShandler404, or to a catch-all route under each collection.Shared test helpers: the v4 tests import
PASSWORD,SignalRecorder,generate_cms_schema,logged_in_client,resolve_refandwritesfromcms/djangoapps/contentstore/rest_api/v2/views/tests/helpers.py, a module scoped to the v2 tests. Hoist them next torouted_actionsincms/djangoapps/contentstore/tests/utils.py, and leave a re-export in v2. Then a v2-only edit cannot change what the v4 tests prove, and retiring v2 cannot break them. Not done here, because it edits frozen v2 test code.Later category-2 opt-ins (no version bump needed): an
orderingparameter and a text-search parameter on v4.Test environment: run in a Tutor container (
openedx-dev:23.0.0-main) withopenedx-core1.11.0,openedx-authz1.24.0 andopenedx-forum0.4.8 installed to match the pins (the image ships older releases). CI is the authoritative run.🤖 Generated with Claude Code