Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
a7e307f
feat: add /api/user/v2/ users, user_preferences and time_zones
Abdul-Muqadim-Arbisoft Oct 14, 2026
149e8c8
feat: publish the user API in the LMS schema
Abdul-Muqadim-Arbisoft Oct 14, 2026
e487de7
feat: standardize the account, name-change, verification and profile …
Abdul-Muqadim-Arbisoft Oct 23, 2026
72a869d
fix: let the account tests be collected under the CMS settings
Abdul-Muqadim-Arbisoft Oct 26, 2026
222917e
fix: name the library JWT class on the views that replace a default a…
Abdul-Muqadim-Arbisoft Oct 26, 2026
7cb862f
fix: align four published details across the three /api/user/v2/ phases
Abdul-Muqadim-Arbisoft Oct 26, 2026
a3e7445
test: hold the four corrected success codes to the handlers that answ…
Abdul-Muqadim-Arbisoft Oct 29, 2026
94af55b
fix: mark the superseded user addresses Studio serves as superseded t…
Abdul-Muqadim-Arbisoft Oct 29, 2026
b572b73
fix: carry the service's appended components into the documentation s…
Abdul-Muqadim-Arbisoft Oct 29, 2026
0e0124f
fix: refuse a page size no collection of this version will give
Abdul-Muqadim-Arbisoft Oct 29, 2026
cdffcf7
fix: say in words why a request was refused, not in printed data
Abdul-Muqadim-Arbisoft Oct 29, 2026
f1e6e65
fix: answer a member account address from the record the lookup resol…
Abdul-Muqadim-Arbisoft Oct 31, 2026
c9be206
fix: answer every account holding a selected email address
Abdul-Muqadim-Arbisoft Oct 31, 2026
d54e8fd
fix: admit an operation on one account by the rules of the account re…
Abdul-Muqadim-Arbisoft Oct 31, 2026
da42623
fix: describe each address by the body and the media types it reads
Abdul-Muqadim-Arbisoft Oct 31, 2026
d02caae
docs: record where the retirement of a re-used address is read differ…
Abdul-Muqadim-Arbisoft Oct 31, 2026
7da47ed
test: guard the record-visibility layer of the two queryset-backed us…
Abdul-Muqadim-Arbisoft Oct 31, 2026
eb6e0f2
fix: describe each address of this version to the caller that asks, n…
Abdul-Muqadim-Arbisoft Oct 31, 2026
ffcf6cf
fix: refuse a selected property name that reaches into a property
Abdul-Muqadim-Arbisoft Oct 31, 2026
78299b0
fix: test and describe the rate limit on changing a recovery address
Abdul-Muqadim-Arbisoft Oct 31, 2026
87047e9
fix: describe the body a search is asked for in, not the one it answe…
Abdul-Muqadim-Arbisoft Oct 31, 2026
69c5b73
fix: say both things that get a clearing of a profile image refused
Abdul-Muqadim-Arbisoft Oct 31, 2026
f4c6f0f
fix: publish the two refusals every address of this version could alr…
Abdul-Muqadim-Arbisoft Oct 31, 2026
540d11a
fix: stop naming the site's sharing configuration in a refusal about …
Abdul-Muqadim-Arbisoft Nov 1, 2026
8f6b550
fix: fetch rows carried inside another resource through the policy th…
Abdul-Muqadim-Arbisoft Nov 1, 2026
5346d5b
fix: describe the superseded user addresses the same way in both serv…
Abdul-Muqadim-Arbisoft Nov 1, 2026
cd436b7
fix: mount the prefix that answers unknown addresses after the applic…
Abdul-Muqadim-Arbisoft Nov 1, 2026
e933133
fix: serve the document published for client generation from the cach…
Abdul-Muqadim-Arbisoft Nov 1, 2026
7cc2a8e
test: hold the settings module the published reference is generated from
Abdul-Muqadim-Arbisoft Nov 1, 2026
2aef217
fix: carry inside a preference only the owner entries the caller may …
Abdul-Muqadim-Arbisoft Nov 1, 2026
33f7e92
fix: name the refusal of an address more than one account answers to
Abdul-Muqadim-Arbisoft Nov 1, 2026
45d906f
fix: record a profile image on the account the request was admitted for
Abdul-Muqadim-Arbisoft Nov 1, 2026
03b3301
fix: read an address held and left more than once through the addresses
Abdul-Muqadim-Arbisoft Nov 1, 2026
4095ee5
docs: record beside the name in an address what each address does wit…
Abdul-Muqadim-Arbisoft Nov 1, 2026
3c194e9
test: say which refusal the recording of an image without a date is e…
Abdul-Muqadim-Arbisoft Nov 1, 2026
44063e8
refactor: hold these addresses, their descriptions and the hook that …
Abdul-Muqadim-Arbisoft Nov 1, 2026
ffe739c
test: check that neither schema module reaches into the other service
Abdul-Muqadim-Arbisoft Nov 1, 2026
87ea4c5
test: hold each version to the refusal it answers with
Abdul-Muqadim-Arbisoft Nov 1, 2026
f5fd5ae
style: satisfy the repository's own lint on the lines this work adds
Abdul-Muqadim-Arbisoft Nov 1, 2026
9892aa2
fix: refuse a restricted application's token on every operation of /a…
Abdul-Muqadim-Arbisoft Nov 4, 2026
466c8bd
fix: admit the API key at the user directory and stored preferences, …
Abdul-Muqadim-Arbisoft Nov 4, 2026
3ba2cd4
fix: read an unconfirmed account's session at the email search, as v1…
Abdul-Muqadim-Arbisoft Nov 4, 2026
decbf90
fix: refuse an image whose metadata cannot be written back with a 400
Abdul-Muqadim-Arbisoft Nov 4, 2026
cbbc334
fix: publish the superseded account requests without the replacement'…
Abdul-Muqadim-Arbisoft Nov 4, 2026
9fd376d
fix: read a name, a searched address and the account selectors as sent
Abdul-Muqadim-Arbisoft Nov 4, 2026
44e95d3
fix: publish the page links as members every page carries
Abdul-Muqadim-Arbisoft Nov 4, 2026
4133581
test: pin the two refusals the account list and member add over versi…
Abdul-Muqadim-Arbisoft Nov 4, 2026
a6a154e
fix: read the directory's username selector as sent
Abdul-Muqadim-Arbisoft Nov 4, 2026
6eb8cf5
fix: tell an inactive non-staff caller the email search is not there
Abdul-Muqadim-Arbisoft Nov 4, 2026
4d0bb0c
fix: report an ambiguous account address under the username it carried
Abdul-Muqadim-Arbisoft Nov 4, 2026
dec231f
test: drive the absent-account branches through what the services raise
Abdul-Muqadim-Arbisoft Nov 4, 2026
659717b
revert: serve /lms-api/schema/ as master serves it
Abdul-Muqadim-Arbisoft Nov 4, 2026
648b3b4
fix: answer a refused method, Accept, media type or body as other API…
Abdul-Muqadim-Arbisoft Nov 4, 2026
48a013e
fix: answer an unserved address under /api/user/v2/ as the rest of th…
Abdul-Muqadim-Arbisoft Nov 4, 2026
fbec0e5
fix: leave the shared profile image helper as it was and record in ve…
Abdul-Muqadim-Arbisoft Nov 4, 2026
0846c64
test: generate the user documents once, from the user mounts only
Abdul-Muqadim-Arbisoft Nov 4, 2026
ff17d04
test: leave the existing schema tests as they were and add beside them
Abdul-Muqadim-Arbisoft Nov 4, 2026
1ec6d4c
docs: shorten the comments added to settings, URLconfs and schema mod…
Abdul-Muqadim-Arbisoft Nov 4, 2026
88bce64
fix: mark Studio's superseded user addresses through its own hook
Abdul-Muqadim-Arbisoft Nov 4, 2026
ff4e951
refactor: import the replacing serializers where lint-imports can see…
Abdul-Muqadim-Arbisoft Nov 4, 2026
45b7d17
fix: name each refused item of a list body in words
Abdul-Muqadim-Arbisoft Nov 4, 2026
e7f2c4e
feat: standardize the preferences and email opt-in APIs as /api/user/v2/
Abdul-Muqadim-Arbisoft Oct 26, 2026
178e375
fix: name the library JWT class on the views that replace a default a…
Abdul-Muqadim-Arbisoft Oct 26, 2026
6e1ce0c
fix: publish a body no request may leave out as one the operation needs
Abdul-Muqadim-Arbisoft Oct 29, 2026
80f5462
test: hold the four corrected success codes to the handlers that answ…
Abdul-Muqadim-Arbisoft Oct 29, 2026
09f3cab
fix: admit an operation on one account by the rules of the account re…
Abdul-Muqadim-Arbisoft Oct 31, 2026
123a84b
fix: describe each address by the body and the media types it reads
Abdul-Muqadim-Arbisoft Oct 31, 2026
40e758b
fix: describe the body a search is asked for in, not the one it answe…
Abdul-Muqadim-Arbisoft Oct 31, 2026
0a0931b
fix: publish the two refusals every address of this version could alr…
Abdul-Muqadim-Arbisoft Oct 31, 2026
057e7be
fix: describe the superseded user addresses the same way in both serv…
Abdul-Muqadim-Arbisoft Nov 1, 2026
fa86aeb
fix: say in the document that a change has to name a preference
Abdul-Muqadim-Arbisoft Nov 1, 2026
3c4f614
style: satisfy the repository's own lint on the lines this work adds
Abdul-Muqadim-Arbisoft Nov 1, 2026
0489b34
test: refuse a restricted application's token at every preference and…
Abdul-Muqadim-Arbisoft Nov 4, 2026
33ec17c
fix: check a preference value as it was sent, as version 1 does
Abdul-Muqadim-Arbisoft Nov 4, 2026
34285a6
fix: answer a preference name with whitespace around it as absent
Abdul-Muqadim-Arbisoft Nov 4, 2026
14a1258
refactor: drop the preference view's handler for a refusal the servic…
Abdul-Muqadim-Arbisoft Nov 4, 2026
3b4a31f
test: restore the two checks the restack dropped, without the whole d…
Abdul-Muqadim-Arbisoft Nov 4, 2026
5848989
fix: leave Studio's preference addresses current in its own document
Abdul-Muqadim-Arbisoft Nov 4, 2026
75a854e
fix: publish the superseded preference writes without the replacement…
Abdul-Muqadim-Arbisoft Nov 4, 2026
9eab6ac
feat: standardize the account retirement pipeline as /api/user/v2/
Abdul-Muqadim-Arbisoft Oct 26, 2026
e43b4c0
fix: refuse an ambiguous account name, and answer the method a caller…
Abdul-Muqadim-Arbisoft Oct 27, 2026
1a81c0c
fix: publish what a caller must send, and read every step it names
Abdul-Muqadim-Arbisoft Oct 27, 2026
16d1eaa
test: pin the claims of /api/user/v2/ that no test could fail
Abdul-Muqadim-Arbisoft Oct 27, 2026
41e6883
fix: resolve every retirement operation through its own resource
Abdul-Muqadim-Arbisoft Oct 27, 2026
702986b
fix: publish the partner-report entry the claim can actually produce
Abdul-Muqadim-Arbisoft Oct 28, 2026
4261205
fix: judge the record a request resolved, and let a document keep its…
Abdul-Muqadim-Arbisoft Oct 28, 2026
37ee875
fix: name the retirement component once, and cover its queue reads in…
Abdul-Muqadim-Arbisoft Nov 1, 2026
2879ea5
fix: keep the line a refused batch wrote about itself, and name the c…
Abdul-Muqadim-Arbisoft Nov 1, 2026
903751d
fix: say that a selected name reaching into a property is refused, an…
Abdul-Muqadim-Arbisoft Nov 1, 2026
d97f67c
fix: publish and measure the refusal every retirement address already…
Abdul-Muqadim-Arbisoft Nov 1, 2026
9b79e8f
docs: describe each retirement address to the caller that asks, not t…
Abdul-Muqadim-Arbisoft Nov 1, 2026
e896143
style: import the two modules of this version on a line each
Abdul-Muqadim-Arbisoft Nov 1, 2026
7c2c073
test: hold the superseded names Studio publishes for the retirement a…
Abdul-Muqadim-Arbisoft Nov 1, 2026
dc89ba3
style: declare the attributes a helper fills before it fills them
Abdul-Muqadim-Arbisoft Nov 1, 2026
829d3e3
fix: append the component Studio's document had stopped carrying
Abdul-Muqadim-Arbisoft Nov 1, 2026
62b5fa7
fix: keep the user retirement model out of Studio's Authoring API doc…
Abdul-Muqadim-Arbisoft Nov 4, 2026
17e4c14
fix: dispose of a whole retirement queue in one batch request
Abdul-Muqadim-Arbisoft Nov 4, 2026
05c4c3b
fix: match each name a batch disposal is given exactly as it was sent
Abdul-Muqadim-Arbisoft Nov 4, 2026
70e818b
test: read the disposal's permissions from its declaration in every r…
Abdul-Muqadim-Arbisoft Nov 4, 2026
23f1eab
test: name each refused name of a batch disposal by its position
Abdul-Muqadim-Arbisoft Nov 4, 2026
0583ff5
fix: refuse a cooling-off period or a claim limit too large to act on
Abdul-Muqadim-Arbisoft Nov 4, 2026
e89e302
refactor: read the organizations an account studied with from a share…
Abdul-Muqadim-Arbisoft Nov 4, 2026
9e556fd
refactor: judge in version 2 whether a retirement admits pipeline work
Abdul-Muqadim-Arbisoft Nov 4, 2026
8932cd0
fix: keep the retirement move whole without changing a generation set…
Abdul-Muqadim-Arbisoft Nov 4, 2026
08f351b
fix: name the retirement choice sets after the models that carry them
Abdul-Muqadim-Arbisoft Nov 4, 2026
e76dedd
style: shorten the retirement pipeline's long comments
Abdul-Muqadim-Arbisoft Nov 5, 2026
c59981f
test: read the retirement permissions from their declaration in every…
Abdul-Muqadim-Arbisoft Nov 5, 2026
2e821e4
feat: serve registration, sign-in and password reset under the user A…
Abdul-Muqadim-Arbisoft Nov 1, 2026
6d6f887
feat: mark the older registration, sign-in and password reset address…
Abdul-Muqadim-Arbisoft Nov 1, 2026
02322c6
test: read what each sign-in, registration and reset view declares be…
Abdul-Muqadim-Arbisoft Nov 1, 2026
8741c85
fix: publish the label and the hint of a registration input as possib…
Abdul-Muqadim-Arbisoft Nov 1, 2026
0cdc411
fix: publish the refusals of an unusable Accept header and an unreada…
Abdul-Muqadim-Arbisoft Nov 1, 2026
9df770d
fix: keep the sign-in address that takes its version as a parameter o…
Abdul-Muqadim-Arbisoft Nov 1, 2026
97c9ce6
fix: publish the new sign-in with the CSRF requirement the older sign…
Abdul-Muqadim-Arbisoft Nov 1, 2026
3172079
test: count a database write by its first keyword
Abdul-Muqadim-Arbisoft Nov 1, 2026
0a77b22
test: check that reading a form sends no signal and no event
Abdul-Muqadim-Arbisoft Nov 1, 2026
1e09f00
test: read the forms while a sign-in through another provider is unde…
Abdul-Muqadim-Arbisoft Nov 1, 2026
f8e3af0
fix: publish every refusal the password reset token check answers wit…
Abdul-Muqadim-Arbisoft Nov 1, 2026
7332145
test: show both sign-in limits shared in every direction
Abdul-Muqadim-Arbisoft Nov 1, 2026
c210c6c
fix: publish the registrations that need no password
Abdul-Muqadim-Arbisoft Nov 1, 2026
1fc0d63
fix: publish the refusal of a body that cannot be parsed
Abdul-Muqadim-Arbisoft Nov 1, 2026
d7dbd19
docs: say that refused registrations count towards the registration l…
Abdul-Muqadim-Arbisoft Nov 1, 2026
51c4db1
docs: describe the sign-in answered with 400 after a correct password
Abdul-Muqadim-Arbisoft Nov 1, 2026
09c2d62
docs: give the true reasons the sign-in and registration routes are c…
Abdul-Muqadim-Arbisoft Nov 1, 2026
012552e
fix: publish what the one sign-in refusal answered with 403 always ca…
Abdul-Muqadim-Arbisoft Nov 1, 2026
0c451a1
test: hold the members each response promises to the bodies really sent
Abdul-Muqadim-Arbisoft Nov 1, 2026
b67a419
fix: describe the CSRF token in every document whose sign-in names it
Abdul-Muqadim-Arbisoft Nov 2, 2026
3bd5007
test: generate the LMS user documents with the authentication app's m…
Abdul-Muqadim-Arbisoft Nov 5, 2026
78c4fbf
test: hold each document check of the authentication addresses to a s…
Abdul-Muqadim-Arbisoft Nov 5, 2026
ae4332c
test: generate the authentication addresses' documents once, from the…
Abdul-Muqadim-Arbisoft Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

