Repository navigation
feat: add GET /api/authoring/v2/help_urls/ (FC-0118) - #39264
Open
Faraz32123 wants to merge 1 commit into
Open
Faraz32123 wants to merge 1 commit into
Faraz32123 wants to merge 1 commit into
Conversation
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
marked this pull request as draft
October 20, 2026 10:53
Faraz32123
marked this pull request as ready for review
October 21, 2026 11:26
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.
Closes #39067
Summary
This adds v2 of the Studio help URLs API at
GET /api/authoring/v2/help_urls/. It serves the documentation linkof 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_urlsis unchanged and is markeddeprecated: truein 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:
BearerAuthenticationAllowInactiveUser(set by@view_auth_classes). v2 accepts JWT and session only. A Bearer caller gets 401 on v2 and keeps working on v1.{"detail": …}. v2 errors are the standardized envelope(
type,title,status,detail,instance). Thedetailtext is the same.The success body is unchanged. v2 calls the same
get_help_urls()as v1, and the parity tests show the twobodies 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
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"}errorsHelpUrlsobject of 26 URL fields; JWT or session auth, inactive accounts admitted through session; the error envelope;openedx-platform-sdktagcms/lib/spectacular.py)GET /api/contentstore/v1/help_urlsdeprecated: truein the generated schema onlyv2/views/tests/test_help_urls.py,cms/lib/tests/test_spectacular.py)ADRs applied
HelpUrlsSerializer: 26 read-onlyURLFields in help-token order, each withhelp_text; explicitref_name = "HelpUrls"IsAuthenticated, the only legacy check (@view_auth_classes(is_authenticated=True)); deny paths tested200 → HelpUrls,401 → ErrorResponse;openedx-platform-sdktag; legacy op marked deprecatedAPIViewby the decision rule: one object with no members, which a viewsetlistwould publish as an arrayStandardizedErrorMixin; versioned, because v1 keeps{"detail"}/help_token/<token>redirect is a help-tokens library HTML redirect outside/api/(JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented; Bearer dropped/api/authoring/v2/help_urls/, URL namehelp_url_list, trailing slashget_queryset()and no records; the body is identical for every callerCompliance matrix
HelpUrlsSerializer. Noserializer_classis declared on theAPIView, which has noget_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_linkspermission_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@extend_schemaonget; tag on the classHelpUrlsSchemaTest: operationIdv2_help_urls_retrieve, tag, object component with 26string/uriproperties allrequired, 401 →ErrorResponse, nodeprecatedkeyAPIView; 0 queries in the viewtest_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_stackauthn(challenge header kept); 405/406internal; 500 generic, with no exception textassert_error_envelopein each refusal test;HelpUrlsResponseTest::test_an_unreadable_configuration_is_a_generic_server_error;HelpUrlsParityTest::test_the_unauthenticated_error_keeps_the_legacy_detailHelpUrlsIdempotenceTest::test_a_session_read_writes_nothing,::test_a_jwt_read_writes_nothing, harness self-check::test_the_capture_sees_a_write_when_one_happens/help_token/<token>is an HTML redirect owned by the help-tokens library, outside/api/.HelpUrlsResponseTest::test_the_body_is_one_object_keyed_by_token(keys equal the help tokens; noresults/count)HelpUrlsParityTest::test_query_strings_are_ignored_on_both_routesSessionAuthenticationAllowInactiveUseris 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_refusedv2/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.HelpUrlsSchemaTest::test_only_the_legacy_help_urls_operation_is_deprecated;CmsMarkSupersededPathsTest::test_only_the_legacy_help_urls_address_is_markedauthoring(Studio-specific). Collectionhelp_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_redirectsget_queryset(). Layer 1 isIsAuthenticated. 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:
SUPERSEDED_PATHSentryBearerAuthenticationAllowInactiveUserto the v2 tupleauthentication_classespermission_classesStandardizedErrorMixinSDK tag
The operation carries
tags=["openedx-platform-sdk"], so the SDK gainsv2_help_urls_retrieve, which returns a typed26-field
HelpUrlsmodel.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.pyandv1/urls.pyare not in the diff. Therest_api/v1/test packagepasses unmodified (288 passed).
Same data path. v2 calls the same
get_help_urls(), which is not modified and is protected by the versionsgate.
Parity tests (
HelpUrlsParityTest) compare v1 and v2 for one signed-in caller: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.
?foo=bar,?view=minimal,?fields=home): ignored on both routes.{"detail": "Authentication credentials were not provided."}; v2 carries thesame
detailin the envelope.Intentional differences, asserted to be the only ones and asserted to occur:
typetitlestatusunlisted_pageOther 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:
Output:
ALLOWEDv2/authoring_urls.py;ADDEDv2/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 pathhelp_tokens.ini: the token sourceview_utils.py: the legacy auth decoratorrest_api/urls.py: thev1/includecms/urls.py: the legacy and authoring mountsThe three
ADDEDfiles are new modules for a new resource version inside the per-resourcev2/package, whichalready holds other resources (
textbooks,video_settings).In-place edits
cms/djangoapps/contentstore/rest_api/v2/authoring_urls.pypath("help_urls/", …, name="help_url_list"). The two existing routes keep their pattern, name and view.v2/views/tests/test_textbooks.pyandtest_video_settings.pypass unmodified (the wholev2/views/tests/package plustest_spectacular.py: 271 passed); URL snapshot gate pending step 4cms/lib/spectacular.pySUPERSEDED_PATHSentry. 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_markedleaves 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.pytest_the_superseded_paths_are_exactly_these, since not every entry has two spellings now.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 requiresa 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 catalogueMethodNotAllowedorNotAcceptable. They are deliberately not registered locally, because the error-type registry is process-wide andwould change every standardized Studio API.
Deprecations
GET /api/contentstore/v1/help_urlsis superseded byGET /api/authoring/v2/help_urls/and markeddeprecated: truein the published schema. The superseded view is unchanged: no response header, and no deprecationnotes 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
IsAuthenticated@view_auth_classes(is_authenticated=True)(openedx/core/lib/api/view_utils.py)permission_classes = (IsAuthenticated,)Deny-path tests:
WWW-Authenticate: JWT realm="api". POST/PUT/PATCH/DELETE → 401, on v2 andv1.
Gate report
v2/authoring_urls.py), 3 ADDED (new per-resource v2 modules), 0 VIOLATIONauthoring_v2namespace absent in container. Schema properties verified byHelpUrlsSchemaTestusinggenerate_schema(): operationId, tag, component name, type, 26 required fields, v1 deprecated, v2 not deprecated.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.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 inHelpUrlsSchemaTestandreverse()/resolve()calls inHelpUrlsRoutingTest).Review
Review found 0 blockers, 0 majors. Two minor findings were investigated and closed as non-issues:
@extend_schema— the sibling viewvideo_settings.pyomits these status codes (project convention); the schema test already pinsset(responses) == {"200", "401"}as the explicit contract. Adding 405/406 would contradict project convention and break that assertion. Not applied.help_textassertion 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 26help_textstrings, 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
src/help-urls/data/api.tsto/api/authoring/v2/help_urls/(with the trailingslash), and drop the 8 keys the server has never sent. This is a separate PR after this merges.
MethodNotAllowed,NotAcceptable,ParseErrorandUnsupportedMediaType, sothat 405/406/400/415 are not typed
internal. No issue exists yet; to be filed.the schema lists it as required. Decide between failing loudly and
allow_nullin the next version.get_help_urls()reassigns the help-tokens singleton's config on every call,which may race under threaded workers. Not reproduced.
v2_help_urls_retrieveafter merge.🤖 Generated with Claude Code