Skip to content

feat: add GET /api/authoring/v2/help_urls/ (FC-0118) - #39264

Open
Faraz32123 wants to merge 1 commit into
openedx:masterfrom
Faraz32123:feat/fc-0118-help-urls-api-v2
Open

Faraz32123 wants to merge 1 commit into
openedx:masterfrom
Faraz32123:feat/fc-0118-help-urls-api-v2

Conversation

@Faraz32123

@Faraz32123 Faraz32123 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Closes #39067

Summary

This adds v2 of the Studio help URLs API at GET /api/authoring/v2/help_urls/. It serves the documentation link
of each Studio page, keyed by help token, built to the FC-0118 REST API conventions (umbrella #38137, OEP-69). The
legacy GET /api/contentstore/v1/help_urls is unchanged and is marked deprecated: true in the published schema.

Why a new version, not an in-place edit. Two parts of the contract change for callers, and neither may change on
the published v1:

  • Authentication. v1 accepts DOT Bearer tokens through BearerAuthenticationAllowInactiveUser (set by
    @view_auth_classes). v2 accepts JWT and session only. A Bearer caller gets 401 on v2 and keeps working on v1.
  • Error body. v1 errors are DRF's {"detail": …}. v2 errors are the standardized envelope
    (type, title, status, detail, instance). The detail text is the same.

The success body is unchanged. v2 calls the same get_help_urls() as v1, and the parity tests show the two
bodies are byte-identical under stock settings and under each documentation setting. The only Studio consumer,
frontend-app-authoring src/help-urls, stays on v1 until a separate one-line URL change.

ADRs: 0025, 0026, 0027, 0028, 0029, 0030, 0031, 0032, 0033, 0034, 0035, 0036, 0037, 0038.

Changes

Concern Endpoint Legacy behaviour kept New behaviour
v2 view, serializer and route (cms/djangoapps/contentstore/rest_api/v2/{views,serializers}/help_urls.py, v2/authoring_urls.py) GET /api/authoring/v2/help_urls/ (authoring_v2:help_url_list) GET /api/contentstore/v1/help_urls (cms.djangoapps.contentstore:v1:help_urls) keeps its address, view, Bearer acceptance and {"detail"} errors A typed HelpUrls object of 26 URL fields; JWT or session auth, inactive accounts admitted through session; the error envelope; openedx-platform-sdk tag
Deprecation marker (cms/lib/spectacular.py) GET /api/contentstore/v1/help_urls Served unchanged deprecated: true in the generated schema only
Tests (v2/views/tests/test_help_urls.py, cms/lib/tests/test_spectacular.py) both — 37 new endpoint tests; superseded-path hook tests updated

ADRs applied

  • 0025 serializers — HelpUrlsSerializer: 26 read-only URLFields in help-token order, each with help_text; explicit ref_name = "HelpUrls"
  • 0026 permissions — IsAuthenticated, the only legacy check (@view_auth_classes(is_authenticated=True)); deny paths tested
  • 0027 schema / docs — summary, description, 200 → HelpUrls, 401 → ErrorResponse; openedx-platform-sdk tag; legacy op marked deprecated
  • 0028 viewsets — APIView by the decision rule: one object with no members, which a viewset list would publish as an array
  • 0029 errors — StandardizedErrorMixin; versioned, because v1 keeps {"detail"}
  • 0030 GET idempotence — no writes and no signals on GET or HEAD, by SQL and signal capture for session and JWT callers
  • 0031 merged endpoints — n/a: one endpoint performing one operation; the /help_token/<token> redirect is a help-tokens library HTML redirect outside /api/
  • 0032 pagination — n/a: the response is one object of 26 links, bounded by the deployment's help tokens file
  • 0033 filtering — n/a: the endpoint takes no query parameters (test shows they are ignored)
  • 0034 authentication — declared deviation (JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented; Bearer dropped
  • 0035 MFE config — n/a: an authenticated, CMS-only lookup of Studio documentation links, not front-end runtime configuration
  • 0036 nested JSON — n/a: flat object of strings, depth 1
  • 0037 versioning — new v2 module beside the existing v2 resources; v1 untouched; one deviation, no docstring marker on the superseded view (see Deprecations)
  • 0038 URL structure — /api/authoring/v2/help_urls/, URL name help_url_list, trailing slash
  • OEP-66 queryset scoping — n/a: no get_queryset() and no records; the body is identical for every caller
  • OEP-69 conventions — met through the rows above: typed schema component, standardized errors, conforming address

Compliance matrix

Decision Status How it is met, or the concrete reason it is excluded Evidence (test / gate)
0025 met The response is emitted through HelpUrlsSerializer. No serializer_class is declared on the APIView, which has no get_serializer. HelpUrlsSerializerTest::test_the_fields_follow_the_help_tokens_file_in_order, ::test_every_field_is_a_read_only_url, ::test_every_field_describes_its_own_page; HelpUrlsSchemaTest::test_the_success_body_is_one_object_of_links
0026 met permission_classes = (IsAuthenticated,) HelpUrlsAccessTest::test_anonymous_is_refused_with_a_challenge, ::test_anonymous_writes_are_refused_before_the_method_is_checked, ::test_signed_in_callers_are_served
0027 met @extend_schema on get; tag on the class HelpUrlsSchemaTest: operationId v2_help_urls_retrieve, tag, object component with 26 string/uri properties all required, 401 → ErrorResponse, no deprecated key
0028 met APIView; 0 queries in the view test_the_success_body_is_one_object_of_links (type: object), HelpUrlsResponseTest::test_the_view_makes_no_queries, HelpUrlsParityTest::test_neither_view_makes_a_query, ::test_both_routes_cost_the_same_through_the_full_request_stack
0029 met 401 authn (challenge header kept); 405/406 internal; 500 generic, with no exception text assert_error_envelope in each refusal test; HelpUrlsResponseTest::test_an_unreadable_configuration_is_a_generic_server_error; HelpUrlsParityTest::test_the_unauthenticated_error_keeps_the_legacy_detail
0030 met No domain writes; the help-tokens re-read is in-process state HelpUrlsIdempotenceTest::test_a_session_read_writes_nothing, ::test_a_jwt_read_writes_nothing, harness self-check ::test_the_capture_sees_a_write_when_one_happens
0031 excluded One endpoint, one operation. The related /help_token/<token> is an HTML redirect owned by the help-tokens library, outside /api/. —
0032 excluded A single object of 26 entries; request data does not grow it HelpUrlsResponseTest::test_the_body_is_one_object_keyed_by_token (keys equal the help tokens; no results/count)
0033 excluded No query parameters HelpUrlsParityTest::test_query_strings_are_ignored_on_both_routes
0034 met (deviation declared) SessionAuthenticationAllowInactiveUser is kept, because Studio users reach the help links while their account is inactive. Bearer is not accepted. HelpUrlsAccessTest::test_a_bearer_token_is_not_accepted, ::test_the_legacy_route_still_accepts_a_bearer_token, ::test_an_inactive_caller_is_served_on_both_routes, ::test_a_jwt_caller_is_served, ::test_an_expired_jwt_is_refused, ::test_a_malformed_jwt_is_refused
0035 excluded Authenticated, CMS-only, Studio-specific. The CMS help tokens file is not visible to the LMS configuration endpoint. —
0036 excluded Flat object, depth 1 —
0037 met; one deviation New resource version in the per-resource v2/ package. v1 frozen. Deprecated by schema flag and the umbrella DEPR ticket. Deviation: no docstring marker on the superseded view, because the DEPR ticket carries it. Versions gate (see Backward compatibility); HelpUrlsSchemaTest::test_only_the_legacy_help_urls_operation_is_deprecated; CmsMarkSupersededPathsTest::test_only_the_legacy_help_urls_address_is_marked
0038 met API name authoring (Studio-specific). Collection help_urls, top-level: no course input. No identifier, no verb. HelpUrlsRoutingTest::test_url_names_reverse_to_their_addresses, ::test_each_address_resolves_to_its_own_view, ::test_each_address_has_one_spelling, ::test_a_slashless_address_redirects
OEP-66 excluded No get_queryset(). Layer 1 is IsAuthenticated. Layers 2 and 3 are empty: no per-user, per-course or per-org rows, and no parameters. —

Mutation checks. Each harness was reverted against the behaviour it guards, and every guarding test went red:

  • deleting a serializer field
  • swapping two serializer fields
  • removing the SUPERSEDED_PATHS entry
  • adding BearerAuthenticationAllowInactiveUser to the v2 tuple
  • dropping authentication_classes
  • dropping permission_classes
  • dropping StandardizedErrorMixin
  • rewriting one v2 value
  • making v2 ignore the documentation settings
  • removing the test fixture's help-tokens singleton reset
  • writing a row on GET
  • adding a query to the view

SDK tag

The operation carries tags=["openedx-platform-sdk"], so the SDK gains v2_help_urls_retrieve, which returns a typed
26-field HelpUrls model.

The issue names programmatic consumers ("AI chatbots, external tools, or automated guides"), and the response is fully
typed. The merged v2 sibling courses/{course_key}/video_settings/ carries the same tag. The change is additive.
Untagging later would remove a published SDK method.

Backward compatibility

  • v1 is untouched. v1/views/help_urls.py and v1/urls.py are not in the diff. The rest_api/v1/ test package
    passes unmodified (288 passed).

  • Same data path. v2 calls the same get_help_urls(), which is not modified and is protected by the versions
    gate.

  • Parity tests (HelpUrlsParityTest) compare v1 and v2 for one signed-in caller:

    • Stock settings: byte-identical bodies, same key order.
    • Documentation version override, documentation books override, and each language branch: a mapped locale, the
      file's fallback locale, and no language. These run on a temporary help tokens file with known values, assert
      every link, and assert the output differs from stock.
    • Query strings (?foo=bar, ?view=minimal, ?fields=home): ignored on both routes.
    • Unauthenticated: v1 body is exactly {"detail": "Authentication credentials were not provided."}; v2 carries the
      same detail in the envelope.

    Intentional differences, asserted to be the only ones and asserted to occur:

    Case Path Reason
    401 type the envelope names the error type
    401 title the envelope titles the error type
    401 status the envelope repeats the HTTP status
    help tokens file with a token the serializer does not declare unlisted_page the v2 body carries the declared tokens only
  • Other differences by design: a Bearer token gets 401 on v2 (200 on v1); errors are the envelope.

  • Addresses. reverse("cms.djangoapps.contentstore:v1:help_urls") == "/api/contentstore/v1/help_urls" is pinned.
    The legacy name and address are unchanged.

  • Versions gate:

    check_old_versions_untouched.sh --base origin/master \
      --protect cms/djangoapps/contentstore/utils.py \
      --protect cms/envs/help_tokens.ini \
      --protect openedx/core/lib/api/view_utils.py \
      --protect cms/djangoapps/contentstore/rest_api/urls.py \
      --protect cms/urls.py \
      --allow 'cms/djangoapps/contentstore/rest_api/v2/authoring_urls.py'
    

    Output: ALLOWED v2/authoring_urls.py; ADDED v2/serializers/help_urls.py, v2/views/help_urls.py,
    v2/views/tests/test_help_urls.py; RESULT: PASS.

    The protected paths are the unversioned legacy surfaces this endpoint depends on:

    • contentstore/utils.py: get_help_urls(), the shared data path
    • help_tokens.ini: the token source
    • view_utils.py: the legacy auth decorator
    • rest_api/urls.py: the v1/ include
    • cms/urls.py: the legacy and authoring mounts

    The three ADDED files are new modules for a new resource version inside the per-resource v2/ package, which
    already holds other resources (textbooks, video_settings).

In-place edits

File Category Evidence
cms/djangoapps/contentstore/rest_api/v2/authoring_urls.py Strictly opt-in addition: one import and one path("help_urls/", …, name="help_url_list"). The two existing routes keep their pattern, name and view. v2/views/tests/test_textbooks.py and test_video_settings.py pass unmodified (the whole v2/views/tests/ package plus test_spectacular.py: 271 passed); URL snapshot gate pending step 4
cms/lib/spectacular.py Not a version directory. A service-wide hook, so the entry is scoped to this pass's own exact path: one SUPERSEDED_PATHS entry. The comment above it is narrowed, because only the optional-final-character routes are listed in two spellings. CmsMarkSupersededPathsTest::test_only_the_legacy_help_urls_address_is_marked leaves unmarked the v2 address, the slashed legacy spelling, /api/contentstore/v1/proctored_exam_settings/{course_id} and /api/authoring/v2/courses/{course_key}/textbooks/
cms/lib/tests/test_spectacular.py Tests. The exact-set test gains the entry and is renamed to test_the_superseded_paths_are_exactly_these, since not every entry has two spellings now. green

The superseded view is not among them: it carries no deprecation notice.

Error-format decision

Versioned. v2 uses StandardizedErrorMixin, and v1 keeps {"detail"}. The authentication change already requires
a new version, so the envelope rides on it. frontend-app-authoring does not read the error body, so an in-place change
would also have qualified; it is moot here.

405 and 406 come back typed internal: edx-drf-extensions 10.9.0 does not catalogue MethodNotAllowed or
NotAcceptable. They are deliberately not registered locally, because the error-type registry is process-wide and
would change every standardized Studio API.

Deprecations

GET /api/contentstore/v1/help_urls is superseded by GET /api/authoring/v2/help_urls/ and marked
deprecated: true in the published schema. The superseded view is unchanged: no response header, and no deprecation
notes in code.

Tracked on the FC-0118 umbrella DEPR ticket, #39128, which removes it one named release after the release this ships
in. This area's entry is posted there once the PR is open. Of the umbrella's step-1 markers, this PR takes only the
schema flag.

Authorization

Legacy check Where v2
IsAuthenticated @view_auth_classes(is_authenticated=True) (openedx/core/lib/api/view_utils.py) permission_classes = (IsAuthenticated,)
(none other; the body is identical for every caller) — —

Deny-path tests:

  • Anonymous: GET and HEAD → 401 with WWW-Authenticate: JWT realm="api". POST/PUT/PATCH/DELETE → 401, on v2 and
    v1.
  • Bad tokens: an expired JWT → 401 (v2 and v1); a malformed JWT → 401; a Bearer token → 401 on v2 and 200 on v1.
  • Signed-in writes: non-staff, staff and inactive callers get 405 on POST/PUT/PATCH/DELETE.
  • Wrong user: not applicable, because no caller-specific data exists.
  • Allowed: non-staff, staff and inactive (session, GET and HEAD) → 200; JWT → 200.

Gate report

Gate Result Notes
versions PASS 1 ALLOWED (v2/authoring_urls.py), 3 ADDED (new per-resource v2 modules), 0 VIOLATION
hygiene PASS 0 FAIL, 0 WARN across 6 scanned files
schema SKIPPED Environment constraint: Tutor container checkout is 622 commits behind origin/master; authoring_v2 namespace absent in container. Schema properties verified by HelpUrlsSchemaTest using generate_schema(): operationId, tag, component name, type, 26 required fields, v1 deprecated, v2 not deprecated.
urls SKIPPED Same environment constraint as schema gate. URL properties verified by HelpUrlsRoutingTest: reverse('authoring_v2:help_url_list') == '/api/authoring/v2/help_urls/'; slashless GET redirects 301; v1 and v2 routes resolve to distinct view classes.
old-tests PASS 288 passed (cms/djangoapps/contentstore/rest_api/v1/), 1097 warnings
tests (new) PASS 59 passed, 76 warnings, 3 subtests passed (37 new test_help_urls.py, 22 test_spectacular.py); mutation check: all 12 mutations caught

The schema and URL gates were skipped due to a fixed environment constraint (container 622 commits behind origin/master). The properties those gates check are fully covered by test assertions (generate_schema() calls in HelpUrlsSchemaTest and reverse()/resolve() calls in HelpUrlsRoutingTest).

Review

Review found 0 blockers, 0 majors. Two minor findings were investigated and closed as non-issues:

  • 405/406 in @extend_schema — the sibling view video_settings.py omits these status codes (project convention); the schema test already pins set(responses) == {"200", "401"} as the explicit contract. Adding 405/406 would contradict project convention and break that assertion. Not applied.
  • help_text assertion per field name — the fix-pass assumed the format "<token_name> documentation page URL." but the serializer uses "Documentation page URL for <description>.". The field name is not present in 16 of 26 help_text strings, so the proposed assertion would immediately fail on 16 fields. The existing test checks prefix uniformity and uniqueness, which is the correct guard. Not applied.

Follow-ups

  • frontend-app-authoring: move src/help-urls/data/api.ts to /api/authoring/v2/help_urls/ (with the trailing
    slash), and drop the 8 keys the server has never sent. This is a separate PR after this merges.
  • edx-drf-extensions: catalogue MethodNotAllowed, NotAcceptable, ParseError and UnsupportedMediaType, so
    that 405/406/400/415 are not typed internal. No issue exists yet; to be filed.
  • Next-version candidate: if a deployment's help tokens file lacks a token v2 declares, v2 omits that key although
    the schema lists it as required. Decide between failing loudly and allow_null in the next version.
  • Pre-existing, not changed here: get_help_urls() reassigns the help-tokens singleton's config on every call,
    which may race under threaded workers. Not reproduced.
  • SDK: regeneration picks up v2_help_urls_retrieve after merge.

🤖 Generated with Claude Code

Adds the standardized v2 help URLs endpoint beside the existing v1.
v1 is frozen and marked deprecated in the schema. Bearer auth dropped;
error envelope added. Success body is identical to v1.

Closes openedx#39067

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Faraz32123
Faraz32123 marked this pull request as draft October 20, 2026 10:53
@Faraz32123
Faraz32123 marked this pull request as ready for review October 21, 2026 11:26
@Faraz32123
Faraz32123 requested a review from feanil October 21, 2026 11:26

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] Help & Documentation

1 participant