from cms.lib.xblock.authoring_mixin import AuthoringMixin
from cms.lib.xblock.upstream_sync import UpstreamSyncMixin
from openedx.core.djangoapps.user_api.schema_components import CREDENTIAL_SCHEMES
from openedx.core.lib.derived import Derived
from openedx.core.lib.features_setting_proxy import FeaturesProxy
from openedx.envs.common import * # pylint: disable=wildcard-import # noqa: F403
Expand Down Expand Up @@ -1340,6 +1341,13 @@ def _should_send_xblock_events(settings):
'cms.lib.spectacular.cms_mark_migrated_paths',
'openedx.core.apidocs.untag_deprecated_operations',
],
# The sign-in this service serves at the user API's addresses names the
# CSRF token, so the token is described here from the definition the other
# service appends. Only that credential is described: no operation of this
# service names any other.
'APPEND_COMPONENTS': {
'securitySchemes': {'csrf': CREDENTIAL_SCHEMES['csrf']},
},
# Stripped from each path to derive its tags and operationId, so changing
# it renames every operationId. Paths are published in full, from the
# service root, so /api/contentstore and /api/authoring resolve against the
Expand Down
41 changes: 39 additions & 2 deletions cms/lib/spectacular.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,52 @@

from drf_spectacular.openapi import AutoSchema

