Skip to content

feat: apply FC-0118 ADRs to legacy libraries API - #39238

Draft
Faraz32123 wants to merge 2 commits into
masterfrom
feat/apply_fc_0118_ADRs_to_libraries_api
Draft

Faraz32123 wants to merge 2 commits into
masterfrom
feat/apply_fc_0118_ADRs_to_libraries_api

Conversation

@Faraz32123

Copy link
Copy Markdown
Contributor

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/libraries
  • GET /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/ addresses deprecated: true in 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:

  • Unpaginated. It returns the whole list as a bare libraries array. Paginating it would change the body shape every caller parses.
  • Writes on GET. For a non-staff user on a site with course-creator approval on, the GET creates a CourseCreator row and fires post_save. Removing that changes what the endpoint does.
  • Ignored org. v1 and v3 document an org filter, but no code reads it. Making it work changes results for callers who already send it.
  • Screen-shaped address. home/libraries names 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_libraries so that it cannot be confused with v2 content libraries, which live at /api/libraries/v2/.

Changes

Commit Endpoint(s) Legacy behaviour kept New behaviour
feat: add a paginated v4 legacy library list GET /api/authoring/v4/legacy_libraries/ (new) Same visibility rules (has_studio_read_access, same helper), same row fields and names, can_edit from has_studio_write_access, is_migrated filter, empty list while legacy libraries are disabled, inactive-session callers still served Seven-key page envelope (10 per page, page_size up to 100); fixed order: display name ignoring case, then library key; org filters by the library key's organization; empty filter values give 400; standardized error envelope; no Bearer; no CourseCreator write or signal on GET
feat: mark the v1 and authoring v3 legacy library lists deprecated in the CMS schema GET /api/contentstore/v1/home/libraries, GET /api/authoring/v3/home/libraries/ Routes, views, responses and URL names unchanged deprecated: true in the published schema (exact paths in SUPERSEDED_PATHS; /api/authoring/v3/home/ and /api/authoring/v3/home/courses/ stay current)

What each commit carries:

  1. 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, and v4/views/tests/test_legacy_libraries.py without two schema tests: LegacyLibraryListSchemaTest::test_the_superseded_addresses_are_deprecated and …::test_the_other_home_operations_stay_current.
  2. 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.py and its test module put back to the base, the v4 module and the hook module give 1 failed, 76 passed. The one failure is test_the_superseded_addresses_are_deprecated, which is why it moves to the second commit. test_the_other_home_operations_stay_current passes in the first commit, but it guards the hook against marking too much, so it goes with the hook.

