Skip to content

feat: apply ADR 0038 (URL structure standardization) across 6 standardized APIs - #39078

Open
Abdul-Muqadim-Arbisoft wants to merge 7 commits into
openedx:masterfrom
edly-io:feat/apply_url_structure_adr_on_apis
Open

feat: apply ADR 0038 (URL structure standardization) across 6 standardized APIs#39078
Abdul-Muqadim-Arbisoft wants to merge 7 commits into
openedx:masterfrom
edly-io:feat/apply_url_structure_adr_on_apis

Conversation

@Abdul-Muqadim-Arbisoft

Copy link
Copy Markdown
Contributor

Summary

Applies ADR 0038 — Standardize REST API URL Structure (#39003) to the 6 APIs previously standardized under the FC-0118 effort, per the ADR's implementation note 4: "Migrate the APIs already standardized under FC-0118 first … each keeping its current version number."

Every migration follows the ADR's OEP-21 rule: the conforming path is mounted as an additional route to the same view, the legacy path stays live for its deprecation window and is marked deprecated: true in the OpenAPI schema. No existing address changes behaviour and none is removed.

Depends on edx-drf-extensions==10.8.0, whose shared course_key / usage_key path converters (edx-drf-extensions#573) this PR consumes (ADR 0038 rule 9).

Commit Area Related PR Legacy address (kept, deprecated) Conforming address (new)
1 shared converters edx-drf-extensions#573 registers <course_key:…> / <usage_key:…> per service
2 Xblock v1 #38723 /api/contentstore/v1/xblock/{usage_key}/ /api/authoring/v1/xblocks/{usage_key}/
3 CourseHome v3 #38694 /api/contentstore/v3/home/… /api/authoring/v3/home/… + x-internal
4 CourseHome v4 #38684 /api/contentstore/v4/home/courses/ /api/authoring/v4/courses/
5 Course Detail v3 #38708 /api/contentstore/v3/course_details/{id}/ /api/authoring/v3/courses/{course_key}/details/
6 AuthorGrading v3 #38726 /api/contentstore/v3/authoring_grading/{id}/ /api/authoring/v3/courses/{course_key}/grading/
7 Enrollment v2 #38724 slashless routes kept required-trailing-slash routes + snake_case URL names

Changes

feat — shared opaque-key URL converters (commit 1)

Registers the CourseKeyConverter / UsageKeyConverter from edx-drf-extensions 10.8.0 once per service (lms/urls.py, cms/urls.py). Conforming routes resolve opaque keys in the URLconf, views receive parsed keys, and malformed or deprecated (Org/Course/Run, i4x://) keys become routing-level 404s. Bumps edx-drf-extensions 10.7.0 → 10.8.0.

feat — the five CMS APIs (commits 2–6)

New authoring_urls.py modules per version, mounted from cms/urls.py under their full api/authoring/v{N}/ prefix (rule 5), serving the same viewsets as the legacy mounts:

  • rule 2/3 — plural collections under a domain-named api_name (authoring), not the implementing Django app (contentstore);
  • rule 4/8 — screen-shaped course_details / authoring_grading become courses/{course_key}/details/ and …/grading/ (the ADR's own target), one level deep; home/courses/ (v4) becomes the concrete plural courses/;
  • rule 4 (BFF) — v3 home stays screen-named under the ADR's BFF provision: one canonical conforming mount, marked x-internal in the schema;
  • rule 9 — course/usage keys resolved by the shared converters; views coerce a parsed key back to the string form their bodies expect (single code path for both mounts);
  • rule 11snake_case, version-free, unique URL names.

cms/lib/spectacular.py gains a post-processing hook marking the legacy addresses deprecated: true and the home BFF x-internal; cms_api_filter now also admits /api/authoring/ paths.

feat — Enrollment v2 (commit 7)

/api/enrollment/v2/ already conforms in name and version position; this fixes the remaining rule 6/11 violations, dual-mounted beside the legacy slashless routes:

Legacy (kept, deprecated) Conforming (new)
^enrollments/?$ (optional slash — the ADR's rule 6 example) split: enrollments/ (exact) + slashless legacy — every address that resolved before still resolves
enrollment/{username},{course_key} enrollments/{username},{course_key}/ (plural collection, rule 2)
course/{course_key} courses/{course_key}/
name enrollment-v2-roles name user_roles (path unchanged)

The two legacy retrieve forms no longer share one URL name (the argument-signature fragility rule 11 calls out). The LMS schema hook marks slashless /v2/ addresses deprecated — scoped to v2, since deprecating v1 is its own DEPR decision. Deeper targets (collapsing singular enrollment/, DELETE on the member address for unenroll, me addressing) are contract changes that belong to a future v3 per ADR 0037.

@Abdul-Muqadim-Arbisoft
Abdul-Muqadim-Arbisoft requested review from Faraz32123, feanil and taimoor-ahmed-1 and removed request for a team, feanil and kdmccormick September 6, 2026 20:16
…ons (ADR 0038)

Register the CourseKeyConverter / UsageKeyConverter path converters — added to edx-drf-extensions 10.8.0 (openedx/edx-drf-extensions#573) per ADR 0038's 'Code examples' section — once per service in lms/urls.py and cms/urls.py, as <course_key:...> / <usage_key:...>. ADR 0038 rule 9: conforming routes resolve opaque keys in the URLconf, views receive parsed keys, and malformed or deprecated (Org/Course/Run, i4x://) keys become routing-level 404s.

Bumps edx-drf-extensions 10.7.0 -> 10.8.0, the release that adds the converters (plus the ADR 0029/0032/0036 building blocks this API series already consumes). Converter unit tests live in the library; the per-API URL tests in the following commits cover resolve/reverse integration through the real routes.
@Abdul-Muqadim-Arbisoft
Abdul-Muqadim-Arbisoft force-pushed the feat/apply_url_structure_adr_on_apis branch from 0f1ccfa to 4ef11af Compare September 7, 2026 07:22
Mount the conforming routes beside the legacy /api/contentstore/v1/xblock/ ones (OEP-21), serving the same XblockViewSet: the collection becomes plural (rule 2), the API name describes the domain rather than the implementing Django app (rule 3), the usage key is resolved by the shared usage_key converter, which turns malformed and deprecated i4x:// keys into routing-level 404s (rule 9), and URL names are snake_case, version-free, and unique (rule 11). The viewset's initial() coerces a parsed UsageKey back to the string form the action methods expect, so both mounts share one contract.

The legacy routes stay live for their deprecation window and are marked deprecated: true in the OpenAPI schema via the new cms_mark_migrated_paths post-processing hook; cms_api_filter now also admits /api/authoring/ paths. Tests pin reverse() literals, same-view resolution for both mounts, the routing-level 404, and handler parity on the conforming routes.

ADR 0038 (implementation note 4) asks that /api/authoring/v1/xblocks/ be reconciled with the Learning Core /api/xblock/v2/xblocks/ rather than leaving two names for what looks like one API; that reconciliation is an API-owner decision tracked with the DEPR work, not part of this mechanical migration.
…ernal BFF)

Mount the conforming home/, home/courses/, and home/libraries/ routes at /api/authoring/v3/ beside the legacy /api/contentstore/v3/home/ ones (OEP-21), serving the same HomeViewSet, with snake_case version-free URL names (rule 11) and the domain-named api_name (rule 3).

home is a BFF aggregate for the Studio home screen. Rule 4 disfavors screen names as resources, but the ADR's BFF provision applies: the surface keeps the /api/ prefix and one canonical conforming mount, and is marked x-internal in the OpenAPI schema — on both mounts — so clients can tell it apart from a stable resource contract. The legacy routes are additionally marked deprecated: true. Tests pin reverse() literals, same-view resolution for all three action pairs, and the ADR 0029 envelope on the conforming mount.
Mount the conforming courses/ collection at /api/authoring/v4/ beside the legacy /api/contentstore/v4/home/courses/ route (OEP-21), serving the same HomeCoursesViewSet: the screen-shaped home/courses/ address becomes the concrete plural collection of authorable courses (rule 4), filtered, sorted, and paginated in the query string, under the domain-named api_name (rule 3) with a snake_case version-free URL name (rule 11).

The legacy route stays live for its deprecation window and is marked deprecated: true in the OpenAPI schema. Tests pin the reverse() literal, same-view resolution for both mounts, and 401/200 contract parity on the conforming mount.
Mount the conforming /api/authoring/v3/courses/{course_key}/details/ route beside the legacy /api/contentstore/v3/course_details/{course_id}/ one (OEP-21), serving the same CourseDetailsViewSet: the screen-shaped collection becomes a sub-resource of the plural courses/ collection, one level deep — the ADR's own target for these endpoints (rules 4 and 8) — with the course key resolved by the shared course_key converter, which turns malformed and deprecated Org/Course/Run keys into routing-level 404s (rule 9).

resolve_course_key() now also accepts an already-parsed CourseKey, so both mounts funnel through one code path and share one contract. The legacy route stays live for its deprecation window and is marked deprecated: true in the OpenAPI schema. Tests pin the reverse() literal, same-view resolution, the routing-level 404, and 401/403 parity on the conforming mount.
Mount the conforming /api/authoring/v3/courses/{course_key}/grading/ route beside the legacy /api/contentstore/v3/authoring_grading/{course_key}/ one (OEP-21), serving the same AuthoringGradingViewSet: the app-flavored authoring_grading collection becomes the grading sub-resource of the plural courses/ collection, one level deep (rules 3, 4 and 8) — the authoring_ prefix is dropped because the namespace already says it — with the course key resolved by the shared course_key converter (rule 9). Both mounts funnel through resolve_course_key(), which already accepts parsed keys, so they share one contract.

The legacy route stays live for its deprecation window and is marked deprecated: true in the OpenAPI schema. Tests pin the reverse() literal, same-view resolution, the routing-level 404, and 401/200 PATCH parity on the conforming mount.
…nforming URL names)

/api/enrollment/v2/ already conforms in API name and version position; this fixes the remaining rule 6 and rule 11 violations. Conforming routes are dual-mounted (OEP-21) beside the legacy slashless ones, serving the same views: GET /enrollments/ (the admin list's optional-slash pattern — the ADR's own rule 6 example — is split into an exact slashed route plus a slashless legacy route, so every address that resolved before still resolves), GET /enrollments/{username},{course_key}/ under the plural collection (rule 2), GET /courses/{course_key}/ (plural, slashed), and roles/ renamed from the versioned kebab-case enrollment-v2-roles to user_roles (rule 11; path unchanged). Conforming member routes resolve course keys with the shared course_key converter (rule 9); the views coerce a parsed CourseKey back to the string form their bodies expect, so both mounts share one contract.

The two legacy retrieve forms no longer share one URL name — Django resolved that only by argument signature, the fragility rule 11 calls out — and the slashless legacy addresses are marked deprecated: true in the OpenAPI schema via a post-processing hook scoped to /v2/ (deprecating v1 is its own DEPR decision). Deeper ADR 0038 targets — collapsing the singular enrollment/ collection into enrollments/, replacing the unenroll verb (rule 10) with DELETE on the member address, and addressing the requesting user as me (rule 9) — are contract changes and belong to a future v3 per ADR 0037.

Tests pin the reverse() literals, same-view resolution for every legacy/conforming pair, the optional-slash coverage split, the unique legacy names, the routing-level 404, and 401 parity on both admin-list addresses.
@Abdul-Muqadim-Arbisoft
Abdul-Muqadim-Arbisoft force-pushed the feat/apply_url_structure_adr_on_apis branch from 4ef11af to 1ad2f0e Compare September 7, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant