Skip to content

feat: serve course rerun defaults and settings from authoring v2 - #39307

Draft
taimoor-ahmed-1 wants to merge 17 commits into
openedx:masterfrom
edly-io:feat/standardize-course-settings-api
Draft

taimoor-ahmed-1 wants to merge 17 commits into
openedx:masterfrom
edly-io:feat/standardize-course-settings-api

Conversation

@taimoor-ahmed-1

@taimoor-ahmed-1 taimoor-ahmed-1 commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #39303 and #39229. This branch starts from a merge of #39303 (advanced settings, head 9b4d60ea0d, which adds IsExistingCourse and the shared test helpers) into #39229 (course outline v2, head 53ea5f44a1, which adds CanViewCourse). The PR targets master, so until both merge it also carries their commits and that merge commit; only the commits after that merge belong to this PR. Once #39303 and #39229 merge, this branch is rebased onto master. The merge commit's conflict resolution (in views/permissions.py, cms/lib/spectacular.py and its test) only keeps both sides' additions, so it leaves nothing of its own to carry.

Summary

Part of #39059

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:

v1 (unchanged, now deprecated: true) v2 URL name (authoring_v2:)
GET /api/contentstore/v1/course_rerun/{course_id} GET /api/authoring/v2/courses/{course_key}/rerun_defaults/ course_rerun_defaults
GET /api/contentstore/v1/course_settings/{course_id} GET /api/authoring/v2/courses/{course_key}/settings/ (?view=minimal, the default, or ?view=full) course_settings

It applies ADRs 0025–0038 and OEP-69.

Rerun defaults. The values Studio's rerun page starts from: the course's org, number, run and display_name, the site's allow_unicode_course_id, and the caller's course_creator_status. These are the same six fields as v1, with the same values. Only global staff may read them. v2 uses DRF's IsAdminUser, which checks user.is_staff, as v1's GlobalStaff().has_user does; a test compares the two for every combination of is_staff, is_superuser and is_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, with ENABLE_CREATOR_GROUP on, the lookup records a course creator request for a non-staff user, which is a write. course_creator_status is therefore always granted, 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=full adds language_options, possible_pre_requisite_courses (while prerequisite courses are on) and credit_requirements (for a credit course while credit eligibility is on). Access goes through CanViewCourse from #39229: the course must exist (404), then the caller needs user_has_course_permission(courses.view_course, READ), the same call v1 makes inline. Two content changes:

  • content_experiments_enabled is added. It says whether content experiments apply to the course (split_test installed and in the course's advanced modules). This is the value v1's group configurations listing reports as should_show_experiment_groups. That listing's successor (PR G) drops the flag, and also drops mfe_proctored_exam_settings_url, which v1 course settings already returns and v2 keeps. Both fields' help texts tell callers to read them here.
  • The prerequisite list no longer depends on the request's query string. v1 passes its own request to Studio's course listing, which reads search, ordering, active_only and its other parameters. Sent to v1, those parameters narrowed or reordered possible_pre_requisite_courses, and a malformed start_date_on_or_after made 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):

  • course settings returns its three heavy members only on request (view=full), gains content_experiments_enabled, and refuses an unknown view (400);
  • the prerequisite list ignores the course listing's query parameters;
  • errors use the standard envelope, and a course the modulestore cannot load is a 404, not a 500;
  • Bearer tokens are refused;
  • addresses gain a trailing slash and a checked course key.

Changes

Commits (one per concern):