ADRs applied

  • 0025 serializers — LegacyLibrarySerializer (output, help_text on every field, nullable collection fields) and LegacyLibraryListQuerySerializer (parameters)
  • 0026 permissions — IsAuthenticated on the view, with per-library read/write checks reused unchanged; 13 allow/deny tests
  • 0027 schema / docs — @extend_schema with summary, description, parameters and 200/400/401/404, and the openedx-platform-sdk tag
  • 0028 viewsets — applied with one recorded deviation: the ViewSet is mounted with path(..., as_view({"get": "list"})), not registered on a DRF Router, because a router names the route legacy_library-list and ADR 0038 rule 11 asks for a snake_case name (legacy_library_list, like its sibling course_list in 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: plain ViewSet (modulestore data path) with a service module; bounded query count; see the matrix
  • 0029 errors — StandardizedErrorMixin; 400/401/404 documented with ErrorResponse. Three known gaps lie outside this view's code and are recorded under Error-format decision: 405 and 406 enveloped as internal, a 500 logged without its traceback, and an HTML 404 for addresses below the collection
  • 0030 GET idempotence — the v4 handler writes nothing and sends no signal, shown by SQL capture against a fixture where v3 does write. The writes the platform layers make on the same path (JWT user provisioning, and openedx-authz's PolicyCacheControl row, 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 notes
  • 0031 merged endpoints — n/a: one read operation; the three legacy addresses are versions of it, not an action-URL family
  • 0032 pagination — DefaultPagination envelope via paginate_iterable, with a seven-key schema and a page_size description that states the default and the cap
  • 0033 filtering — org and is_migrated validated by a serializer (non-ORM list); deterministic order
  • 0034 authentication — JWT plus session-allowing-inactive, a deviation commented at the declaration; Bearer dropped on v4 (frozen v1 still accepts it until it is removed)
  • 0035 MFE config — n/a: no configuration endpoint in this area; the list is per-user and not shared-cached
  • 0036 nested JSON — n/a: a flat list of thin objects (11 scalar fields, nothing embedded)
  • 0037 versioning — applied with one recorded deviation: the superseded views carry no deprecation marker in code, following the FC-0118 review direction on feat: apply FC-0118 ADRs to youtube transcripts APIs #39100 and feat: standardize Course Videos API into authoring v1 #39102. OEP-21's one-line Deprecated <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
  • 0038 URL structure — /api/authoring/v4/legacy_libraries/, name authoring_v4:legacy_library_list; legacy names kept
  • OEP-66 queryset scoping — mechanism n/a: the data path is the modulestore, with no get_queryset() for ScopedQuerysetMixin to wrap; the three layers are implemented in the service and tested (see the matrix)
  • OEP-69 conventions — the decisions above as recorded, including the 0028 and 0037 deviations; hygiene gate PASS (no governance identifiers, no deprecation prose, no local primitives beyond the documented paginator subclass)

Compliance matrix

Decision Status How it is met, or the concrete reason it is excluded Evidence (test / gate)
0025 met Output serializer with help_text on all 11 fields; migrated_to_* optional; collection key and title allow_null (the migrator returns None without 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 H9
0026 met permission_classes = (IsAuthenticated,). Record visibility is _accessible_libraries_iter (has_studio_read_access), and can_edit is format_library_for_view (has_studio_write_access), both called unchanged LegacyLibraryListAccessTest (13 tests; see Authorization)
0027 met @extend_schema on list; tags=["openedx-platform-sdk"]; operationId v4_legacy_libraries_list; not x-internal. Parameter descriptions say that org is an exact, case-sensitive match and that is_migrated counts only migrations that forward LegacyLibraryListSchemaTest (7 tests, including …::test_the_query_parameters_are_published_with_their_descriptions, which asserts all four descriptions exactly); schema gate (check-present for /api/authoring/v4/legacy_libraries/)
0028 met, one recorded deviation LegacyLibraryViewSet(StandardizedErrorMixin, IterablePaginationMixin, viewsets.ViewSet); legacy_libraries_service.py. Deviation: routed with path(..., as_view({"get": "list"})) rather than a DRF Router, because a router would name the route legacy_library-list against ADR 0038 rule 11 (snake_case names); the same form as the sibling course_list route and merged #39078, #39100, #39157 LegacyLibraryListUrlTest::test_the_address_resolves_to_the_list_action; LegacyLibraryListQueryCountTest (4 tests)
0029 met; three gaps outside the view recorded Mixin first in the MRO. 400 validation with errors keyed by parameter; 401 authn; 404 not-found for a page past the end or a page that is not a number. Known gaps, none of them raised by this view's code: 405 and 406 are enveloped as internal (the library catalogs neither MethodNotAllowed nor NotAcceptable); 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 below LegacyLibraryListErrorTest (4, including the full errors value for both 400s and test_writes_are_not_allowed for 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
0030 met The service never builds the Studio home context, so it never calls _get_course_creator_status: no CourseCreator row, no post_save. Writes outside the handler, classified and kept: (1) JWT user provisioning at authentication (edx-drf-extensions, on every JWT endpoint); (2) waffle's cache.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 in setUp, 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 and POST /request_course_creator, which depends on it LegacyLibraryListReadOnlyTest::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, no CourseCreator row). Hygiene H8 cannot see a service module and is not the evidence.
0031 excluded Single read operation; versions and mounts are handled by 0037 and 0038 —
0032 met LegacyLibraryPagination + paginate_iterable; 10 default, 100 max. The subclass overrides get_paginated_response_schema, so the schema documents all seven keys with next/previous nullable. It also sets page_size_query_description to 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, after CourseTextbookPagination; that copy is accepted for this PR, and the library fix is a follow-up LegacyLibraryListPagesTest (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_found
0033 met Non-ORM list, so a serializer validates query_params.dict() (an absent boolean stays absent); org exact match; empty values give 400; order (display_name casefolded, library_key); no ordering parameter (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)
0034 met (declared deviation) (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_classes decorator, 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 change LegacyLibraryListAccessTest::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
0035 excluded No front-end configuration endpoint —
0036 excluded Flat list of thin objects; no ?view= or ?fields= —
0037 met, one recorded deviation New resource module in 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-line Deprecated <link> marker would be allowed. It is deliberately not added; if a reviewer asks for a marker, that single line is the form versions gate (1 ALLOWED, 4 ADDED, no VIOLATION); 56 legacy and neighbour tests unmodified and green (old-tests gate 24, tests gate 32); LegacyLibraryListSchemaTest::test_the_superseded_addresses_are_deprecated
0038 met api/authoring/v4/ + legacy_libraries/; trailing slash; no nesting; snake_case, version-free name; legacy names unchanged LegacyLibraryListUrlTest (5); urls gate
OEP-66 mechanism excluded, layering met Modulestore data path, no get_queryset(). Layer 1 IsAuthenticated; layer 2 the shared visibility iterator; layer 3 org/is_migrated, which only narrow layer 2. A later index-level scope would pass kwargs through modulestore().get_library_summaries(**kwargs), called from _accessible_libraries_iter LegacyLibraryListAccessTest::test_filters_only_narrow_what_the_caller_can_read, …::test_another_users_library_never_appears_under_any_filter
OEP-69 met Conventions above, with the 0028 and 0037 deviations as recorded in their rows; no governance identifiers in code or schema text; no deprecation prose hygiene gate PASS, 0 FAIL. Its only WARN on a file in this PR is H3 on the documented paginator subclass; the other four are on Tutor settings files outside the PR (see Gate report). check_code_hygiene.py --base origin/master on 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 new v4/ files), no VIOLATION. Summary line: files in new version dirs: 0 other files: 10 added in old dirs: 4 allowed: 1 / RESULT: PASS.

    • Two of the ten "other files" are in this PR: cms/lib/spectacular.py and cms/lib/tests/test_spectacular.py.
    • The other eight are Tutor's own settings modules, cms/envs/tutor/ and lms/envs/tutor/ (__init__.py, development.py, production.py and test.py in 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.
    • The plan freezes nine shared modules with --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.py and docs/cms-openapi.yaml. Run on the host checkout with those flags and --allow v4/authoring_urls.py, the same check gives other files: 2 and 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-libraries and authoring_v3:home_libraries keep 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).

    • INFO: GET /api/authoring/v4/legacy_libraries/ added; GET /api/contentstore/v1/home/libraries and GET /api/authoring/v3/home/libraries/ newly marked deprecated.
    • The two WARNs are drf-spectacular's generic "could not resolve authenticator" for JwtAuthentication and SessionAuthenticationAllowInactiveUser on the new view. Base schema generation already emits the same warning for other views that use those classes, for example HomeViewSet and AuthoringGradingViewSet.
  • 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.

    • Callers: global staff, a library user and organization staff.
    • Filters: none, is_migrated=true and is_migrated=false. 20 rows are compared, and the count is asserted.
    • 401 envelopes are identical.
    • 400 (maybe, '', null) is refused by both. The only differences are those declared in INTENTIONAL_DIFFERENCES, verbatim:
    INTENTIONAL_DIFFERENCES = {
        "items": [],
        "unauthenticated": [],
        "invalid_is_migrated": [
            ("errors", "a parameter error is keyed by the parameter name"),
            ("detail", "the error library renders a keyed error as the text of the whole error dictionary"),
        ],
    }

    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, nullable next and previous, the page_size and org descriptions, a field's help_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. With is_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_iter and format_library_for_view from cms/djangoapps/contentstore/views/course.py, as utils.get_library_context already does.

In-place edits

File Category Evidence
cms/djangoapps/contentstore/rest_api/v4/authoring_urls.py strictly opt-in addition: one new path() for a new resource. The courses/ route (authoring_v4:course_list) is unchanged. URL gate PASS; LegacyLibraryListUrlTest::test_the_neighbouring_course_list_is_unchanged; v4 home tests unmodified and green
cms/lib/spectacular.py shared schema hook: two exact paths appended to SUPERSEDED_PATHS; the comment above it updated to stay accurate CmsMarkSupersededPathsTest::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_current
cms/lib/tests/test_spectacular.py registry test, not a frozen-version test: test_the_superseded_paths_are_exactly_these (renamed from test_both_spellings_of_each_superseded_path_are_listed, whose name no longer fits the set) asserts the exact set of SUPERSEDED_PATHS, so it gains the two paths; two tests added the diff itself

New 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 unrelated courses resource. v4/views/__init__.py and v4/serializers/__init__.py are 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 bad is_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:

  • 405 and 406 are typed internal. edx-drf-extensions 10.9.0 catalogs neither MethodNotAllowed nor NotAcceptable (_BUILTIN_ERROR_TYPES, edx_rest_framework_extensions/errors.py:87-95). A POST therefore gets a 405 envelope with type …/errors/internal and title "Internal Server Error". So does a GET with Accept: application/xml or text/html, as a 406. Registering the types from an area is ruled out; the fix belongs in the library. LegacyLibraryListErrorTest::test_writes_are_not_allowed pins the 405 as it is today. The 406 was reproduced in review.
  • A 500 leaves no traceback in the log. The library's exception handler turns an unexpected exception into the generic 500 envelope without logging it (errors.py:228-237). The server log has only Django's Internal 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.
  • HTML 404 below the collection. No route matches /api/authoring/v4/legacy_libraries/<anything>/. Such a request falls through to the CMS handler404 and gets a text/html page, even with Accept: 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/ and GET /api/authoring/v3/home/libraries/ are superseded by GET /api/authoring/v4/legacy_libraries/. They are marked deprecated: true in 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:

  • The list is paginated: {count, num_pages, current_page, start, next, previous, results}, 10 per page by default, page_size up to 100. Each item in results has the fields of an item in the old libraries array, under the same names.
  • Items are ordered by display name (case-insensitive), then library key.
  • org now filters by the organization in the library key, as an exact, case-sensitive match; v1 and v3 documented it but ignored it. is_migrated behaves as before. An empty value for either is a 400.
  • The trailing slash is required.
  • Errors use the standardized envelope (v1 callers previously got {"detail": ...} or a bare list).
  • Bearer tokens are not accepted on v4: use JWT or a session. v1 accepts them until it is removed.
  • The v4 GET has no side effects. On sites with ENABLE_CREATOR_GROUP, v1 and v3 created an unrequested course-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 v3 home endpoints) and the course rerun context (get_course_rerun_context, utils.py:1740).
  • POST /request_course_creator depends on that record (cms/djangoapps/contentstore/views/user.py:27-31). It calls user_requested_access, which does CourseCreator.objects.get(user=user) (cms/djangoapps/course_creators/views.py:89). On a site with ENABLE_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

Legacy check Kind Where it lives in v4 Deny-path test
IsAuthenticated endpoint access permission_classes = (IsAuthenticated,) test_anonymous_is_refused_with_a_challenge (401 authn)
libraries_v1_enabled() global visibility gate service, checked first ContentLibrariesDisabledTest, LegacyLibrariesDisabledTest (one fixture class per side; staff get count == 0, as on v3)
has_studio_read_access per library (including the limited-staff hide and inactive-user refusal) record visibility _accessible_libraries_iter(user), unchanged test_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_nothing
has_studio_write_access for can_edit per-row field format_library_for_view, unchanged test_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.sh against base origin/master, run inside the Tutor dev container on the bind-mounted checkout, on the final tree. No gate was skipped.

==================== summary ====================
  versions             PASS
  hygiene              PASS
  schema               PASS
  urls                 PASS
  old-tests            PASS
  tests                PASS
  artifacts: /tmp/api-adr-gates.fAkfJ5
ALL GATES PASSED
Gate Result line(s)
versions files in new version dirs: 0 other files: 10 added in old dirs: 4 allowed: 1 / RESULT: PASS
hygiene 13 file(s) scanned / RESULT: PASS (0 FAIL, 5 WARN)
schema base: 69 paths, head: 70 paths / RESULT: PASS (0 BREAKING, 2 WARN, 3 INFO)
urls head routes: 2163, base routes: 2162, new api routes checked: 1 / RESULT: PASS (0 FAIL, 0 WARN)
old-tests 24 passed, 70 warnings
tests 111 passed, 672 warnings

The container sees files that the PR does not contain. Tutor bind-mounts its settings modules into cms/envs/tutor/ and lms/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:

  • versions, other files: 10: two files from this PR (cms/lib/spectacular.py and its test) plus eight Tutor settings modules.
  • hygiene, 13 file(s) and 5 WARN: the PR's seven files plus six Tutor modules. Four of the WARNs are H11 on line 184 of cms/envs/tutor/development.py, cms/envs/tutor/production.py, lms/envs/tutor/development.py and lms/envs/tutor/production.py, each a comment of the form # RemovedInDjango5xWarning: 'xxx' is deprecated. …, which Tutor generates. The fifth is H3 on LegacyLibraryPagination, the documented paginator subclass. On the host checkout the same hygiene check gives PASS (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 gave 111 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-sdk tag, so the next openedx-platform-sdk regeneration gains v4_legacy_libraries_list. The v3 addresses are x-internal and drop out of generators that skip it.

  • frontend-app-authoring:

    • The libraries tab calls v1 and searches, filters and paginates the whole list client-side. Moving to v4 means paging (at most 100 per page) or asking for server-side search, before v1 and v3 are removed.
    • The draft FAA Make paver respect NO_PREREQ_INSTALL #3140 migrates to /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 unrequested CourseCreator record; the course rerun context is the other. Before that happens, POST /request_course_creator needs a write that creates the record: an explicit endpoint, or user_requested_access creating it when it is missing. This PR records the dependency and adds no POST.

  • edx-drf-extensions (library changes, not made here):

    1. DefaultPagination.get_paginated_response_schema documents four of the seven keys, and the inherited page_size description gives neither the default nor the cap. The library fix should cover both. This PR adds the second local subclass, after CourseTextbookPagination. The two now differ, since only this one carries the page_size text. 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).
    2. MethodNotAllowed and NotAcceptable are not cataloged, so a 405 or 406 is enveloped as internal / "Internal Server Error".
    3. The exception handler returns the generic 500 envelope without logging the exception, so no traceback reaches the server log.
    4. A keyed ValidationError is rendered into detail as 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 CMS handler404, 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_ref and writes from cms/djangoapps/contentstore/rest_api/v2/views/tests/helpers.py, a module scoped to the v2 tests. Hoist them next to routed_actions in cms/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 ordering parameter and a text-search parameter on v4.

  • Test environment: run in a Tutor container (openedx-dev:23.0.0-main) with openedx-core 1.11.0, openedx-authz 1.24.0 and openedx-forum 0.4.8 installed to match the pins (the image ships older releases). CI is the authoritative run.

🤖 Generated with Claude Code

Faraz32123 and others added 2 commits October 15, 2026 12:54
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>
@Faraz32123 Faraz32123 linked an issue Oct 15, 2026 that may be closed by this pull request
@Faraz32123
Faraz32123 marked this pull request as draft October 15, 2026 08:40
@Faraz32123 Faraz32123 mentioned this pull request Oct 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API] Libraries

1 participant