Repository navigation
feat: serve the course optimizer from authoring v1 - #39304
taimoor-ahmed-1 wants to merge 9 commits into
Conversation
The v0 advanced settings view checks course existence with the @verify_course_exists decorator and access with an inline check_course_advanced_settings_access call in each handler. The conforming views need these as permission classes. IsExistingCourse answers 404 for a course key that names no course run: an unknown course, a key that names only a version, or a library key. CanViewAdvancedSettings and CanManageAdvancedSettings make the same check_course_advanced_settings_access(read|write) call the v0 view makes, so the policy-permission and legacy Studio-access branches are unchanged. Existing classes in the module are untouched.
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id} answers
errors with developer_message bodies and some failures with a 500,
accepts Bearer tokens, returns every settings field by default, and
reports a refused value with the field parser's own text, which can be
Python exception text.
GET/PATCH /api/authoring/v1/courses/{course_key}/advanced_settings/ is
its successor. Errors use the standard envelope; a refused value is
reported under its setting name with one fixed message and the original
text is logged. The default read is the set of settings course authors
edit (v0 with fetch_all=0); view=full returns every field and fields
narrows either view. Authentication keeps the session class that admits
inactive users, as the legacy endpoint did; Bearer is dropped. Access is
IsAuthenticated, IsExistingCourse and a per-action class making v0's
read or write check, and an action without a declared permission is
never served. Parity tests compare v1 with v0 through the test client;
the declared differences are the error envelope and the fixed message.
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id} now has a
conforming successor at /api/authoring/v1/courses/{course_key}/advanced_settings/.
Both v0 operations are marked deprecated in the CMS schema through an
exact SUPERSEDED_PATHS entry, and the view's docstrings name the
successor, how each v0 read maps to a v1 read, and the deprecation
ticket. The view itself is unchanged. The schema hook test that used
this path as its example of an unrelated contentstore path now uses the
heartbeat, which is not deprecated.
Importing cms.djangoapps.contentstore.rest_api.v1.authoring_urls at module level builds its routes before cms/urls.py registers the course_key path converter, so collecting the module failed with an ImproperlyConfigured error. The URL-name test now imports it when it runs, as the youtube transcript tests do. Also wraps a line pylint reported as too long.
The first CI run of the module showed three expectations that did not match the platform: - The legacy role checks consult the policy engine, which admits superusers, so a superuser who is not global staff is served by v0 and v1 alike. The test now expects that. - The proctoring provider field keeps its default unless proctored exams are enabled, so the proctoring tests enable them. - The JWT middleware appends NotJwtRestrictedApplication to the permissions of views that accept JWTs, so the wiring test compares only the classes the view declares.
The side-effect recorder, the body comparison helpers, the schema helpers and the client helpers in the advanced settings tests are not specific to advanced settings. They move unchanged, under public names, into v1/views/tests/helpers.py so the other authoring v1 endpoint tests reuse one copy instead of each carrying its own.
The v0 course optimizer views check courses.edit_course_content inline, falling back to legacy read access to start a link check and to legacy write access for everything else. The conforming views need these as permission classes: CanStartLinkCheck makes the read-fallback call and CanEditCourseContent the write-fallback call, so the two stay separate when the endpoints are merged. Existing classes are unchanged.
The v0 course optimizer spreads two resources over four verb-style
addresses (link_check, link_check_status, rerun_link_update,
rerun_link_update_status), answers starts with 200, returns PascalCase
and camelCase reports, passes recorded failure text to clients, answers
a disabled feature with 400 and accepts Bearer tokens.
/api/authoring/v1/courses/{course_key}/link_check/ and
.../previous_run_link_update/ each take POST to start a run (202 with
its status) and GET to read the latest run the caller may see. The
default read is the status; view=full adds the report or the results.
Keys are snake_case, failures carry fixed messages, errors use the
standard envelope, a disabled feature answers 404, and links sent with
action=all are not read, as on v0. Starting a link check keeps v0's
read-level check and every other operation its write-level check, and
per-user run visibility is reused unchanged. Parity tests compare each
read and each queued task with v0.
The four v0 course optimizer operations now have conforming successors
under /api/authoring/v1/courses/{course_key}/. They are marked
deprecated in the CMS schema through exact SUPERSEDED_PATHS entries, and
the views' docstrings name the successors and the deprecation ticket.
The views themselves are unchanged.
|
Thanks for the pull request, @taimoor-ahmed-1! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Update the status of your PRYour PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate. Where can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Summary
Standardizes the course optimizer resources of the Course Management area (#39059, part of #38137). This is PR B of the eight resource-group PRs for the area. #39303 (advanced settings) is the first; custom pages and tabs, file assets, video transcripts and images, course settings, group configurations and team members follow separately.
Four action-style v0 routes become two resources, each with a verb per operation:
POST /api/contentstore/v0/link_check/{course_id}POST /api/authoring/v1/courses/{course_key}/link_check/authoring_v1:course_link_checkGET /api/contentstore/v0/link_check_status/{course_id}GET /api/authoring/v1/courses/{course_key}/link_check/POST /api/contentstore/v0/rerun_link_update/{course_id}POST /api/authoring/v1/courses/{course_key}/previous_run_link_update/authoring_v1:course_previous_run_link_updateGET /api/contentstore/v0/rerun_link_update_status/{course_id}GET /api/authoring/v1/courses/{course_key}/previous_run_link_update/v0 stays mounted and unchanged. It is marked
deprecated: truein the CMS schema and in its docstrings.It applies ADRs 0025–0038 and OEP-69.
Behaviour.
POSTqueues the same Celery task with the same arguments as v0 (check_broken_links,update_course_rerun_links). It answers 202 Accepted with{"status": "Pending"}.GETreturns the latest run of the course that the caller may see: their own, or anyone's for a superuser, through django-user-tasks'StatusFilterBackendas on v0.view(or withview=minimal) it returns the state of the run. For a link check that is{status, created_at, error}; for a link update,{status}.view=fulladds the link check's report (output) or the link update's per-linkresults.Uninitiated(no run) and the link-update task's own stages,ScanningandUpdating.{"action": "all" | "single", "data": [...]}.datais validated (each link needsurl,typeandid) whenactionissingle. Whenactionisallit is not read, as on v0.Why a new version and not a fix in place. Each of these changes the contract for v0's callers, so they ride v1 (ADR 0037):
LinkCheckStatus/displayNametostatus/display_name;view=full;The Course Optimizer page in frontend-app-authoring calls all four v0 routes (
src/optimizer-page/data/api.ts).Changes
feat: add course content editing permission classesuser_has_course_permissionand the legacy READ/WRITE fallback unchangedCanStartLinkCheck(the edit-course-content permission, with Studio read access as the legacy fallback, as v0's link check POST) andCanEditCourseContent(the same permission, with Studio write access as the fallback, as v0's other three operations)feat: serve the course optimizer from authoring v1POST/GET /api/authoring/v1/courses/{course_key}/link_check/,POST/GET /api/authoring/v1/courses/{course_key}/previous_run_link_update/(new)docs: mark the v0 course optimizer deprecated in favour of authoring v1deprecated: truein the schema (four exactSUPERSEDED_PATHSentries); the docstrings name each successor, what changes, and #39128ADRs applied
help_texton every field and.v1component names; the queued-run, status, report and result shapes are all publishedIsAuthenticated,IsExistingCourse, then a per-action class making the same check v0 makes for that operation; v0's READ-for-starting/WRITE-for-the-rest split is kept; unmapped actions are refused by construction; deny matrix with policy permissions off and on@extend_schemaon all four operations with summary, description, theviewparameter, request and response examples and every 4xx asErrorResponse; all four taggedopenedx-platform-sdkViewSets backed by a service module, mounted withpath(...)and the sharedcourse_keyconverter, as every route inauthoring_urls.pyisinternal, and per-link errors underdataare rendered as Python repr strings. A malformed key gets the CMS HTML 404 at routingPolicyCacheControlrow on the first permission check against a database that lacks it, as on v0. And the report builder thatview=fullreuses would create acourse_infoblock if a reported one had been deleted since the check ran; that path is currently unreachable in practice and also exists on v0POST(start) andGET(latest run). Thelink_updatestart's two operations are an enumeratedaction(all,single), each with its request shape. Each legacy permission check survives per actionview=fulland bounded by the links the run flaggedviewselects a representation and is validated by a serializer(JwtAuthentication, SessionAuthenticationAllowInactiveUser)declared with its reason, as ADR 0034 requires for an endpoint that usedSessionAuthenticationAllowInactiveUser; Bearer droppedview=minimal); the four-level report and the per-link results only withview=full, and the default read never builds themdeprecated: truein the schema; the DEPR entry is a comment on [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 until it is folded into the issue body/api/authoring/v1/courses/{course_key}/link_check/and.../previous_run_link_update/: domain API name, one-per-course children ofcourses, no verbs, shared converter, trailing slash, version-free URL names; the v0 names are keptget_queryset(); record visibility is django-user-tasks'StatusFilterBackend, reused unchanged through the providerCompliance matrix
QueuedRunSerializer(CourseOptimizerQueuedRun.v1),LinkCheckSerializer(LinkCheck.v1) with the report serializers (LinkCheckReport*.v1,LinkCheckPreviousRunLink.v1), andPreviousRunLinkUpdateSerializerwithLinkUpdateResult.v1. Request:PreviousRunLinkUpdateRequestSerializerwithLinkToUpdate.v1. Query: oneviewserializer per resource. The report serializers map the provider's camelCase members to snake_case withsource=. The linktypehelp text documents its four values (course_content,course_updates,handouts,custom_pages)CourseOptimizerSchemaTest(test_every_member_is_described,test_the_minimal_and_full_members,test_starting_a_link_update_takes_an_action_and_links,test_the_link_types_are_documented),CourseOptimizerReadTest.test_the_bodies_match_the_published_schema; hygiene H9user_has_course_permission(user, "courses.edit_course_content", course_key, READ|WRITE)call v0 makes inline, so the policy branch and the legacy Studio-access branch are inherited unchanged. Starting a link check keeps v0's READ fallback; reading it and both link-update operations keep WRITE; the two are separate classes, not one. An action missing from the per-action map raisesImproperlyConfiguredrather than running with login onlyCourseOptimizerAccessLegacyPermissionsTest(incl.test_starting_a_check_needs_only_read_access_and_the_rest_write_access,test_starting_a_check_is_refused_without_read_access),CourseOptimizerAccessAuthzTest,CourseOptimizerPermissionWiringTest; each access case asserts v0's answer and that a refused start queues nothingviewenum (minimal,full), 202/200/400/401/403/404 withErrorResponse, request examples for bothactionvalues, and aview=fullreport example. Operation IDsv1_courses_link_check_create/_retrieveandv1_courses_previous_run_link_update_create/_retrieve. All four are taggedopenedx-platform-sdk; the deprecated v0 operations lose the tag throughuntag_deprecated_operationsCourseOptimizerSchemaTest(test_the_operations_are_published_for_the_sdk,test_the_read_parameters,test_starting_a_link_check_takes_no_body,test_the_examples_are_valid_bodies,test_the_states_are_one_published_enum,test_the_v0_operations_are_deprecated_and_left_out_of_the_sdk,test_only_superseded_or_migrated_addresses_are_deprecated)CourseLinkCheckViewSetandCoursePreviousRunLinkUpdateViewSet(StandardizedErrorMixin,viewsets.ViewSet) withcreateandretrieveoverv1/course_optimizer_service.py. They are plain ViewSets because a run is a queued task read back from its recorded status, not a row. Deviation: mounted withpath("courses/<course_key:course_key>/.../", ...as_view({...})), because a router's regex routes cannot use the shared path convertersCourseOptimizerRoutingTest.test_the_addresses;CourseOptimizerQueryCount{LegacyPermissions,Authz}TestStandardizedErrorMixinfirst. 401authn, 403authz, 404not-found(unknown, version-only or library course key, or the feature off for link updates, each with a fixed detail), 400validationfor the body andview. A failed link check reports a fixederrormessage, and a link that could not be updated a fixederror_message. The text the tasks record can be an exception's, so it is never returned. Gaps pinned by tests that flip when fixed: (1) malformed JSON (400), 405, 406 and 415 are typedinternal, because edx-drf-extensions 10.9.0 does not catalogue them; (2) the errors of each link underdataare rendered as Python repr strings (e.g."{'url': [ErrorDetail(string='This field is required.', code='required')]}"), because the library does not normalize nested errors; (3) a malformed course key gets the CMS HTML 404 at routingCourseOptimizerReadTest(test_a_failed_link_check_reports_a_fixed_message_not_the_recorded_text,test_the_full_view_reports_each_links_outcome_with_a_fixed_failure_message);CourseOptimizerStartTest(test_each_link_needs_a_url_a_type_and_an_id,test_the_links_must_be_a_list);CourseOptimizerRoutingTest(test_a_malformed_json_body_is_a_400_typed_internal,test_an_unsupported_media_type_is_a_415_typed_internal,test_other_methods_are_not_allowed,test_unsupported_accept_is_refused_with_406,test_an_unparsable_or_slash_form_key_is_an_html_404)view=full, the run's artifact and the course outline. On every read, with policy permissions off and on, nothing is written to SQL or Mongo, no modulestore item is created or updated, and no Django signal or Open edX event is sent, including on commit. Exceptions, both outside this code and both present on v0: (a) the first read on a database without openedx-authz'sPolicyCacheControlrow inserts it (one INSERT, itspre_save/post_save), allow-listed by name; (b) the provider's report builder fetches each reported component withget_xblock, which creates acourse_infoblock that is not found. A reportedcourse_infoblock can only be missing if it was deleted after the check ran, so the path is currently unreachable, and no test exercises itCourseOptimizerReadSideEffects{LegacyPermissions,Authz}Test(test_reads_have_no_side_effects,test_the_first_v0_read_writes_what_the_first_v1_read_writes,test_the_creation_recorder_sees_an_item_created)link_check+link_check_status→link_check/, andrerun_link_update+rerun_link_update_status→previous_run_link_update/, with POST to start and GET for the latest run. Not merged with each other (different resources, gates and states). The link update's modes stay an enumeratedaction(all,single) withsinglerequiringdata, documented with an example each. Permissions are not flattened: per action, starting a check keeps READ and the rest keep WRITE. No deprecated aliases are written: the legacy routes stay on their own frozen viewsCourseOptimizerPermissionWiringTest.test_each_action_checks_the_course_then_its_own_permission;CourseOptimizerStartTest(test_every_link_is_queued_as_v0_queues_it,test_the_named_links_are_queued_as_v0_queues_them,test_single_needs_links);CourseOptimizerSchemaTest.test_the_examples_are_valid_bodiesviewis enum-validated by a serializer. v0 had no query parametersCourseOptimizerReadTest.test_an_unknown_view_is_refused(JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented at the declaration: kept from the legacy endpoints, so an inactive account's browser session stays authenticated and the course permission decides. Bearer is dropped. This matches #39303, #39100, #39157 and #39227; #39229/#39274 use the platform default on/api/authoring/v2/, so the authoring mounts are not yet uniformCourseOptimizerAuthenticationTest(test_a_jwt_caller_may_start_and_read,test_a_session_caller_may_start_and_read,test_an_inactive_session_is_authenticated,test_a_bearer_token_is_refused); hygiene H4view=minimal(default): the run's state only, projected withedx_rest_framework_extensions.shaping.project; the report builder and the results artifact are not touched.view=full: the four-level report, or the per-link results. Both shapes are published (outputandresultsoptional)CourseOptimizerReadTest(test_the_default_view_does_not_build_the_report,test_the_default_view_of_a_link_update_does_not_read_its_results,test_the_full_view_reports_the_flagged_links_in_the_course,test_the_default_view_of_a_link_update_is_its_state);CourseOptimizerSchemaTest.test_the_minimal_and_full_membersv1/. v0 frozen apart from docstring-only markers: the syntax tree with docstrings stripped equals the base.deprecated: truethrough four exactSUPERSEDED_PATHSentries; no runtime header (frozen routes). DEPR: an entry comment on #39128, to be folded into the issue bodyCmsMarkSupersededPathsTest.test_the_course_optimizer_operations_are_marked_and_their_successors_are_not;CourseOptimizerSchemaTest.test_the_v0_operations_are_deprecated_and_left_out_of_the_sdkauthoringAPI name,courses/{course_key}/link_check/andcourses/{course_key}/previous_run_link_update/(one-per-course children;rerun_link_updatewould have read as a verb),course_keyconverter, trailing slash,course_link_checkandcourse_previous_run_link_updateCourseOptimizerRoutingTest(test_the_addresses,test_the_v0_addresses_are_unchanged,test_v0_still_takes_a_slash_separated_key_that_v1_does_not,test_the_url_names_are_unique_in_the_namespace,test_the_address_without_its_trailing_slash_redirects_to_it); url_snapshot rules check on a hand-built snapshot (the urls gate itself was skipped)USER_TASKS_STATUS_FILTERS(StatusFilterBackend: own runs, everyone's for a superuser), through the provider exactly as v0. (3) User selection:viewonly selects a representationCourseOptimizerVisibility{LegacyPermissions,Authz}Test(test_each_caller_sees_only_their_own_link_checks,test_each_caller_sees_only_their_own_link_updates,test_a_run_of_another_course_is_not_seen,test_global_staff_who_is_not_a_superuser_sees_only_their_own_runs)Backward compatibility
v0 is unchanged.
v0/views/course_optimizer.pygains docstring text only; its syntax tree with docstrings stripped equals the base. Its routes, views and URL names are untouched, and v0's own tests are not edited.Same tasks, same arguments. Each accepted start queues the task v0 queues, with the arguments v0 passes. The tests assert the two calls are equal for the same caller and body. The one exception: when
actionisall, v0 hands anydatait was sent to the task, which ignores it, while v1 hands[].Parity. v1 is compared with v0 through the Django test client, with policy permissions off and on, after translating v0's member names to v1's. A successful link check's
view=fullreport is v0's report member for member. The only declared body differences, verbatim from the test module:Each successful case asserts the exact set of changed members, so a declared path cannot hide a difference elsewhere in the report.
test_every_declared_difference_occursfails if a declared difference never occurs.Status and request differences from v0. Each is pinned against v0's own answer. Verbatim from the test module:
The full table, with what else changed:
CourseOptimizerResponseDifferencesTest.test_starting_a_run_is_accepted…test_reads_of_an_unknown_courseuninitiated…test_reads_of_an_unknown_course…test_reads_of_an_unknown_course{"error"}…test_the_feature_off;CourseOptimizerFeatureOff{LegacyPermissions,Authz}Testurl,typeorid(single)…test_a_link_missing_a_member…test_a_body_that_is_not_an_objectviewother thanminimal/full…test_an_unknown_viewUninitiated…test_a_status_stored_in_the_sessionuninitiated,results: []Uninitiated…test_no_link_update_yet…test_a_bearer_token…test_a_slash_separated_course_keyCourseOptimizerRoutingTest.test_the_address_without_its_trailing_slash_redirects_to_itLinkCheckStatus,displayName, …status,display_name, …CourseOptimizerReadTest.test_the_full_report_is_v0s_report_in_snake_caseview=full_ParityMixin.test_reads_without_a_run,CourseOptimizerReadTest.test_the_default_view_does_not_build_the_report_ParityMixin.test_a_failed_link_check,_ParityMixin.test_a_link_update_with_a_failed_linkdatasent withaction: allCourseOptimizerStartTest.test_links_sent_with_action_all_are_not_readThe 401 → 404 → 403 order matches v0 wherever v0 looked the course up. For link updates, a caller without permission is still refused with 403 when the feature is off, as on v0.
Known inherited gap (both versions). A flagged link in a component nested below a unit's child, such as one inside a
library_contentorsplit_testblock, gives the provider's report a fifth level that the four-level shape cannot hold. The full read then answers 500, on v1 and on v0 alike; the default view is unaffected. The provider is not changed here. Pinned with alibrary_contentblock byCourseOptimizerReadTest.test_a_flagged_link_below_a_unit_child_fails_the_full_read_on_both_versions;split_testchildren take the same path in the provider and are not separately tested.Provider reuse. Run visibility comes from the provider's
_latest_task_statusand_latest_course_link_update_task_status, imported by their private names. They applyUSER_TASKS_STATUS_FILTERSexactly as v0 does, and the provider is a frozen legacy module, so this PR does not rename them. The report comes from its publicgenerate_broken_links_descriptorandsort_course_sections, andview=fullresults fromget_course_link_update_data.Query counts, measured against v0 in the same test (
CourseOptimizerQueryCount{LegacyPermissions,Authz}Test.test_reads_cost_no_more_than_v0):view=fullruns at most one more SQL query than v0: the course lookup, which v0's reads skip.No literal baseline was measured, because no platform environment was available.
Schema. Four new operations and
deprecated: trueon the four v0 operations. The run states are one enum component,StatusEnum, shared by both reads and both starts, so no new enum-naming warning is emitted. Risk: drf-spectacular names it from the field name, so if another CMS serializer later publishes astatusenum with different values, both get hash-suffixed names and the generator warns. Pinning the name needs anENUM_NAME_OVERRIDESsettings change, which this PR does not make (test_the_states_are_one_published_enumwill flag it).docs/cms-openapi.yamlis not edited; thegenerate_openapi_schemasworkflow regenerates it.In-place edits
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.pycms/djangoapps/contentstore/rest_api/v1/authoring_urls.pyCourseOptimizerRoutingTestcms/djangoapps/contentstore/views/permissions.py(shared)openedx_authz.constants.permissionsimport widened by one name; existing classes unchangedCourseOptimizerPermissionWiringTestand the access testscms/lib/spectacular.py,cms/lib/tests/test_spectacular.pySUPERSEDED_PATHSentries; the exact-set test updated, plus a test that the four v0 operations are marked and their successors are notCmsMarkSupersededPathsTestNew files inside the pre-existing
v1/:course_optimizer_service.py,serializers/course_optimizer.py,views/course_optimizer.py,views/tests/test_course_optimizer.py. They import nothing fromrest_api/v0/; the tests use the sharedv1/views/tests/helpers.pyadded by #39303.Error-format decision
Versioned. v0 keeps its
developer_message/{"error": ...}bodies, its 400 for the feature flag, its 500s and the recorded failure text unchanged. v1 uses the envelope. The Course Optimizer page calls v0 today (frontend-app-authoringsrc/optimizer-page/data/api.ts), so the format change rides the new version along with the member-name and status-code changes.Deprecations
The four v0 operations are superseded as in the Summary table. They are
deprecated: truein the CMS schema, and the docstrings name the successors. The views are otherwise unchanged: no runtime header. Tracked on the FC-0118 umbrella DEPR ticket, #39128; this PR's entry is posted there with the PR. Window per #39128: removed one named release after the release this ships in.Migration notes:
/api/authoring/v1/courses/{course_key}/link_check/and.../previous_run_link_update/, with the trailing slash:POSTto start a run,GETfor the latest one.{"status": "Pending"}.LinkCheckStatus→status,LinkCheckCreatedAt→created_at,LinkCheckError→error,LinkCheckOutput→output, and in the reportdisplayName→display_name,brokenLinks→broken_links,lockedLinks→locked_links,externalForbiddenLinks→external_forbidden_links,previousRunLinks→previous_run_links(original_link,is_updated,updated_link).view=full. Without it, the reads return the run's state only.Uninitiatedon both resources (the link update read spelled ituninitiated).erroranderror_messagecarry fixed messages, not the recorded failure text.action: all,datais not read and may be omitted. Withaction: single, every link needs a non-blankurl,typeandid.Authorization
@view_auth_classes()(IsAuthenticated) on all four viewsIsAuthenticatedtest_anonymous_caller_is_refused_with_401(both permission modes),test_anonymous_caller_of_an_unknown_course_is_refused_with_401@verify_course_exists()on the two v0 POSTs; none on the two GETsIsExistingCourseon all four (also 404 for version-only and library keys)test_unknown_course_is_404_before_permission_is_checked,test_a_version_only_course_key_is_404,test_a_library_is_not_a_course; v0 did not look the course up on its two reads (see the unknown-course rows of the difference table)user_has_course_permission(edit_course_content, READ)inlineCanStartLinkCheck, same calltest_starting_a_check_is_refused_without_read_access; no role, other-course staff/instructor, limited staff (legacy); auditor, limited staff, data researcher, beta tester, a role on another course, legacy staff without a policy role (policy permissions on)user_has_course_permission(edit_course_content, WRITE)inlineCanEditCourseContent, same calltest_starting_a_check_needs_only_read_access_and_the_rest_write_access(Studio read access without write → only the start is allowed, on v0 and v1); the same deny callers as aboveenable_course_optimizer_check_prev_run_links(course_key)after the permission check, 400 on v0test_link_updates_are_404_for_a_caller_who_may_use_them,test_a_caller_who_may_not_use_them_is_still_refused_with_403,test_the_feature_is_checked_before_the_body,test_instructors_and_global_staff_get_404_tooUSER_TASKS_STATUS_FILTERS(StatusFilterBackend) in the providertest_each_caller_sees_only_their_own_link_checks,test_each_caller_sees_only_their_own_link_updates,test_a_run_of_another_course_is_not_seen,test_global_staff_who_is_not_a_superuser_sees_only_their_own_runsOrder: 401, then 404 for a missing course, then 403, then 404 for the feature off, then 400 for the body. Allowed callers:
test_a_superuser_who_is_not_global_staff_is_allowed_as_on_v0). A superuser also sees everyone's runs, as on v0.Each access case asserts v0's status for the same caller. Every refused start asserts that no task was queued.
Gate report
Run on the host against
9b4d60ea0d(the head of #39303, on which this branch is stacked). No platform environment was available, so four gates were skipped. The test module has not been run locally, and CI is its first run.v0/views/course_optimizer.pydocstrings,v1/authoring_urls.py), 4 ADDED, nothing protected touched (23 shared legacy modules protected)url_snapshot.py check) on a hand-built snapshot of the new routesFollow-ups
openedx-platform-sdk; the next openedx-platform-sdk regeneration gains them.src/optimizer-page/data/api.tscan move to the v1 addresses. It needs:view=fullfor the report and the results; the new member names (the page camel-cases responses withcamelCaseObject, so it readsstatus,createdAtandoutputwhere it readlinkCheckStatus,linkCheckCreatedAtandlinkCheckOutput); 202 on start; 404 for the feature off.ParseError,MethodNotAllowed,NotAcceptableandUnsupportedMediaType, and normalize nested validation errors (per-item objects instead of Python repr strings;detailwithout the repr of the error dict). Issue to be filed; the pinned tests flip when it lands.PolicyCacheControl: the first permission check on a fresh database inserts the cache row on a read; seeding it by migration would make reads write-free. Issue to be filed.library_content,split_test) break the four-level report on v0 and v1. A future version could report such components at the unit level. Recorded as a future-version candidate; the provider is not changed here.COURSE_NOT_FOUND_MESSAGEand an inline course-existence check inCanViewCourse. Whichever of feat: serve the course outline from authoring v2 #39229 and feat: serve course advanced settings from authoring v1 #39303 lands second should build onIsExistingCourse, which this PR reuses./api/authoring/: v1 here, feat: serve course advanced settings from authoring v1 #39303 and the feat: apply FC-0118 ADRs to course content APIs #39157 v2 views keepSessionAuthenticationAllowInactiveUser; feat: serve the course outline from authoring v2 #39229 and feat: serve course containers from authoring v2 #39274 use the default. One policy for new versions of such endpoints should be agreed.link_check/andprevious_run_link_update/are singular, one-per-course children ofcourses/{course_key}/. They follow ADR 0038's owncourses/{course_key}/details/singleton precedent: each course has one current check and one current update.rerun_link_updatewas renamed because "rerun" reads as a verb and the resource is the update of links to the previous run.