# Legacy operations superseded by a conforming address under /api/authoring/.
# Each is listed in the truncated spelling as well as the full one: the routes
# Legacy operations superseded by a conforming address. Each contentstore one
# is listed in the truncated spelling as well as the full one: the routes
# make the final character optional, so both addresses resolve, and which one
# the generator emits depends on how it renders that regex.
SUPERSEDED_PATHS = (
"/api/contentstore/v0/youtube_transcripts/{course_id}/chec",
"/api/contentstore/v0/youtube_transcripts/{course_id}/check",
"/api/contentstore/v0/youtube_transcripts/{course_id}/uploa",
"/api/contentstore/v0/youtube_transcripts/{course_id}/upload",
# The user API this service mounts as the LMS does; the LMS serves the replacements.
# The preference addresses are left out: this service's own pages change a
# preference through them, and it serves no replacement for them.
"/api/user/v1/me",
"/api/user/v1/accounts",
"/api/user/v1/accounts/search_emails",
"/api/user/v1/accounts/{username}",
"/api/user/v1/accounts/{username}/deactivate/",
"/api/user/v1/accounts/{username}/image",
"/api/user/v1/accounts/{username}/verification_status/",
"/api/user/v1/accounts/{username}/verifications/",
"/api/user/v1/accounts/name_change/",
"/api/user/v1/accounts/name_change/{username}/confirm/",
"/api/user/v1/users/",
"/api/user/v1/users/{id}/",
"/api/user/v1/user_prefs/",
"/api/user/v1/user_prefs/{id}/",
"/api/user/v1/preferences/{pref_key}/users/",
"/api/user/v1/forum_roles/{name}/users/",
"/api/user/v1/preferences/time_zones/",
"/api/user/v1/accounts/{username}/retirement_status/",
"/api/user/v1/accounts/retirement_queue/",
"/api/user/v1/accounts/retirements_by_status_and_date/",
"/api/user/v1/accounts/update_retirement_status/",
"/api/user/v1/accounts/retirement_cleanup/",
"/api/user/v1/accounts/retirement_partner_report/",
"/api/user/v1/accounts/retirement_partner_report_cleanup/",
"/api/user/v1/accounts/cancel_retirement/",
"/api/user/v1/accounts/retire/",
"/api/user/v1/accounts/retire_misc/",
# The registration, sign-in and password reset addresses of the authentication app.
"/api/user/v1/account/registration/",
"/api/user/v2/account/registration/",
"/api/user/v1/account/password_reset/",
"/api/user/v1/account/password_reset/token/validate/",
"/api/user/v1/validation/registration",
"/api/user/{api_version}/account/login_session/",
)

