Repository navigation
feat: serve course advanced settings from authoring v1 - #39303
taimoor-ahmed-1 wants to merge 7 commits into
Conversation
The v0 advanced settings view checks course existence with the @verify_course_exists decorator and access with an inline check_course_advanced_settings_access call in each handler. The conforming views need these as permission classes. IsExistingCourse answers 404 for a course key that names no course run: an unknown course, a key that names only a version, or a library key. CanViewAdvancedSettings and CanManageAdvancedSettings make the same check_course_advanced_settings_access(read|write) call the v0 view makes, so the policy-permission and legacy Studio-access branches are unchanged. Existing classes in the module are untouched.
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id} answers
errors with developer_message bodies and some failures with a 500,
accepts Bearer tokens, returns every settings field by default, and
reports a refused value with the field parser's own text, which can be
Python exception text.
GET/PATCH /api/authoring/v1/courses/{course_key}/advanced_settings/ is
its successor. Errors use the standard envelope; a refused value is
reported under its setting name with one fixed message and the original
text is logged. The default read is the set of settings course authors
edit (v0 with fetch_all=0); view=full returns every field and fields
narrows either view. Authentication keeps the session class that admits
inactive users, as the legacy endpoint did; Bearer is dropped. Access is
IsAuthenticated, IsExistingCourse and a per-action class making v0's
read or write check, and an action without a declared permission is
never served. Parity tests compare v1 with v0 through the test client;
the declared differences are the error envelope and the fixed message.
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id} now has a
conforming successor at /api/authoring/v1/courses/{course_key}/advanced_settings/.
Both v0 operations are marked deprecated in the CMS schema through an
exact SUPERSEDED_PATHS entry, and the view's docstrings name the
successor, how each v0 read maps to a v1 read, and the deprecation
ticket. The view itself is unchanged. The schema hook test that used
this path as its example of an unrelated contentstore path now uses the
heartbeat, which is not deprecated.
|
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. |
Importing cms.djangoapps.contentstore.rest_api.v1.authoring_urls at module level builds its routes before cms/urls.py registers the course_key path converter, so collecting the module failed with an ImproperlyConfigured error. The URL-name test now imports it when it runs, as the youtube transcript tests do. Also wraps a line pylint reported as too long.
The first CI run of the module showed three expectations that did not match the platform: - The legacy role checks consult the policy engine, which admits superusers, so a superuser who is not global staff is served by v0 and v1 alike. The test now expects that. - The proctoring provider field keeps its default unless proctored exams are enabled, so the proctoring tests enable them. - The JWT middleware appends NotJwtRestrictedApplication to the permissions of views that accept JWTs, so the wiring test compares only the classes the view declares.
The side-effect recorder, the body comparison helpers, the schema helpers and the client helpers in the advanced settings tests are not specific to advanced settings. They move unchanged, under public names, into v1/views/tests/helpers.py so the other authoring v1 endpoint tests reuse one copy instead of each carrying its own.
The authentication comment narrated where the classes came from; it now says what an inactive session gets: staff status admits it, and its roles count only on courses where policy-based permissions are on. The superuser test claimed to cover a superuser who is not global staff. Saving a superuser sets is_staff, so the factory user is global staff; the test is renamed and its docstring gives that reason.
Summary
Standardizes the advanced settings resource of the Course Management area (#39059, part of #38137). This is the first of eight resource-group PRs for the area. The others (course optimizer, custom pages and tabs, file assets, video transcripts and images, course settings, group configurations, team members) follow separately.
GET /api/authoring/v1/courses/{course_key}/advanced_settings/andPATCHon the same address (URL nameauthoring_v1:course_advanced_settings) replaceGET/PATCH /api/contentstore/v0/advanced_settings/{course_id}. v0 stays mounted and unchanged. It is markeddeprecated: truein the CMS schema and in its docstrings.It applies ADRs 0025–0038 and OEP-69.
Representations. Without
view(or withview=minimal), the GET returns the settings course authors edit: v0's answer withfetch_all=0, which is what the Advanced Settings page requests.view=fullreturns every settings field, which is v0's default.fields(comma-separated setting names, applied with the library'sproject()) narrows either view and replaces v0'sfilter_fields. PATCH takes{<setting>: {"value": ...}}and returns the editable set, as v0 does.Why a new version and not a fix in place. Each of these changes the contract for v0's callers, so they ride v1 (ADR 0037):
Changes
feat: add course existence and advanced settings permission classescheck_course_advanced_settings_accessandverify_course_existsunchangedIsExistingCourse(404 for an unknown, version-only or library key),CanViewAdvancedSettingsandCanManageAdvancedSettings, which make the samecheck_course_advanced_settings_access(read|write)call v0 makesfeat: serve course advanced settings from authoring v1GET/PATCH /api/authoring/v1/courses/{course_key}/advanced_settings/(new)docs: mark the v0 advanced settings deprecated in favour of authoring v1GET/PATCH /api/contentstore/v0/advanced_settings/{course_id}deprecated: truein the schema (one exactSUPERSEDED_PATHSentry); the docstrings name the successor, how each v0 read maps to v1, and #39128test: import the authoring URLconf inside the test that needs ittest: align the advanced settings tests with the platform's behaviourtest: share the authoring v1 test helpersv1/views/tests/helpers.pyfor the other authoring v1 tests to reusedocs: describe what an inactive session and a superuser are allowedis_staff)ADRs applied
help_texton every field and.v1component names; the response published as an object keyed by setting nameIsAuthenticated,IsExistingCourse, then a per-action class making the same read/write check as v0; unmapped actions are refused by construction; deny matrix with policy permissions off and on@extend_schemaon both actions with summary, description,view/fieldsparameters, examples and every 4xx asErrorResponse; both taggedopenedx-platform-sdkViewSetbacked by a service module, mounted withpath(...)and the sharedcourse_keyconverter, as every route inauthoring_urls.pyisinternaluntil edx-drf-extensions catalogues them, and a malformed key gets the CMS HTML 404 at routing (both pinned by tests)PolicyCacheControlrow on the first permission check against a database that lacks it, as on v0viewandfieldsselect a representation and are validated by a serializer(JwtAuthentication, SessionAuthenticationAllowInactiveUser)declared with its reason, as ADR 0034 requires for an endpoint that usedSessionAuthenticationAllowInactiveUser; Bearer droppedview=full;fieldsprojects with the library helperdeprecated: truein the schema; the DEPR entry is a comment on [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 until it is folded into the issue body/api/authoring/v1/courses/{course_key}/advanced_settings/: domain API name, one-per-course child ofcourses, shared converter, trailing slash, version-free URL name; the v0 name is keptget_queryset();fieldsandviewonly narrow what the caller may already readCompliance matrix
CourseAdvancedSettingsSerializer(an object keyed by setting name, each anAdvancedSetting.v1withvalue,display_name,help,deprecated,hide_on_enabled_publisher),CourseAdvancedSettingsUpdateSerializer(request) andAdvancedSettingsQuerySerializer. The map shape is published through a smallOpenApiSerializerExtension: drf-spectacular 0.30 cannot publish a named map-of-serializer component otherwise, and edx-drf-extensions has nothing equivalent.valueis untyped because a setting's type depends on the field. v0's per-field typed schema also omitted the inherited settings that the endpoint returnsAdvancedSettingsSchemaTest(test_every_member_is_described,test_the_settings_are_an_object_keyed_by_setting_name,test_the_update_body_is_json_settings_each_with_a_value),AdvancedSettingsReadTest.test_the_body_matches_the_published_schema; hygiene H9check_course_advanced_settings_access(user, course_key, "read"|"write")call, so both the policy-permission branch and the legacy Studio-access branch are inherited unchanged. NoDISABLE_ADVANCED_SETTINGSgate is added: v0's REST endpoint has none, and Pages & Resources depends on this endpoint. An action missing from the per-action map raisesImproperlyConfiguredrather than running with login onlyAdvancedSettingsAccessLegacyPermissionsTest,AdvancedSettingsAccessAuthzTest,AdvancedSettingsSiteRestriction{LegacyPermissions,Authz}Test,AdvancedSettingsPermissionWiringTest,AdvancedSettingsProctoringProviderTest; each access case asserts v0's answer tooviewenum (minimal,full),fields, examples, 200/400/401/403/404 withErrorResponse. Operation IDsv1_courses_advanced_settings_retrieve/_partial_update. Both taggedopenedx-platform-sdk; the deprecated v0 operations lose the tag throughuntag_deprecated_operationsAdvancedSettingsSchemaTest(incl.test_the_v0_operations_are_deprecated_and_left_out_of_the_sdk,test_only_superseded_or_migrated_addresses_are_deprecated)CourseAdvancedSettingsViewSet(StandardizedErrorMixin, viewsets.ViewSet)withretrieveandpartial_updateoverv1/advanced_settings_service.py. It is a plain ViewSet because the data path isCourseMetadataover the modulestore. Deviation: mounted withpath("courses/<course_key:course_key>/advanced_settings/", ...as_view({...})), because a router's regex routes cannot use the shared path convertersAdvancedSettingsRoutingTest.test_the_address;AdvancedSettingsQueryCount{LegacyPermissions,Authz}TestStandardizedErrorMixinfirst. 401authn, 403authz, 404not-found(unknown, version-only or library course key; a course the modulestore does not return), 400validationwitherrorskeyed by setting name. A refused value is reported with one fixed message per setting (INVALID_SETTING_MESSAGE); the validator's own text, which can be Python exception text, is logged with the course key and setting name and never returned. Tab-save and unexpected failures return a fixed message. Gaps pinned by tests that flip when fixed: malformed JSON (400), 405, 406 and 415 are typedinternal(edx-drf-extensions 10.9.0 does not catalogue them), and a malformed course key gets the CMS HTML 404 at routingAdvancedSettingsUpdateTest(test_a_refused_value_is_reported_with_a_fixed_message_and_nothing_is_stored, the tab-save and unexpected-failure tests);AdvancedSettingsRoutingTest(test_a_malformed_json_body_is_a_400_typed_internal,test_an_unsupported_media_type_is_a_415_typed_internal,test_other_methods_are_not_allowed,test_unsupported_accept_is_refused_with_406,test_an_unparsable_or_slash_form_key_is_an_html_404)view=full,fields), with policy permissions off and on, nothing is written to SQL or Mongo and no Django signal or Open edX event is sent, including callbacks run on commit. The exception is the first read on a database without openedx-authz'sPolicyCacheControlrow, which inserts it (one INSERT, itspre_save/post_save). That write belongs to the permission library, happens on v0 too, and is allow-listed by nameAdvancedSettingsReadSideEffects{LegacyPermissions,Authz}Test(test_reads_have_no_side_effects,test_the_v0_read_makes_the_same_first_lookup_write,test_the_recorder_sees_writes_signals_and_modulestore_updates, incl. an on-commit case)viewis enum-validated.fieldsis validated by a serializer. v0'sfetch_allandfilter_fieldsare not carried over and are ignored on v1AdvancedSettingsReadTest.test_the_v0_query_parameters_are_ignored(JwtAuthentication, SessionAuthenticationAllowInactiveUser), commented at the declaration: an inactive account's browser session stays authenticated, so the course permission decides: staff status admits it, and its roles count only on courses where policy-based permissions are on. Bearer is dropped. This matches #39100, #39157 and #39227; #39229/#39274 use the platform default on/api/authoring/v2/, so the authoring mounts are not yet uniformAdvancedSettingsAuthenticationTest(test_a_bearer_token_is_refused, the JWT and inactive-session tests); hygiene H4view=full;fieldsprojects withedx_rest_framework_extensions.shaping.projectAdvancedSettingsReadTest(view and fields tests),_ParityMixin.test_equivalent_reads_return_identical_bodiesv1/. v0 frozen apart from docstring-only markers: the syntax tree with docstrings stripped equals the base.deprecated: truethrough one exactSUPERSEDED_PATHSentry; no runtime header (frozen route). DEPR: an entry comment on #39128, to be folded into the issue bodyCmsMarkSupersededPathsTest;AdvancedSettingsSchemaTest.test_the_v0_operations_are_deprecated_and_left_out_of_the_sdkauthoringAPI name,courses/{course_key}/advanced_settings/(a one-per-course child),course_keyconverter, trailing slash,course_advanced_settingsAdvancedSettingsRoutingTest(test_the_address,test_the_address_without_its_trailing_slash_redirects_to_it,test_v0_still_takes_a_slash_separated_key_that_v1_does_not)viewandfieldsnarrow within what the caller may read and cannot widen itAdvancedSettingsReadTestfields testsBackward compatibility
v0 is unchanged.
v0/views/advanced_settings.pygains docstring text only; its syntax tree with docstrings stripped equals the base. Its route, view and URL name are untouched, and v0's own tests are not edited.Parity. v1 is compared with v0 through the Django test client, with policy permissions off and on. Equivalent reads return identical bodies: no
viewmatches v0fetch_all=0,view=fullmatches v0's default, andfieldsmatchesfilter_fields. A valid PATCH stores the same values and returns the same body. The only declared body differences, verbatim from the test module:The comparison fails both on an undeclared difference and on a declared difference that never occurs.
Status and request differences from v0. Each is pinned against v0's own answer:
AdvancedSettingsResponseDifferencesTest.test_a_course_the_modulestore_returns_nothing_for…test_an_update_whose_body_is_not_an_objectvalue…test_an_update_entry_without_a_valueviewvalue…test_an_unknown_view…test_a_bearer_token…test_a_slash_separated_course_keyAdvancedSettingsRoutingTest.test_the_address_without_its_trailing_slash_redirects_to_itAdvancedSettingsUpdateTest.test_a_refused_value_is_reported_with_a_fixed_message_and_nothing_is_storedview=fullfor every field)_ParityMixin.test_the_same_parameters_return_different_settingsMalformed entries for settings authors do not edit are ignored on both versions (
test_a_malformed_entry_of_a_setting_authors_do_not_edit_is_ignored_by_both). The 401 → 404 → 403 order is the same as v0's.Query counts. For a GET, v1 runs no more queries than v0, measured in the same test. No literal baseline was measured, because no platform environment was available.
Schema. Two new operations, and
deprecated: trueon the two v0 operations.docs/cms-openapi.yamlis not edited; thegenerate_openapi_schemasworkflow regenerates it.In-place edits
cms/djangoapps/contentstore/rest_api/v0/views/advanced_settings.pycms/djangoapps/contentstore/rest_api/v1/authoring_urls.pyAdvancedSettingsRoutingTestcms/djangoapps/contentstore/views/permissions.py(shared)AdvancedSettingsPermissionWiringTestand the access testscms/lib/spectacular.py,cms/lib/tests/test_spectacular.pySUPERSEDED_PATHSentry; the comment above the tuple reworded to describe exact-path matching (identical to #39229's wording); the exact-set test and the "unrelated path" example (now/api/contentstore/v0/heartbeat, never deprecated) updatedCmsMarkSupersededPathsTestNew files inside the pre-existing
v1/:advanced_settings_service.py,serializers/advanced_settings.py,views/advanced_settings.py,views/tests/test_advanced_settings.py. They import nothing fromrest_api/v0/.Error-format decision
Versioned. v0 keeps its
developer_message/error_codebodies, its 500s and the validator's text unchanged. v1 uses the envelope. The Advanced Settings page reads v0's error body today (frontend-app-authoringsrc/advanced-settings/AdvancedSettings.tsx), so the format change rides the new version. Its move to v1 is a separate frontend change, which will show the fixed per-setting message in place of the validator text.Deprecations
GET/PATCH /api/contentstore/v0/advanced_settings/{course_id}are superseded byGET/PATCH /api/authoring/v1/courses/{course_key}/advanced_settings/. They aredeprecated: truein the CMS schema, and the docstrings name the successor. The view is otherwise unchanged: no runtime header. Tracked on the FC-0118 umbrella DEPR ticket, #39128; this resource's entry is posted there with the PR. Window per #39128: removed one named release after the release this ships in.Migration notes:
/api/authoring/v1/courses/{course_key}/advanced_settings/, with the trailing slash.fetch_all=0: noview. v0 default (every field):view=full.filter_fields=a,b:fields=a,b(it selects within the chosen view).fetch_allandfilter_fieldsare ignored on v1.Authorization
@view_auth_classes(is_authenticated=True)IsAuthenticatedtest_anonymous_caller_is_refused_with_401(both permission modes), incl. on an unknown course@verify_course_exists()IsExistingCourse(also 404 for version-only and library keys)check_course_advanced_settings_access(user, key, "read")ingetCanViewAdvancedSettings, same callcheck_course_advanced_settings_access(user, key, "write")inpatchCanManageAdvancedSettings, same callCourseMetadata.validate_proctoring_settings: only global staff may changeproctoring_providerafter the course startsAdvancedSettingsProctoringProviderTest: course staff → 400 and the value unchanged; global staff → 200; before start → 200 (v0 and v1)Order: 401, then 404 for a missing course, then 403. Allowed: course staff and instructor, org staff and instructor, global staff, and with policy permissions on, the roles that grant viewing (GET) or managing (PATCH) advanced settings. A superuser is admitted, as on v0: saving a superuser also sets
is_staff, so every superuser is global staff (test_a_superuser_is_allowed).Gate report
Run on the host against
upstream-pr/master(87cd3811a4). No platform environment was available, so four gates were skipped. The test module has not been run locally, and CI is its first run.v0/views/advanced_settings.pydocstrings,v1/authoring_urls.py), 4 ADDED, nothing protected touched (23 shared legacy modules protected)Follow-ups
openedx-platform-sdk; the next openedx-platform-sdk regeneration gains them.src/advanced-settings/data/api.tsandsrc/pages-and-resources/data/api.jscan move to the v1 address. Pages & Resources reads v0's default full set, so it needsview=full(orfields=).CourseMetadatato tell the two apart. That is a future-version candidate.ParseError,MethodNotAllowed,NotAcceptableandUnsupportedMediaType, and render a field-keyeddetailwithout the Python repr of the error dict. 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.COURSE_NOT_FOUND_MESSAGEand an inline course-existence check inCanViewCourse. Whichever of the two lands second should build onIsExistingCourse./api/authoring/: v1 and the feat: apply FC-0118 ADRs to course content APIs #39157 v2 views keepSessionAuthenticationAllowInactiveUser; feat: serve the course outline from authoring v2 #39229 and feat: serve course containers from authoring v2 #39274 use the default. One policy for new versions of such endpoints should be agreed.