Commit Endpoint(s) Legacy behaviour kept New behaviour
feat: serve course rerun defaults from authoring v2 GET …/v2/courses/{course_key}/rerun_defaults/ (new) v1 route, view and URL name unchanged View, service, serializer, route and tests. The service reuses the legacy helper contentstore.utils.get_course_rerun_context, which is not edited. Permissions: DRF's IsAdminUser and IsExistingCourse from #39303
feat: serve course settings from authoring v2 GET …/v2/courses/{course_key}/settings/ (new) v1 route, view and URL name unchanged View, service, serializers, route and tests. The service reuses contentstore.utils.get_course_settings, contentstore.views.course.are_content_experiments_enabled and certificates.api.can_show_certificate_available_date_field, none of them edited. Permission: CanViewCourse from #39229
docs: mark the v1 course rerun and settings endpoints deprecated in favour of authoring v2 the two v1 operations Routes, views and responses unchanged Added docstring notes naming each successor; reverted by the next commit. (deprecated: true in the schema comes from the two exact SUPERSEDED_PATHS entries, each with a test, added in the feat commits so every commit is green.)
docs: remove the deprecation notes from the v1 settings and rerun docstrings the two v1 operations v1 files back to their base content A view docstring is the published description of the v1 operation, so the notes changed the legacy schema text and the OPTIONS body; SUPERSEDED_PATHS and the #39128 entry carry the deprecation
fix: satisfy the import contract and typing checks for course settings v2 — — ignore_imports entry for the service's lms import; the view parameter description is a shared plain string (mypy); two help texts no longer refer to the unmerged group configurations successor
test: read with a freshly loaded user when recording side effects — — The side-effects tests authenticate a new user instance per measured read (role lookups are cached on the instance); get_user_model(); FIRST_READ_EFFECTS annotated

ADRs applied

  • 0025 serializers — separate output serializers (CourseRerunDefaults.v2, CourseSettings.v2, PrerequisiteCourse.v2, CreditRequirement.v2), help_text on every field, explicit .v2 component names, a query serializer for view; read-only, so no request body serializer
  • 0026 permissions — rerun defaults: IsAuthenticated, IsExistingCourse, IsAdminUser (equivalent to v1's GlobalStaff().has_user); settings: IsAuthenticated, CanViewCourse (v1's inline call); deny matrix with policy permissions off and on
  • 0027 schema / docs — @extend_schema on both with summary, description, parameters, examples and every 4xx as ErrorResponse; both tagged openedx-platform-sdk
  • 0028 viewsets — applied except router registration: two ViewSets over service modules, mounted with path(...) like every route in v2/authoring_urls.py, because a router's regex routes cannot use the shared path converters
  • 0029 errors — partial: the envelope on every response these views produce; 405 and 406 are typed internal, and a malformed course key or an address no route matches gets the CMS HTML 404 (library and routing gaps, pinned by tests); the published ErrorResponse component's text comes from edx-drf-extensions
  • 0030 GET idempotence — met for this code (no write, signal or event on GET or HEAD, on either view), with one inherited write outside it: openedx-authz may insert its PolicyCacheControl row on the first lookup against a database that lacks it, exactly as the v1 read does there
  • 0031 merged endpoints — n/a: nothing merged; rerun defaults and settings stay separate resources (different payloads, different gates), and content_experiments_enabled is one member moved from the group configurations listing, not a merged endpoint
  • 0032 pagination — n/a: both resources are one object per course; the full view's prerequisite list stays unbounded, as on v1 (see Known gaps and risks)
  • 0033 filtering — n/a: no filter or sort parameter; the course listing's parameters that leaked into v1's prerequisite list are no longer read
  • 0034 authentication — (JwtAuthentication, SessionAuthenticationAllowInactiveUser) declared with its reason, as ADR 0034 requires for an endpoint that used SessionAuthenticationAllowInactiveUser; Bearer dropped
  • 0035 MFE config — n/a: per-course authoring data behind authentication; the site switches settings carries are part of what the settings pages need about the course, not front-end configuration
  • 0036 nested JSON — settings: the language list, prerequisite list and credit requirements are opt-in with view=full, so they are never in the default payload (payload only: see Known gaps and risks); rerun defaults are six flat scalars
  • 0037 versioning — partial: new v2 modules, v1 not edited, deprecated: true in the schema through SUPERSEDED_PATHS; the DEPR entry on #39128 is posted; the removal release is [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128's window, not a named release
  • 0038 URL structure — courses/{course_key}/rerun_defaults/ and courses/{course_key}/settings/; shared converter, trailing slash, version-free URL names; the v1 names kept. One stated departure: the ADR lists course_settings among screen names used as resources. settings/ is kept on purpose: the payload is the course's settings (dates, enrolment, licence and so on) and settings is a noun for it; the Studio screen name course_settings is not used, and folding into v3 details/ would change access (different gate)
  • OEP-66 queryset scoping — n/a as a mechanism: neither view owns a get_queryset(); the prerequisite list comes from Studio's course listing for the caller, as on v1; the layering is shown in the matrix
  • OEP-69 conventions — the decisions above as recorded, with the 0028, 0029, 0030 and 0037 departures stated; no governance identifiers in code or schema text