# Legacy addresses of APIs migrated to /api/authoring/, marked deprecated for
Expand Down
276 changes: 273 additions & 3 deletions cms/lib/tests/test_spectacular.py

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions cms/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.urls import path

from openedx.core.apidocs import cached_schema_view
from openedx.core.djangolib.testing.utils import unresolved_references


class CmsModuleTests(TestCase):
Expand All @@ -29,6 +30,20 @@ def test_api_docs_schema(self):
response = self.client.get('/api-docs/schema/')
assert response.status_code == 200

def test_every_reference_in_each_served_document_resolves(self):
"""
Each document this service serves defines every component it refers to.

A component appended through the settings is copied in as written, so
one pointing at a model that only another document defines leaves a
reference that a client generator refuses.
"""
for address in ('/api-docs/schema/', '/authoring-api/schema/'):
response = self.client.get(address, HTTP_ACCEPT='application/vnd.oai.openapi+json')
assert response.status_code == 200, address
unresolved = unresolved_references(response.json())
assert not unresolved, (address, unresolved)

@override_settings(
OPENAPI_CACHE_TIMEOUT=60,
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
Expand Down
2 changes: 2 additions & 0 deletions docs/docs_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@

# OpenAPI 3 security schemes, consumed by drf-spectacular. "Basic" uses the
# OpenAPI 3 http/basic form rather than Swagger 2.0's type: basic.
# Components the service settings already append are kept.
SPECTACULAR_SETTINGS["APPEND_COMPONENTS"] = { # noqa: F405
**SPECTACULAR_SETTINGS.get("APPEND_COMPONENTS", {}), # noqa: F405
"securitySchemes": {
"Basic": {
"type": "http",
Expand Down
34 changes: 28 additions & 6 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
from path import Path as path

from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
from openedx.core.djangoapps.user_api.schema_components import (
CREDENTIAL_SCHEMES,
KEPT_COMPONENT_SCHEMAS,
)
from openedx.core.lib.derived import Derived
from openedx.core.lib.features_setting_proxy import FeaturesProxy
from openedx.envs.common import * # pylint: disable=wildcard-import # noqa: F403
Expand Down Expand Up @@ -2179,23 +2183,41 @@

add_optional_apps(OPTIONAL_APPS, INSTALLED_APPS) # noqa: F405

###################### drf-spectacular (LMS enrollment schema) ######################
###################### drf-spectacular (LMS API schema) ######################
SPECTACULAR_SETTINGS = {
'TITLE': 'LMS Enrollment API',
'TITLE': 'LMS API',
'VERSION': '0.1.0',
'SERVE_INCLUDE_SCHEMA': False,
'PREPROCESSING_HOOKS': ['lms.lib.spectacular.lms_api_filter'],
# Mark legacy slashless enrollment addresses deprecated. The enum hook is
# drf-spectacular's default, restated because setting this key replaces
# the default list.
# Mark superseded addresses deprecated. The enum hook is drf-spectacular's
# default, restated because setting this key replaces the default list.
'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums',
'lms.lib.spectacular.lms_mark_legacy_paths_deprecated',
'openedx.core.djangoapps.user_api.schema.mark_superseded_user_paths',
'openedx.core.apidocs.untag_deprecated_operations',
],
# Carried by every document built from these settings; Studio generates the
# identical model from its own description of the operation.
'APPEND_COMPONENTS': {
'schemas': dict(KEPT_COMPONENT_SCHEMAS),
# How a caller sends each credential the operations of this service
# take beside the session cookie. The generator describes the cookie
# and nothing else, so an operation taking a token names these and a
# client generated from the document can offer the caller a way to
# send it.
'securitySchemes': dict(CREDENTIAL_SCHEMES),
},
# Pinned so a second same-named choice field cannot rename these sets; each
# names its model so it never claims a plain name another field may need.
'ENUM_NAME_OVERRIDES': {
'RetirementErasureScopeEnum': 'openedx.core.djangoapps.user_api.v2.serializers.ERASURE_SCOPE_CHOICES',
'RetirementBatchResultOutcomeEnum':
'openedx.core.djangoapps.user_api.v2.serializers.BATCH_OUTCOME_CHOICES',
},
# Stripped from each path to derive its tags and operationId, so changing
# it renames every operationId. Paths themselves are emitted in full.
'SCHEMA_PATH_PREFIX': '/api/enrollment',
'SCHEMA_PATH_PREFIX': r'/api/(enrollment|user)',
# Serve the Swagger UI and ReDoc assets from drf-spectacular-sidecar rather
# than drf-spectacular's default unpinned jsdelivr CDN URLs, keeping them
# self-hosted and version-pinned as the drf-yasg bundles were.
Expand Down
2 changes: 1 addition & 1 deletion lms/envs/devstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def should_show_debug_toolbar(request): # pylint: disable=missing-function-docs
'COMPLETE',
]

###################### drf-spectacular (LMS enrollment schema) ######################
###################### drf-spectacular (LMS API schema) ######################
SPECTACULAR_SETTINGS['SERVERS'] = [ # noqa: F405
{'url': LMS_ROOT_URL, 'description': 'Local'}, # noqa: F405
]
Expand Down
2 changes: 1 addition & 1 deletion lms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ def get_env_setting(setting):
MIDDLEWARE.extend(_YAML_TOKENS.get('EXTRA_MIDDLEWARE_CLASSES', [])) # noqa: F405


###################### drf-spectacular (LMS enrollment schema) ######################
###################### drf-spectacular (LMS API schema) ######################
SPECTACULAR_SETTINGS['SERVERS'] = [ # noqa: F405
{'url': LMS_ROOT_URL, 'description': 'Local'}, # noqa: F405
]
Expand Down
50 changes: 46 additions & 4 deletions lms/lib/spectacular.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,76 @@

from drf_spectacular.openapi import AutoSchema

from openedx.core.djangoapps.user_api.schema import SupersededUserOperations, is_superseded_user_path

LEGACY_V2_PREFIX = "/api/enrollment/v2/"

# The one legacy address that differs from its conforming twin only by the
# trailing slash, so both tokenize to the same operationId. The other legacy
# addresses already have distinct ids and keep them.
COLLIDING_LEGACY_PATH = "/api/enrollment/v2/enrollments"

PUBLISHED_PATH_PATTERN = re.compile(r"^/api/enrollment/v\d+/")

# Older apps share this prefix, so the view's package decides what is published.
PUBLISHED_USER_PREFIX = "/api/user/v2/"
PUBLISHED_USER_VIEW_MODULES = (
"openedx.core.djangoapps.user_api.v2.",
"openedx.core.djangoapps.profile_images.v2.",
)

# User operations share one tag where the derived tag would only name a version.
USER_PATH_PREFIX = "/api/user/"
USER_TAG = "user"

_VERSION_TAG = re.compile(r"v\d+")

# An address that takes its version as a parameter is left out of the
# superseded addresses this schema admits, though it is still marked wherever
# it is published: posting there to sign in publishes the description its
# handler carries, which is not what that sign-in reads or answers with, and
# nothing here can replace it.
_VERSION_PARAMETER = "{api_version}"


def _is_legacy_path(path):
"""Conforming routes always end in a slash, so a slashless v2 path is a legacy address."""
return path.startswith(LEGACY_V2_PREFIX) and not path.endswith("/")


def _is_published_user_path(path, callback):
"""A user address is documented when this version serves it, or when it is a superseded one admitted above."""
if is_superseded_user_path(path) and _VERSION_PARAMETER not in path:
return True
if not path.startswith(PUBLISHED_USER_PREFIX):
return False
view = getattr(callback, "cls", None)
return getattr(view, "__module__", "").startswith(PUBLISHED_USER_VIEW_MODULES)


def lms_api_filter(endpoints):
"""
Pre-processing hook: keep only enrollment v2 endpoints tagged for the SDK.
Pre-processing hook: keep only the endpoints this schema documents.
"""
filtered = []
ENROLLMENT_PATH_PATTERN = re.compile(r"^/api/enrollment/v\d+/")

for path, path_regex, method, callback in endpoints:
if ENROLLMENT_PATH_PATTERN.match(path):
if PUBLISHED_PATH_PATTERN.match(path) or _is_published_user_path(path, callback):
filtered.append((path, path_regex, method, callback))

return filtered


class LmsAutoSchema(AutoSchema):
class LmsAutoSchema(SupersededUserOperations, AutoSchema):
"""
Give the colliding legacy Enrollment v2 address a distinct operationId.

It and its conforming twin tokenize to the same operationId, so
drf-spectacular would otherwise break the tie with a numeral suffix in
registration order. The conforming address keeps the clean id; the
deprecated one is suffixed ``_legacy``.

A user operation whose derived tag would name a version is tagged ``user``.
"""

def get_operation_id(self):
Expand All @@ -48,6 +83,13 @@ def get_operation_id(self):
return f"{operation_id}_legacy"
return operation_id

def get_tags(self):
"""Name the area whenever the tag derived for a user address would name a version."""
tags = super().get_tags()
if self.path.startswith(USER_PATH_PREFIX) and tags and _VERSION_TAG.fullmatch(tags[0]):
return [USER_TAG]
return tags


def lms_mark_legacy_paths_deprecated(result, generator, request, public): # pylint: disable=unused-argument
"""Mark the legacy slashless Enrollment v2 addresses ``deprecated: true``."""
Expand Down
Loading
Loading