Repository navigation
feat: serve course rerun defaults and settings from authoring v2 - #39307
taimoor-ahmed-1 wants to merge 13 commits into
Conversation
The course index existed only at v1, with Bearer auth, a legacy error
body, a slashless address, the whole screen payload as its only shape,
and writes on GET (pre-1900 start dates re-saved, a course overview
built for the clipboard title).
v2 serves GET /api/authoring/v2/courses/{course_key}/outline/. By
default it returns a thin outline tree two levels deep, where every cut
node carries a followable children_url. ?view=full returns v1's screen
payload under v1's names, without the clipboard. Reads write nothing.
Authentication is the platform default, the course-view permission and
its legacy fallback are kept in CanViewCourse, and errors use the
standardized envelope. The v1 course index is marked deprecated in the
CMS schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docstring-only markers naming the v2 successor and DEPR openedx#39128. With docstrings stripped the module matches the base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mypy checks rest_api/v2/views, and pytz ships no type stubs, so the import failed make check-types. The standard library's UTC gives the same timezone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
# Conflicts: # cms/djangoapps/contentstore/views/permissions.py # cms/lib/spectacular.py # cms/lib/tests/test_spectacular.py
Adds GET /api/authoring/v2/courses/{course_key}/rerun_defaults/, replacing
GET /api/contentstore/v1/course_rerun/{course_id}: the values Studio
prefills when re-running a course. Access stays limited to accounts with
staff status, as on v1.
The endpoint uses the standard error envelope, drops Bearer
authentication, answers 404 for an unknown course before checking access,
and is tagged for the SDK. The legacy path is marked deprecated in the
published schema.
Adds GET /api/authoring/v2/courses/{course_key}/settings/, replacing
GET /api/contentstore/v1/course_settings/{course_id}. The default view
returns the scalar settings; view=full adds the language options, the
courses that may be prerequisites and the credit requirements.
It gains content_experiments_enabled, the course-level flag the v2 group
configurations listing no longer carries. Course-list query parameters no
longer narrow or reorder the prerequisite list. The endpoint uses the
standard error envelope, drops Bearer authentication, and is tagged for
the SDK. The legacy path is marked deprecated in the published schema.
…avour of authoring v2 Docstring-only: names the successor addresses and the deprecation ticket.
|
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 rerun defaults and course settings resources of the Course Management area (#39059, part of #38137). This is PR F of the eight resource-group PRs for the area; #39303 (advanced settings) is PR A and #39305 (custom pages and tabs) is PR C.
Two conforming operations replace the two v1 endpoints:
deprecated: true)authoring_v2:)GET /api/contentstore/v1/course_rerun/{course_id}GET /api/authoring/v2/courses/{course_key}/rerun_defaults/course_rerun_defaultsGET /api/contentstore/v1/course_settings/{course_id}GET /api/authoring/v2/courses/{course_key}/settings/(?view=minimal, the default, or?view=full)course_settingsIt applies ADRs 0025–0038 and OEP-69.
Rerun defaults. The values Studio's rerun page starts from: the course's
org,number,runanddisplay_name, the site'sallow_unicode_course_id, and the caller'scourse_creator_status. These are the same six fields as v1, with the same values. Only global staff may read them. v2 uses DRF'sIsAdminUser, which checksuser.is_staff, as v1'sGlobalStaff().has_userdoes; a test compares the two for every combination ofis_staff,is_superuserandis_active. The address names what the payload is (the defaults for a rerun of this course), not v1's screen name. Since only staff get past the permission check, the course-creator lookup never runs for anyone else. That matters because, withENABLE_CREATOR_GROUPon, the lookup records a course creator request for a non-staff user, which is a write.course_creator_statusis therefore alwaysgranted, and its help text says so.Course settings. What Studio's settings pages need about a course and the site: which settings are edited in Studio, which site features are on, the course's names, about page address, proctored exam settings path and upgrade deadline.
view=minimal(the default) returns these 20 members.view=fulladdslanguage_options,possible_pre_requisite_courses(while prerequisite courses are on) andcredit_requirements(for a credit course while credit eligibility is on). Access goes throughCanViewCoursefrom #39229: the course must exist (404), then the caller needsuser_has_course_permission(courses.view_course, READ), the same call v1 makes inline. Two content changes:content_experiments_enabledis added. It says whether content experiments apply to the course (split_testinstalled and in the course's advanced modules). This is the value v1's group configurations listing reports asshould_show_experiment_groups. That listing's successor (PR G) drops the flag, and also dropsmfe_proctored_exam_settings_url, which v1 course settings already returns and v2 keeps. Both fields' help texts tell callers to read them here.search,ordering,active_onlyand its other parameters. Sent to v1, those parameters narrowed or reorderedpossible_pre_requisite_courses, and a malformedstart_date_on_or_aftermade v1 answer 400. v2 gives the listing a request that carries the caller and no query parameters.Known gaps and risks have their own section below.
Why a new version and not a fix in place. Each of these changes the contract for v1's callers (frontend-app-authoring calls both endpoints), so they ride v2 (ADR 0037):
view=full), gainscontent_experiments_enabled, and refuses an unknownview(400);Changes
Commits (one per concern):
feat: serve course rerun defaults from authoring v2GET …/v2/courses/{course_key}/rerun_defaults/(new)contentstore.utils.get_course_rerun_context, which is not edited. Permissions: DRF'sIsAdminUserandIsExistingCoursefrom #39303feat: serve course settings from authoring v2GET …/v2/courses/{course_key}/settings/(new)contentstore.utils.get_course_settings,contentstore.views.course.are_content_experiments_enabledandcertificates.api.can_show_certificate_available_date_field, none of them edited. Permission:CanViewCoursefrom #39229docs: mark the v1 course rerun and settings endpoints deprecated in favour of authoring v2deprecated: truein the schema comes from the two exactSUPERSEDED_PATHSentries, each with a test, added in the feat commits so every commit is green.)ADRs applied
CourseRerunDefaults.v2,CourseSettings.v2,PrerequisiteCourse.v2,CreditRequirement.v2),help_texton every field, explicit.v2component names, a query serializer forview; read-only, so no request body serializerIsAuthenticated,IsExistingCourse,IsAdminUser(equivalent to v1'sGlobalStaff().has_user); settings:IsAuthenticated,CanViewCourse(v1's inline call); deny matrix with policy permissions off and on@extend_schemaon both with summary, description, parameters, examples and every 4xx asErrorResponse; both taggedopenedx-platform-sdkViewSets over service modules, mounted withpath(...)like every route inv2/authoring_urls.py, because a router's regex routes cannot use the shared path convertersinternal, and a malformed course key or an address no route matches gets the CMS HTML 404 (library and routing gaps, pinned by tests); the publishedErrorResponsecomponent's text comes from edx-drf-extensionsPolicyCacheControlrow on the first lookup against a database that lacks it, exactly as the v1 read does therecontent_experiments_enabledis one member moved from the group configurations listing, not a merged endpoint(JwtAuthentication, SessionAuthenticationAllowInactiveUser)declared with its reason, as ADR 0034 requires for an endpoint that usedSessionAuthenticationAllowInactiveUser; Bearer droppedview=full, so they are never in the default payload (payload only: see Known gaps and risks); rerun defaults are six flat scalarsdeprecated: truein the schema; the DEPR entry for [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 is to be posted with this PR, and no removal release is named yetcourses/{course_key}/rerun_defaults/andcourses/{course_key}/settings/; shared converter, trailing slash, version-free URL names; the v1 names kept (naming risk onsettings/stated)get_queryset(); the prerequisite list comes from Studio's course listing for the caller, as on v1; the layering is shown in the matrixCompliance matrix
CourseRerunDefaultsSerializer(CourseRerunDefaults.v2: v1's six fields,display_namenullable).CourseSettingsSerializer(CourseSettings.v2: 20 required members, pluscredit_requirements,language_optionsandpossible_pre_requisite_courses, which are not required). The prerequisite items are typed (PrerequisiteCourse.v2), the credit requirements are typed (CreditRequirement.v2, grouped by namespace) and the language options are[code, name]pairs.CourseSettingsQuerySerializervalidatesview. Explicit.v2ref_names: v1's serializers have none, so they publish asCourseRerunandCourseSettings, and an unsuffixed v2CourseSettingswould collide with v1's. Nullability follows the data, not v1's declarations (Known gaps and risks)RerunDefaultsSchemaTest(test_the_defaults_component,test_the_example_is_a_valid_body);RerunDefaultsReadTest.test_the_body_matches_the_published_schema;SettingsSchemaTest(test_the_settings_component,test_the_prerequisite_course_component,test_every_member_is_described);SettingsReadTest.test_the_body_matches_the_published_schema(both views); hygiene PASSIsAuthenticated,IsExistingCourse,IsAdminUser. Settings:IsAuthenticated,CanViewCourse(existence, thencourses.view_courseREAD). Each viewset has one permission tuple, soretrieve, OPTIONS and unrouted methods all run the same checksRerunDefaultsAccess{LegacyPermissions,Authz}Test,SettingsAccess{LegacyPermissions,Authz}Test(each access case asserts v1's answer too);RerunDefaultsPermissionWiringTest(test_the_course_is_checked_before_staff_status,test_the_staff_check_decides_as_the_v1_check);SettingsPermissionWiringTest.test_the_course_view_permission_is_checked_after_authentication;RerunDefaultsCourseCreatorTest.test_a_caller_who_is_not_staff_is_refused_before_the_lookupv2_courses_rerun_defaults_retrieve,v2_courses_settings_retrieve.course_keyis a path parameter with the converter's pattern; settings addsview(query, enumminimal/full, defaultminimal). ExamplesRerunDefaults,CourseSettingsandCourseSettingsWithListsvalidate against the published components. Every 4xx isErrorResponse(settings 400, 401, 403, 404; rerun defaults 401, 403, 404). Both taggedopenedx-platform-sdk; the deprecated v1 operations lose the tag throughuntag_deprecated_operationsRerunDefaultsSchemaTest(test_the_read_is_published_for_the_sdk,test_the_only_parameter_is_the_course_key,test_the_v1_read_is_deprecated_and_left_out_of_the_sdk);SettingsSchemaTest(test_the_read_is_published_for_the_sdk,test_the_parameters,test_the_examples_are_valid_bodies,test_the_v1_read_is_deprecated_and_left_out_of_the_sdk)CourseRerunDefaultsViewSetandCourseSettingsViewSet(StandardizedErrorMixinfirst, thenviewsets.ViewSet) overv2/rerun_defaults_service.pyandv2/course_settings_service.py. Plain ViewSets, because the data comes from the course block in the modulestore, site settings and feature modules, not from rows of one model. Deviation: mounted withpath(), not a routerRerunDefaultsRoutingTest.test_the_address;SettingsRoutingTest.test_the_addressStandardizedErrorMixinfirst. 401authn, 403authz, 404not-found(unknown, version-only or library course; a course whose overview exists but which the modulestore returns nothing for, logged at INFO). Settings: 400validationfor an unknownview, witherrorskeyed byview. Gaps pinned by tests that flip when fixed: 405 and 406 are typedinternal(edx-drf-extensions 10.9.0 does not catalogue them); a malformed or slash-form key gets the CMS HTML 404 at routing. The publishedErrorResponsecomponent's description is the library's own text, which names ADR 0029; changing it is a library changeSettingsReadTest.test_an_unknown_view_is_refused;RerunDefaultsResponseDifferencesTestandSettingsResponseDifferencesTest(test_a_course_the_modulestore_returns_nothing_for);RerunDefaultsRoutingTestandSettingsRoutingTest(test_other_methods_are_not_allowed,test_unsupported_accept_is_refused_with_406,test_an_unparsable_or_slash_form_key_is_an_html_404)PolicyCacheControlrow, which may insert it. That write belongs to the permission library, and v2 does exactly what the v1 read does thereRerunDefaultsReadSideEffects{LegacyPermissions,Authz}Test.test_reads_have_no_side_effects;SettingsReadSideEffects{,Prerequisites}{LegacyPermissions,Authz}Test(test_reads_have_no_side_effects,test_each_reader_reaches_its_course_listing_branch,test_a_course_without_an_overview_is_read_without_building_one);RerunDefaultsCourseCreatorTestcourses.view_course) have different payloads and gates.content_experiments_enabledmoves one member, not an endpointpossible_pre_requisite_courses(view=fullonly) is unbounded, as on v1SettingsReadTest.test_the_course_list_parameters_change_nothing(JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented at each declaration: an inactive account's session stays authenticated, so the permission decides. Rerun defaults: inactive global staff 200, inactive course instructor 403. Settings with policy permissions off: inactive global staff 200; inactive course staff and inactive holders of the course staff policy role 403 (a role does not count while the account is inactive). With them on: an inactive policy role holder 200 (the policy does not look at activation). All as v1. Bearer is dropped. Same as #39303 and #39305RerunDefaultsAuthenticationTestandSettingsAuthenticationTest(test_a_jwt_caller_…,test_a_session_caller_…,test_an_inactive_session_is_authenticated,test_a_bearer_token_is_refused);SettingsAccessLegacyPermissionsTest.test_a_policy_role_does_not_count_for_an_inactive_account;SettingsAccessAuthzTest.test_an_inactive_session_with_a_role_may_readview=minimalis the default and leaves out the language list, the prerequisite list and the credit requirements;view=fulladds them; an unknownviewis 400. This saves payload, not work (Known gaps and risks). Rerun defaults: six flat scalarsSettingsReadTest(test_the_minimal_view_leaves_out_the_lists_and_credit_requirements,test_the_full_view_adds_the_lists_and_credit_requirements,test_the_full_view_without_prerequisites_or_credit_adds_the_language_list_only,test_the_minimal_view_is_the_default,test_an_unknown_view_is_refused);SettingsSchemaTest.test_the_parametersv2/. v1 frozen apart from docstring-only markers: for both files, the syntax tree with docstrings stripped equals the base.deprecated: truethrough two exactSUPERSEDED_PATHSentries; no runtime header (frozen routes). DEPR: one entry for #39128 covering both, to be posted with this PR; no removal release is namedCmsMarkSupersededPathsTest(test_the_course_rerun_read_is_marked_and_its_successor_is_not,test_the_course_settings_read_is_marked_and_its_successor_is_not);RerunDefaultsSchemaTestandSettingsSchemaTest(test_the_v1_read_is_deprecated_and_left_out_of_the_sdk,test_only_superseded_or_migrated_addresses_are_deprecated)rerun_defaultsandsettingsunder the course (rule 4: what the payload is). Sharedcourse_keyconverter, trailing slash, version-free URL namescourse_rerun_defaultsandcourse_settings; v1's names kept. Two risks are stated under Known gaps and risks: the verb heuristic flagsrerun, andsettingsis a screen-like nameRerunDefaultsRoutingTestandSettingsRoutingTest(test_the_address,test_the_v1_address_is_unchanged,test_v1_still_takes_a_slash_separated_key_that_v2_does_not,test_the_url_names_are_unique_in_the_namespace,test_the_address_without_its_trailing_slash_redirects_to_it); the urls gate itself was skippedviewchooses members, not records; the course listing's own parameters are no longer readSettingsReadTest.test_the_course_listing_is_given_the_caller_and_no_query_parameters;SettingsParityAllFeatures{LegacyPermissions,Authz}Test.test_courses_being_created_as_reruns_are_not_prerequisitesBackward compatibility
v1 is unchanged.
v1/views/course_rerun.pyandv1/views/settings.pygain docstring text only; for each, the syntax tree with docstrings stripped equals the base. Their routes, views and URL names are untouched, and v1's own tests are not edited.Parity. v2 is compared with v1 through the Django test client, with policy permissions off and on:
view=fullis v1's body pluscontent_experiments_enabled, andview=minimalis that without the three heavy members. This holds for course staff (or the course staff policy role) and for global staff, across these fixtures: prerequisite courses off and a course without credit; prerequisite courses and credit eligibility on for a credit course; credit eligibility off for a registered credit course; a course managed by a publishing service; a verified mode with a deadline; a credit course without a minimum grade; a course whose rerun is still running;content_experiments_enabledequals v1's group configurationsshould_show_experiment_groups, with content experiments off and on;The only declared body differences, verbatim from the two test modules:
The comparison fails on any undeclared difference. It also fails when a declared difference never occurs:
test_every_declared_difference_occurs, which for settings runs on the all-features fixture.Status and request differences from v1. Each is listed in its test module and observed against v1's own answer:
As a table, with the request-shape changes that carry no status difference:
content_experiments_enabledamong them; the heavy ones only withview=fullSettingsReadTest.test_the_minimal_view_leaves_out_the_lists_and_credit_requirementssearch,ordering,active_onlyand other course list parametersSettingsResponseDifferencesTest.test_course_list_parametersstart_date_on_or_after…test_a_malformed_start_date_parameterview=default(or any value butminimal/full)viewnot read)validation…test_an_unknown_view…test_a_course_the_modulestore_returns_nothing_for(both modules)…test_a_bearer_token(both modules)course_does_not_exist)…test_a_slash_separated_course_key(both modules)RerunDefaultsRoutingTestandSettingsRoutingTest(test_the_address_without_its_trailing_slash_redirects_to_it)Query counts, relative to v1. No literal baseline was measured, because no platform environment was available. Instead, in the same test, each read runs no more SQL queries and no more Mongo reads than v1's read, and makes no Mongo writes, with policy permissions off and on. Rerun defaults are measured for global staff. Settings is measured for both views, with prerequisite courses on and a credit course, for course staff and for global staff.
Schema. Two new operations, and
deprecated: trueon the two v1 operations.docs/cms-openapi.yamlis not edited; thegenerate_openapi_schemasworkflow regenerates it.In-place edits
cms/djangoapps/contentstore/rest_api/v1/views/course_rerun.pyget)cms/djangoapps/contentstore/rest_api/v1/views/settings.pyget;getalso says whatview=fulladds and that the prerequisite list ignores the course list's parameters)cms/djangoapps/contentstore/rest_api/v2/authoring_urls.pyRerunDefaultsRoutingTest,SettingsRoutingTestcms/lib/spectacular.py,cms/lib/tests/test_spectacular.pySUPERSEDED_PATHSentries; the exact-set test updated and a test added for each pathCmsMarkSupersededPathsTestviews/permissions.pyis not edited by this PR:IsExistingCoursecomes from #39303,CanViewCoursefrom #39229, andIsAdminUserfrom DRF.New files inside the pre-existing
v2/:rerun_defaults_service.py,course_settings_service.py,serializers/rerun_defaults.py,serializers/course_settings.py,views/rerun_defaults.py,views/course_settings.py,views/tests/test_rerun_defaults.py,views/tests/test_course_settings.py. The production modules import nothing fromrest_api/v0/orrest_api/v1/.Test-only imports from v1. Both test modules import these shared helpers from
rest_api/v1/views/tests/helpers.py(added by #39303):FIRST_LOOKUP_SIGNALS,FIRST_LOOKUP_WRITES,UNKNOWN_COURSE_KEY,VERSION_ONLY_KEY,api_client,assert_matches_published_schema,assert_refused,changed_paths,measure,published_schema,record_effects,undeclared_paths. They are the area's shared test helpers (clients, the side-effect recorder, the parity diff, the cost measure, the schema check); importing them keeps one copy rather than a second one inv2/.PASSWORDandlogged_in_clientcome from the existingv2/views/tests/helpers.py.Error-format decision
Versioned. v1 keeps its
developer_message/error_codebodies unchanged. v2 uses the envelope. frontend-app-authoring calls both v1 endpoints: rerun defaults fromsrc/generic/data/api.ts, course settings fromsrc/data/api.tsandsrc/content-tags-drawer/data/api.ts. The settings payload change (heavy members opt-in, one member added) already forces a new version, and the error format rides with it.Deprecations
Both v1 operations are superseded by the v2 operations in the Summary. 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; one entry covering both is posted there with the PR. Window per #39128: removed one named release after the release this ships in.Migration notes:
GET /api/authoring/v2/courses/{course_key}/rerun_defaults/. Same six fields and values; still global staff only.GET /api/authoring/v2/courses/{course_key}/settings/?view=fullfor v1's body pluscontent_experiments_enabled. Withoutview=full(or withview=minimal),language_options,possible_pre_requisite_coursesandcredit_requirementsare left out.viewtakes onlyminimalorfull; any other value is a 400.search,ordering,active_onlyor the course listing's other parameters, and a malformedstart_date_on_or_afteris ignored.content_experiments_enabledhere instead ofshould_show_experiment_groups, andmfe_proctored_exam_settings_urlhere; the group configurations successor (PR G) does not return them.Authorization
@view_auth_classes(is_authenticated=True)(both)IsAuthenticatedtest_anonymous_caller_is_refused_with_401(both endpoints, both permission modes, both views);test_anonymous_caller_of_an_unknown_course_is_refused_with_401@verify_course_exists()(both)IsExistingCourse; settings:CanViewCourse's existence check (both 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);test_global_staff_learn_that_a_course_does_not_existGlobalStaff().has_user(request.user), elsepermission_denied, inline ingetIsAdminUser(DRF;user.is_staff, the same test)user_has_course_permission(user, courses.view_course, course_key, READ), elsepermission_denied, inline ingetCanViewCourse(#39229), same callOrder: 401, then 404 for a missing course, then 403. Allowed on rerun defaults: global staff only (also when the account is inactive). Allowed on settings: with policy permissions off, course staff and instructors, org staff and org instructors, global staff, and active holders of the course staff policy role; with them on, the course auditor, editor, staff and admin roles and global staff.
Superusers. Saving a superuser sets
is_staff: thepre_savereceivergrant_staff_access_to_superusers(common/djangoapps/student/signals/receivers.py) makes every saved superuser global staff, so superusers are admitted as staff on both endpoints. A superuser whose staff status was cleared without a save is a different case. Rerun defaults refuse it with policy permissions off and on, because both addresses check staff status alone. Settings refuse it with policy permissions off, because Studio read access comes from staff status or a role, and admit it with them on, because the policy admits superusers. v1 answers the same in every case (test_a_saved_superuser_has_staff_status_and_may_read,test_a_superuser_without_staff_status_is_refused,test_a_superuser_without_staff_status_may_read_without_a_role). #39303 and #39305 give the policy engine as the reason superusers are admitted; that is the wrong cause.Corrections to the area plan, found in review and checked against the code. The tests follow the platform's behaviour, which v2 keeps:
RerunDefaultsAuthenticationTest.test_an_inactive_session_is_authenticated,SettingsAuthenticationTest.test_an_inactive_session_is_authenticated,SettingsAccessLegacyPermissionsTest.test_a_policy_role_does_not_count_for_an_inactive_accountandSettingsAccessAuthzTest.test_an_inactive_session_with_a_role_may_read.Known gaps and risks
view=minimalsaves payload, not work. The shared helpercontentstore.utils.get_course_settings(frozen, reused unedited) builds the language list, the prerequisite list and the credit requirements whatever the view. The prerequisite list is built by listing every course the caller may view, while prerequisite courses are on. v2 removes those members afterwards, so the default read makes the same lookups asview=full; the query-count tests bound each view by v1's read, not the default by the full view. A helper that skips the lists is a future-version item. The service says so in a comment.settings/is named after a screen. ADR 0038 listscourse_settingsamong screen names used as resources and points such data atcourses/{course_key}/details/and its siblings. This resource is not folded into v3details/because its gate differs:courses.view_coursehere,courses.view_schedule_and_detailsthere, so it is a different resource. Reviewers may ask for a narrower noun; renaming before merge is cheap./api/authoring/v2/mount disagree. This PR's components are suffixed (CourseSettings.v2,CourseRerunDefaults.v2,PrerequisiteCourse.v2,CreditRequirement.v2); feat: serve the course outline from authoring v2 #39229's on the same mount are not (CourseOutlineand its parts). The suffix is needed here: v1'sCourseSettingsSerializerhas noref_nameand publishes asCourseSettings, so an unsuffixed v2 component would collide with it. The mount mixes the two conventions until one side changes.CourseSettings.v2.course_display_nameandCourseRerunDefaults.v2.display_nameare nullable; v1 declared them non-null but returns null for a course without a name (tested on both).PrerequisiteCourse.v2makesdisplay_name,lms_link,rerun_linkandurlnullable; v1'sCourseCommonSerializerdeclares all of them non-null.lms_link_for_about_pageis a plain string; v1 declared a URL format, but the value can be the course's social sharing URL, its marketing URL, or a filter's output.mfe_proctored_exam_settings_urlis a non-null string, empty when the page is not served by the authoring app; v1 declared it nullable.credit_requirementsis typed by namespace and requirement (v1 declared a bare object), and eachlanguage_optionsitem is a[code, name]pair (v1 declared lists of any length).rerun_defaults, because its first word,rerun, is in its verb list. Here the word is a noun (the defaults for a rerun), so the WARN is a false positive, as the plan recorded. The urls gate was skipped, so the WARN is expected on its first run rather than observed.courses.view_group_configurationswithoutcourses.view_course, policy permissions on) cannot readcontent_experiments_enabled. The plan found no shipped role with that combination.Gate report
Run on the host against
a1b1c074a2(the merge of #39303 into #39229 that this branch stacks on). No platform environment was available, so four gates were skipped. The test modules have not been run locally, and CI is their first run.v1/views/course_rerun.pyandv1/views/settings.pydocstring markers,v2/authoring_urls.py), 8 new files in the existing v2 directories, 2 other files (cms/lib/spectacular.pyand its test), nothing protected touched (23 shared legacy modules protected)== []comparisons), the same message #39303 carries with its CI pylint passingFollow-ups
openedx-platform-sdk; the next openedx-platform-sdk regeneration gains them.src/generic/data/api.ts) can move torerun_defaults/unchanged. The settings callers (src/data/api.ts,src/content-tags-drawer/data/api.ts) can move tosettings/, requestingview=fullwhere they use the language list, prerequisites or credit requirements. The group configurations page needs this PR before it moves to PR G's listing, because it readscontent_experiments_enabledandmfe_proctored_exam_settings_urlfrom here.view=minimal, and a bounded or referenced prerequisite list./api/authoring/v2/: agree one component-name convention (suffixed or not) for the mount, and settle thesettingsnoun in review.MethodNotAllowedandNotAcceptable.ErrorResponsecomponent's description isErrorResponseSerializer's docstring, which names ADR 0029 and carries a usage example; it should describe the error body instead. Issue to be filed./api/that match no route, instead of the CMS HTML page. Issue to be filed.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./api/authoring/v2/: v2 here 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 on the same mount. One policy for new versions of such endpoints should be agreed.masterand the merge commit drops out.v2/authoring_urls.py,cms/lib/spectacular.pyand its test. The conflicts are additive, resolved by rebasing in merge order.