Compliance matrix

Decision Status How it is met, or the concrete reason it is excluded Evidence (test / gate)
0025 met CourseRerunDefaultsSerializer (CourseRerunDefaults.v2: v1's six fields, display_name nullable). CourseSettingsSerializer (CourseSettings.v2: 20 required members, plus credit_requirements, language_options and possible_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. CourseSettingsQuerySerializer validates view. Explicit .v2 ref_names: v1's serializers have none, so they publish as CourseRerun and CourseSettings, and an unsuffixed v2 CourseSettings would 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 PASS
0026 met Every v1 check has a v2 home (Authorization below). Order 401, 404, 403. Rerun defaults: IsAuthenticated, IsExistingCourse, IsAdminUser. Settings: IsAuthenticated, CanViewCourse (existence, then courses.view_course READ). Each viewset has one permission tuple, so retrieve, OPTIONS and unrouted methods all run the same checks RerunDefaultsAccess{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_lookup
0027 met Operation IDs v2_courses_rerun_defaults_retrieve, v2_courses_settings_retrieve. course_key is a path parameter with the converter's pattern; settings adds view (query, enum minimal/full, default minimal). Examples RerunDefaults, CourseSettings and CourseSettingsWithLists validate against the published components. Every 4xx is ErrorResponse (settings 400, 401, 403, 404; rerun defaults 401, 403, 404). Both tagged openedx-platform-sdk; the deprecated v1 operations lose the tag through untag_deprecated_operations RerunDefaultsSchemaTest (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)
0028 applied except router registration CourseRerunDefaultsViewSet and CourseSettingsViewSet (StandardizedErrorMixin first, then viewsets.ViewSet) over v2/rerun_defaults_service.py and v2/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 with path(), not a router RerunDefaultsRoutingTest.test_the_address; SettingsRoutingTest.test_the_address
0029 partial StandardizedErrorMixin first. 401 authn, 403 authz, 404 not-found (unknown, version-only or library course; a course whose overview exists but which the modulestore returns nothing for, logged at INFO). Settings: 400 validation for an unknown view, with errors keyed by view. Gaps pinned by tests that flip when fixed: 405 and 406 are typed internal (edx-drf-extensions 10.9.0 does not catalogue them); a malformed or slash-form key gets the CMS HTML 404 at routing. The published ErrorResponse component's description is the library's own text, which names ADR 0029; changing it is a library change SettingsReadTest.test_an_unknown_view_is_refused; RerunDefaultsResponseDifferencesTest and SettingsResponseDifferencesTest (test_a_course_the_modulestore_returns_nothing_for); RerunDefaultsRoutingTest and SettingsRoutingTest (test_other_methods_are_not_allowed, test_unsupported_accept_is_refused_with_406, test_an_unparsable_or_slash_form_key_is_an_html_404)
0030 met for this code; one inherited write GET and HEAD of both endpoints, both settings views, with policy permissions off and on, and for settings with prerequisite courses off and on, write nothing. No read writes to SQL or Mongo, stores through the modulestore, or sends a Django signal or Open edX event. Settings is read by course staff (or a policy role holder), global staff, and a reader whose role records send the course listing to its every-course fallback. The fixture covers every lookup: a credit course with a verified mode, and another course with a rerun still running. Reading a course that has no overview does not build one. Rerun defaults never reach the course-creator lookup for a non-staff caller. The one exception is the first read on a database without openedx-authz's PolicyCacheControl row, which may insert it. That write belongs to the permission library, and v2 does exactly what the v1 read does there RerunDefaultsReadSideEffects{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); RerunDefaultsCourseCreatorTest
0031 excluded Nothing merged. Rerun defaults (global staff) and settings (courses.view_course) have different payloads and gates. content_experiments_enabled moves one member, not an endpoint —
0032 excluded One object per course. possible_pre_requisite_courses (view=full only) is unbounded, as on v1 —
0033 excluded No filter or sort. The course listing's parameters are no longer read SettingsReadTest.test_the_course_list_parameters_change_nothing
0034 met with declared deviation (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 #39305 RerunDefaultsAuthenticationTest and SettingsAuthenticationTest (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_read
0035 excluded Per-course authoring data behind authentication; not front-end configuration —
0036 met (settings); n/a (rerun defaults) view=minimal is the default and leaves out the language list, the prerequisite list and the credit requirements; view=full adds them; an unknown view is 400. This saves payload, not work (Known gaps and risks). Rerun defaults: six flat scalars SettingsReadTest (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_parameters
0037 partial New modules in v2/. v1 not edited: both files are identical to the base. deprecated: true through two exact SUPERSEDED_PATHS entries; no runtime header (frozen routes). DEPR: one entry on #39128 covering both; the removal follows #39128's window versions gate; CmsMarkSupersededPathsTest (test_the_course_rerun_read_is_marked_and_its_successor_is_not, test_the_course_settings_read_is_marked_and_its_successor_is_not); RerunDefaultsSchemaTest and SettingsSchemaTest (test_the_v1_read_is_deprecated_and_left_out_of_the_sdk, test_only_superseded_or_migrated_addresses_are_deprecated)
0038 met with one stated departure rerun_defaults and settings under the course (rule 4: what the payload is). Shared course_key converter, trailing slash, version-free URL names course_rerun_defaults and course_settings; v1's names kept. Departure: settings is on the ADR's screen-name list; kept for the reason given in the checklist above and under Known gaps and risks. The verb heuristic also flags rerun RerunDefaultsRoutingTest and SettingsRoutingTest (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 skipped
OEP-66 mechanism excluded, layering met (1) Endpoint access: the permission classes. (2) Record visibility: one course. The prerequisite list holds the courses the caller may view in Studio, from Studio's course listing for the caller, without this course and without courses still being created as reruns, as on v1. (3) User selection: view chooses members, not records; the course listing's own parameters are no longer read access tests above; SettingsReadTest.test_the_course_listing_is_given_the_caller_and_no_query_parameters; SettingsParityAllFeatures{LegacyPermissions,Authz}Test.test_courses_being_created_as_reruns_are_not_prerequisites
OEP-69 met The decisions above, with the departures stated in their rows. No ADR/OEP/PR identifiers or narration in code or schema text hygiene gate PASS (0 FAIL, 0 WARN)

Backward compatibility

  • v1 is unchanged. v1/views/course_rerun.py and v1/views/settings.py are not in the diff. 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:

    • rerun defaults: global staff get an identical body, with the same members in the same order;
    • settings: view=full is v1's body plus content_experiments_enabled, and view=minimal is 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_enabled equals v1's group configurations should_show_experiment_groups, with content experiments off and on;
    • 401, 403 and 404 differ only in the error body, on both views.

    The only declared body differences, verbatim from the two test modules:

    # test_rerun_defaults.py
    INTENTIONAL_DIFFERENCES = [
        ("type", "errors use the standardized envelope"),
        ("title", "errors use the standardized envelope"),
        ("status", "errors use the standardized envelope"),
        ("detail", "errors use the standardized envelope"),
        ("developer_message", "the v1 error body carries developer_message; the envelope does not"),
        ("error_code", "the v1 not-found body carries error_code; the envelope does not"),
    ]
    
    # test_course_settings.py
    INTENTIONAL_DIFFERENCES = [
        ("content_experiments_enabled", "added: whether content experiments apply to the course, which the v1 "
                                        "group configurations read reports as should_show_experiment_groups"),
        ("language_options", "returned only with view=full"),
        ("possible_pre_requisite_courses", "returned only with view=full"),
        ("credit_requirements", "returned only with view=full"),
        ("type", "errors use the standardized envelope"),
        ("title", "errors use the standardized envelope"),
        ("status", "errors use the standardized envelope"),
        ("detail", "errors use the standardized envelope"),
        ("developer_message", "the v1 error body carries developer_message; the envelope does not"),
        ("error_code", "the v1 not-found body carries error_code; the envelope does not"),
    ]

    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:

    # test_rerun_defaults.py
    RESPONSE_DIFFERENCES = [
        ("read of a course the modulestore returns nothing for", "500", "404",
         "test_a_course_the_modulestore_returns_nothing_for"),
        ("read with a Bearer token", "200", "401", "test_a_bearer_token"),
        ("slash-separated course key", "404 JSON", "404 HTML", "test_a_slash_separated_course_key"),
    ]
    
    # test_course_settings.py
    RESPONSE_DIFFERENCES = [
        ("read with course list parameters such as search", "prerequisites narrowed or reordered",
         "prerequisites unchanged", "test_course_list_parameters"),
        ("read with a malformed start_date_on_or_after", "400", "200", "test_a_malformed_start_date_parameter"),
        ("read with view=default", "200", "400", "test_an_unknown_view"),
        ("read of a course the modulestore returns nothing for", "500", "404",
         "test_a_course_the_modulestore_returns_nothing_for"),
        ("read with a Bearer token", "200", "401", "test_a_bearer_token"),
        ("slash-separated course key", "404 JSON", "404 HTML", "test_a_slash_separated_course_key"),
    ]

    As a table, with the request-shape changes that carry no status difference:

    Request v1 v2 Test
    settings, default read every member, the three heavy ones included the 20 members, content_experiments_enabled among them; the heavy ones only with view=full SettingsReadTest.test_the_minimal_view_leaves_out_the_lists_and_credit_requirements
    settings with search, ordering, active_only and other course list parameters prerequisites narrowed or reordered prerequisites unchanged SettingsResponseDifferencesTest.test_course_list_parameters
    settings with a malformed start_date_on_or_after 400 200 (parameter not read) …test_a_malformed_start_date_parameter
    settings with view=default (or any value but minimal/full) 200 (view not read) 400 validation …test_an_unknown_view
    a course whose overview exists but which the modulestore returns nothing for (both endpoints) 500 404, logged at INFO …test_a_course_the_modulestore_returns_nothing_for (both modules)
    Bearer token (both endpoints) 200 401 …test_a_bearer_token (both modules)
    slash-form course key (both endpoints) 404 JSON (course_does_not_exist) 404 HTML (no route) …test_a_slash_separated_course_key (both modules)
    address without the trailing slash served 301 to the slashed address RerunDefaultsRoutingTest and SettingsRoutingTest (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: true on the two v1 operations. docs/cms-openapi.yaml is not edited; the generate_openapi_schemas workflow regenerates it.

In-place edits

File Category Evidence
pyproject.toml one ignore_imports entry for the v2 settings service's lms.djangoapps.certificates.api import, next to the v1 view's entry for the same import import-linter
cms/djangoapps/contentstore/rest_api/v2/authoring_urls.py strictly opt-in addition: two imports, two routes no line removed; RerunDefaultsRoutingTest, SettingsRoutingTest
cms/lib/spectacular.py, cms/lib/tests/test_spectacular.py two exact SUPERSEDED_PATHS entries; the exact-set test updated and a test added for each path CmsMarkSupersededPathsTest

views/permissions.py is not edited by this PR: IsExistingCourse comes from #39303, CanViewCourse from #39229, and IsAdminUser from 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 from rest_api/v0/ or rest_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 in v2/. PASSWORD and logged_in_client come from the existing v2/views/tests/helpers.py.

Error-format decision

Versioned. v1 keeps its developer_message/error_code bodies unchanged. v2 uses the envelope. frontend-app-authoring calls both v1 endpoints: rerun defaults from src/generic/data/api.ts, course settings from src/data/api.ts and src/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: true in the CMS schema; the v1 views are not edited and send no runtime header. Tracked on the FC-0118 umbrella DEPR ticket, #39128: one entry covering both carries the migration notes (callers of v1 settings need ?view=full for language_options, possible_pre_requisite_courses and credit_requirements; the v2 prerequisite list ignores the course-list parameters search, ordering and active_only). Window per #39128: removed one named release after the release this ships in.

Migration notes:

  • Rerun defaults: call GET /api/authoring/v2/courses/{course_key}/rerun_defaults/. Same six fields and values; still global staff only.
  • Course settings: call GET /api/authoring/v2/courses/{course_key}/settings/?view=full for v1's body plus content_experiments_enabled. Without view=full (or with view=minimal), language_options, possible_pre_requisite_courses and credit_requirements are left out. view takes only minimal or full; any other value is a 400.
  • The prerequisite list no longer changes with search, ordering, active_only or the course listing's other parameters, and a malformed start_date_on_or_after is ignored.
  • Group configurations callers: read content_experiments_enabled here instead of should_show_experiment_groups, and mfe_proctored_exam_settings_url here; the group configurations successor (PR G) does not return them.
  • Errors use the standard envelope.
  • Bearer tokens are refused; use JWT or a session.
  • Every address ends with a slash. Slash-separated (deprecated) course keys are not accepted.

Authorization

Legacy check Where it lives in v2 Deny-path tests
@view_auth_classes(is_authenticated=True) (both) IsAuthenticated test_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) rerun defaults: IsExistingCourse; settings: CanViewCourse's existence check (both also 404 for version-only and library keys) unknown course, version-only key and library key → 404, before any role check (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_exist
rerun: GlobalStaff().has_user(request.user), else permission_denied, inline in get IsAdminUser (DRF; user.is_staff, the same test) no role; course staff, instructor and limited staff, org staff and org instructor, on the course and on another course; with policy permissions on, the course admin, staff, editor and auditor roles and a Studio instructor record; a superuser without staff status in both modes; an inactive instructor; a JWT instructor
settings: user_has_course_permission(user, courses.view_course, course_key, READ), else permission_denied, inline in get CanViewCourse (#39229), same call with policy permissions off: no role, limited staff, staff and instructors of another course, a superuser without staff status, inactive course staff, an inactive holder of the course staff policy role; with them on: limited staff, data researcher and beta tester roles, a course admin role on another course, a Studio staff record without a policy role

Order: 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: the pre_save receiver grant_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:

  • Inactive accounts. The plan expected an inactive session to get 200 on every view. Rerun defaults: inactive global staff 200, an inactive course instructor 403. Settings with policy permissions off: inactive global staff 200, while inactive course staff and inactive policy role holders get 403. With them on, an inactive policy role holder gets 200. The tests are 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_account and SettingsAccessAuthzTest.test_an_inactive_session_with_a_role_may_read.
  • Query counts are relative to v1 (no more than v1), not the literal baselines the plan asked for (Backward compatibility).

Known gaps and risks

  • view=minimal saves payload, not work. The shared helper contentstore.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 as view=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.
  • The full view's prerequisite list is unbounded, as on v1, because the settings page fills a dropdown with it. A reference to the courses list is a future-version candidate.
  • settings/ is on ADR 0038's screen-name list. The ADR lists course_settings among screen names used as resources and points such data at courses/{course_key}/details/ and its siblings. The name is kept: the payload is the course's settings and settings is a noun for it, the route does not use the screen name course_settings, and folding into v3 details/ would change access (courses.view_course here, courses.view_schedule_and_details there). 0038 is unticked above for this reason. Renaming before the SDK is generated is still cheap if reviewers want another noun.
  • Component names on the /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 (CourseOutline and its parts). The suffix is needed here: v1's CourseSettingsSerializer has no ref_name and publishes as CourseSettings, so an unsuffixed v2 component would collide with it. The mount mixes the two conventions until one side changes.
  • Nullability corrected in the new components, so typed clients see different declarations than v1's:
    • CourseSettings.v2.course_display_name and CourseRerunDefaults.v2.display_name are nullable; v1 declared them non-null but returns null for a course without a name (tested on both).
    • PrerequisiteCourse.v2 makes display_name, lms_link, rerun_link and url nullable; v1's CourseCommonSerializer declares all of them non-null.
    • lms_link_for_about_page is 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_url is a non-null string, empty when the page is not served by the authoring app; v1 declared it nullable.
    • credit_requirements is typed by namespace and requirement (v1 declared a bare object), and each language_options item is a [code, name] pair (v1 declared lists of any length).
  • Query counts are relative to v1, not literal (Backward compatibility).
  • URL verb heuristic. The urls gate's rule-10 heuristic flags 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.
  • Moving the experiments flag to settings. A caller who may view a course's group configurations but not the course itself (courses.view_group_configurations without courses.view_course, policy permissions on) cannot read content_experiments_enabled. The plan found no shipped role with that combination.

Gate report

Run on the host against 22b86df6cf (the merge of #39303 into #39229 that this branch stacks on; its message was reworded for commitlint after the gates ran, with the same tree and parents). No platform environment was available, so four gates were skipped locally. The test modules run in CI.

Gate Result
versions PASS: 1 ALLOWED (v2/authoring_urls.py), 8 new files in the existing v2 directories, 2 other files (cms/lib/spectacular.py and its test), nothing protected touched (23 shared legacy modules protected)
hygiene PASS (0 FAIL, 0 WARN) over 13 files
ruff clean; no line over 120 characters
pylint (4.1.3, repo pylintrc) only C1803 (== [] comparisons), the same message #39303 carries with its CI pylint passing
supplementary, not a gate: offline harness with fakes 44/44 checks; 15/15 mutants killed (v1 modelled by stand-ins)
schema SKIPPED (no platform environment)
urls SKIPPED (no platform environment)
old-tests SKIPPED (no platform environment)
tests SKIPPED locally (no platform environment); CI runs them. The first CI run failed one test (the side-effects comparison reused one user instance across reads, so the second read skipped the cached role lookup), fixed in 125a748

Follow-ups

  • SDK: both operations carry openedx-platform-sdk; the next openedx-platform-sdk regeneration gains them.
  • frontend-app-authoring: the rerun page (src/generic/data/api.ts) can move to rerun_defaults/ unchanged. The settings callers (src/data/api.ts, src/content-tags-drawer/data/api.ts) can move to settings/, requesting view=full where 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 reads content_experiments_enabled and mfe_proctored_exam_settings_url from here.
  • Future version of course settings: a settings helper that does not build the lists for view=minimal, and a bounded or referenced prerequisite list.
  • Naming on /api/authoring/v2/: agree one component-name convention (suffixed or not) for the mount, and settle the settings noun in review.
  • edx-drf-extensions:
    • Catalogue MethodNotAllowed and NotAcceptable.
    • The shared ErrorResponse component's description is ErrorResponseSerializer's docstring, which names ADR 0029 and carries a usage example; it should describe the error body instead. Issue to be filed.
  • Platform: a JSON 404 for addresses under /api/ that match no route, instead of the CMS HTML page. Issue to be filed.
  • openedx-authz 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.
  • Authentication across /api/authoring/v2/: v2 here and the feat: apply FC-0118 ADRs to course content APIs #39157 v2 views keep SessionAuthenticationAllowInactiveUser; 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.
  • feat: serve course advanced settings from authoring v1 #39303 and feat: serve course custom pages and tabs from authoring v1 #39305 explain superuser admission with the wrong cause, in their descriptions and test docstrings; to be corrected in those PRs.
  • Rebase: once feat: serve course advanced settings from authoring v1 #39303 and feat: serve the course outline from authoring v2 #39229 merge, this branch is rebased onto master and the merge commit drops out.
  • Sibling PRs G (group configurations) and H (course team) edit the same shared files: v2/authoring_urls.py, cms/lib/spectacular.py and its test. The conflicts are additive, resolved by rebasing in merge order.

Taimoor Ahmed and others added 9 commits October 21, 2026 16:32
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.
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 25, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @taimoor-ahmed-1!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform-oncall.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If 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 PR

Your 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@Faraz32123 Faraz32123 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally (this PR's 3 commits): 284 passed, 1 failed, same as CI. Schema diff clean. Rerun-defaults and settings permissions, auth, prerequisite list and declared differences match v1.

CI is red on a few things (inline):

  • import-linter (lms import),
  • pylint E5142 imported-auth-user in both new test modules,
  • mypy at views/course_settings.py:32, plus var-annotated on FIRST_READ_EFFECTS,
  • the side-effects test,
  • commitlint on the merge commit a1b1c07 (goes away with the rebase).

Also:

  • Deprecation notes in the v1 docstrings (inline); OEP-69 tick and 0037 row aren't accurate because of them.
  • PR body is stale: the DEPR entry is already posted on #39128, and the gate table still says tests never ran.

All findings were compared with the previous version's behaviour; anything inherited from it is marked as such.

from cms.djangoapps.contentstore.utils import get_course_settings
from cms.djangoapps.contentstore.views.course import are_content_experiments_enabled
from cms.djangoapps.contentstore.views.permissions import COURSE_NOT_FOUND_MESSAGE
from lms.djangoapps.certificates.api import can_show_certificate_available_date_field

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaks the "lms and cms are independent" import contract. v1 has the same import but is in ignore_imports in pyproject.toml; this module needs an entry next to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added cms.djangoapps.contentstore.rest_api.v2.course_settings_service -> lms.djangoapps.certificates.api to ignore_imports, right under the v1 entry for the same import (92fddeb).


_VIEW_PARAMETER = OpenApiParameter(
name="view",
description=CourseSettingsQuerySerializer().fields["view"].help_text,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy arg-type: help_text is str | None. A literal string (or a str(...) after a None check) fixes it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter description is now a plain string, VIEW_HELP_TEXT, shared by the serializer field and the OpenApiParameter (92fddeb). FIRST_READ_EFFECTS is annotated in both test modules (125a748).

import ddt
import pytest
from django.conf import settings
from django.contrib.auth.models import User

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pylint E5142 imported-auth-user (same in test_rerun_defaults.py). get_user_model() should do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both test modules import the user model through get_user_model() now (125a748).

Comment on lines +965 to +966
client = api_client(reader)
legacy = self._read_on_a_fresh_database(client, legacy_url(self.course.id), "get", {})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reuses one reader instance for the v1 and v2 reads. The legacy role lookup is cached on the user, so the v2 read never reaches the policy row and records no insert. With a fresh user per read both versions insert the row, so it's the test, not v2. Re-fetching the user inside _read_on_a_fresh_database should fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right: the role cache lives on the user instance, so the v2 read made with the instance that served the v1 read never reached the insert. _read_on_a_fresh_database now builds its client from a freshly loaded user for every measured read, and so does the follow-up read. test_rerun_defaults.py had the same pattern and got the same fix (125a748).

Comment on lines +27 to +28
Deprecated: use ``GET /api/authoring/v2/courses/{course_key}/settings/``
instead. See https://github.com/openedx/openedx-platform/issues/39128.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we drop this deprecation note? We agreed on #39100 not to add DEPR notes in docstrings. This docstring is the published description of the v1 operation, so it changes the legacy schema text and puts an issue URL into it (which also makes the OEP-69 tick untrue). SUPERSEDED_PATHS plus the #39128 entry already carry the deprecation. Same for course_rerun.py (and the extra v2 paragraph in get).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped. Both v1 files are back to their base content (f59f25f); SUPERSEDED_PATHS and the #39128 entry carry the deprecation. The migration advice that was in the get note (view=full for the three heavy fields; the prerequisite list ignores the course-list parameters) lives in the #39128 entry and the description.

name="course_rerun_defaults",
),
path(
"courses/<course_key:course_key>/settings/",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ADR 0038 calls out course_settings as a screen name used as a resource. Since renaming after the SDK is generated removes an operationId, now is the cheap time to pick a noun, or untick 0038 with the reason.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping settings/, and I've unticked 0038 in the description with the reason: the payload is the course's settings (dates, enrolment, licence and so on), and settings is a noun for it; the ADR's objection is to the Studio screen name course_settings, which this route doesn't use. Folding it into v3 details/ would change access, since that resource has a different gate. If reviewers want another noun before the SDK is generated, renaming is still cheap.

Comment on lines +72 to +73
"should_show_experiment_groups flag of the course's group configurations listing, which "
"its successor does not return.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This points at the group configurations successor, which is #39308 and not merged yet, so the published text refers to an endpoint that doesn't exist. Maybe keep the help text about this field only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both help texts (content_experiments_enabled and mfe_proctored_exam_settings_url) now describe the field only (92fddeb). The "read this instead of …" guidance belongs with #39308, where those fields are dropped.

Taimoor Ahmed added 8 commits October 31, 2026 21:32
… into feat/standardize-course-settings-api for IsExistingCourse
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.
…strings

A view docstring is the published description of the v1 operation, so
the notes changed the legacy schema text and the OPTIONS body. The
deprecation is carried by SUPERSEDED_PATHS and the openedx#39128 entry; the two
v1 files are back to their base content.
…s v2

The v2 service imports lms.djangoapps.certificates.api as the v1 view
does, so it needs the same import-linter exemption. The view parameter
description is a plain string shared with the serializer instead of a
help_text that mypy types as optional. Two help texts no longer refer to
a successor endpoint that is not merged.
Role lookups are cached on the user instance, so a v2 read made with the
instance that served the v1 read skipped the policy-row insert the test
expected to see. Each measured read now authenticates a new instance of
the reader. Also imports the user model through get_user_model().
@taimoor-ahmed-1
taimoor-ahmed-1 force-pushed the feat/standardize-course-settings-api branch from 125a748 to a78f486 Compare October 31, 2026 16:32

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

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

3 participants