Skip to content

sandbox.opencraft.com deployment tracking | DO NOT CLOSE - #824

Draft
kaustavb12 wants to merge 929 commits into
open-craft:kaustav/downstream_pr_targetfrom
openedx:master
Draft

kaustavb12 wants to merge 929 commits into
open-craft:kaustav/downstream_pr_targetfrom
openedx:master

Conversation

@kaustavb12

@kaustavb12 kaustavb12 commented Feb 6, 2026 •

Copy link
Copy Markdown
Member

This is a PR for tracking upstream master, and auto-deploying/updating the https://sandbox.opencraft.com/ Open edX instance, using the PR sandbox automation.

This PR should be kept open, because this sandbox is actually a long-lived instance for demo purposes.

The settings headings below are custom settings that are applied to the instance by the automation.

Internal tickets:

WARNING: use single quotes, not double quotes when quoting settings below. There is an issue with the workflow relating to quote escaping being overly zealous.

Settings

AN_IMPORTANT_NOTICE: |
  ##########################################
  This is the OpenCraft Sandbox (sandbox.opencraft.com) tracking upstream master.
  Please do not delete or modify this instance without checking with Fox first.
  ##########################################
PLATFORM_NAME: OpenCraft Sandbox
LMS_HOST: sandbox.opencraft.com
CMS_HOST: studio.sandbox.opencraft.com
PREVIEW_LMS_HOST: preview.sandbox.opencraft.com
GROVE_SIMPLE_THEME_BRANCH: fox/sandbox-fixes
GROVE_SIMPLE_THEME_REPO: https://github.com/open-craft/brand-openedx.git
GROVE_COMMON_SETTINGS: |
  CATALOG_MICROFRONTEND_URL = 'https://apps.sandbox.opencraft.com/catalog'
  ENABLE_CATALOG_MICROFRONTEND = True
  DEFAULT_COURSE_VISIBILITY_IN_CATALOG = 'about'
GROVE_MFE_LMS_COMMON_SETTINGS: |
  MFE_CONFIG['LOGO_URL'] = 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo.png'
  MFE_CONFIG['LOGO_TRADEMARK_URL'] = 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo-trademark.png'
  MFE_CONFIG['LOGO_WHITE_URL'] = 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo-white.png'
  MFE_CONFIG['FAVICON_URL'] = 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/favicon.ico'
  MFE_CONFIG_OVERRIDES['learner-dashboard'] = {'LOGO_URL': 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo-white.png'}
  MFE_CONFIG_OVERRIDES['catalog'] = {'LOGO_URL': 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo-white.png'}
  MFE_CONFIG_OVERRIDES['profile'] = {'LOGO_URL': 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo-white.png'}
  MFE_CONFIG_OVERRIDES['account'] = {'LOGO_URL': 'https://raw.githubusercontent.com/open-craft/brand-openedx/refs/heads/sandbox/logo-white.png'}
OPENEDX_EXTRA_PIP_REQUIREMENTS:
- git+https://gitlab.com/opencraft/dev/openedx-auto-studio.git@master
- git+https://github.com/open-craft/openedx-edit-links.git@main
- xblock-problem-builder
CONTACT_EMAIL: help@opencraft.com

Tutor requirements

pip install git+https://github.com/open-craft/tutor-contrib-s3.git@kaustav/add_support_for_tutor_22
pip install --no-deps 'git+https://github.com/openedx/openedx-k8s-harmony.git@harmony-chart-0.14.0#egg=tutor-contrib-harmony&subdirectory=tutor-contrib-harmony-plugin'
pip install 'git+https://github.com/open-craft/tutor-contrib-newrelic.git@farhaan/support-master'
tutor plugins enable sandbox
tutor plugins enable grove-simple-theme s3 k8s_harmony newrelic
tutor generate-tokens

@kaustavb12
kaustavb12 marked this pull request as draft February 6, 2026 08:34
@open-craft open-craft locked and limited conversation to collaborators Feb 10, 2026
@kaustavb12 kaustavb12 changed the title test: DO NOT MERGE - Dummy PR to track upstream master DO NOT MERGE - DO NOT DELETE - Dummy PR to track upstream master Apr 7, 2026
@kaustavb12 kaustavb12 changed the title DO NOT MERGE - DO NOT DELETE - Dummy PR to track upstream master DO NOT MERGE - DO NOT CLOSE - Dummy PR to track upstream master Apr 7, 2026
Faraz32123 and others added 15 commits July 29, 2026 10:26
fix: test as v1 home/courses endpoint was removed from master 33b3480,
During rebase, this test was missed.
Follow-up from PR #38773 review: Xblock v1 previously had no OpenAPI
schema decoration, which was flagged as a pre-existing gap. This adds
drf-spectacular @extend_schema to all five viewset actions (create,
retrieve, update, partial_update, destroy) so the endpoints show up
with proper request/response bodies, path/query parameters, and error
codes in the generated OpenAPI 3.x schema.

Notes:
- Documents the ADR 0036 ``?view=minimal`` query parameter in the
  schema (enum=["minimal"]) so consumers can discover it.
- Documents the legacy ``?fields=`` selector (graderType /
  ancestorInfo / customReadToken) as ``deprecated=True`` — its
  semantics are pass-through / type-of-response, not the ADR 0036
  CSV subset convention.
- Shared parameter and response building blocks are declared at
  module scope to keep the per-action decorators readable.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat: fix OpenAPI schemas for SDK compatibility

Three changes driven by integration testing with openedx-platform-sdk:

1. authoring_grading serializer — expose grade_cutoffs, grace_period,
   and minimum_grade_credit as proper typed schema fields.
   CourseGradingModel.update_from_json requires all three, but they were
   absent from the serializer so the generated SDK had no typed fields for
   them, forcing callers to smuggle values in via additional_properties.
   Adds GracePeriodSerializer (hours / minutes / seconds) and documents
   that grade_cutoffs and grace_period must be present in every PATCH.

2. course_details serializer — mark certificate_available_date as
   allow_null=True. The field is legitimately null for many courses, but
   the missing flag caused the SDK client to crash with
   fromisoformat(None) when deserialising the response.

3. v4 HomeCoursesViewSet — add _HomeCoursesAutoSchema (same pattern as
   the existing v3 _HomeAutoSchema). The viewset builds its paginator
   manually inside list() rather than via pagination_class, so
   drf-spectacular's _is_list_view() returns True and generates an array
   schema. Overriding _is_list_view() for the list action and using an
   inline_serializer makes the schema reflect the actual paginated object
   shape (count, num_pages, current_page, start, next, previous,
   results).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: sort imports in v4 home view to satisfy ruff I001

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: wrap long lines in v4 home view inline_serializer fields

Fixes pylint C0301 (line-too-long) — lines 175 and 177 exceeded the
120-character limit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#38849)

* feat: add drf-spectacular on LMS for enrollment API schema generation

Adds drf-spectacular support to LMS so the openedx-platform-sdk can
generate enrollment v2 API bindings from a proper LMS schema rather than
mounting enrollment URLs in CMS as a workaround.

Changes:
- openedx/envs/common.py: add DEFAULT_SCHEMA_CLASS to REST_FRAMEWORK so
  drf-spectacular's AutoSchema is used across all LMS environments
- lms/envs/common.py: add drf_spectacular to INSTALLED_APPS
- lms/lib/spectacular.py: preprocessing hook that filters to enrollment
  v2 endpoints only (/api/enrollment/v\d+/)
- lms/envs/devstack.py, lms/envs/production.py: SPECTACULAR_SETTINGS with
  path prefix trim and enrollment-only title
- lms/urls.py: expose schema at /lms-api/schema/ via SpectacularAPIView
- cms/lib/spectacular.py: revert enrollment URL workaround (no longer needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: move settings to common.py

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…lution

ENABLE_MKTG_SITE was a feature flag that selected between two URL resolution
strategies: MKTG_URLS (external marketing site) vs MKTG_URL_LINK_MAP (local
Django URL reversal). The local-template path is now legacy; all operators
are expected to use MKTG_URLS.

This commit removes the flag and its conditional branches, keeping the
MKTG_URLS-based behaviour unconditionally:

- Drop the setting definition and toggle annotation from openedx/envs/common.py
- Remove the override from lms/envs/devstack.py and both mock.yml files
- Simplify marketing_link() and is_marketing_link_set() in edxmako/shortcuts.py
  to always consult MKTG_URLS; drop the MKTG_URL_LINK_MAP fallback branches and
  the now-unused NoReverseMatch/reverse/set_custom_attribute/get_current_request_hostname imports
- Update validate_marketing_site_setting() in checks.py to always validate
  MKTG_URLS rather than gating validation on ENABLE_MKTG_SITE
- Trim edxmako/tests.py: remove False-path test variants and the MKTG_URL_LINK_MAP
  link-reversal test; each remaining test now exercises a single, unconditional code path

Part of #37053

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
index() (served at /) now unconditionally checks MKTG_URLS['ROOT'] and
redirects when it differs from LMS_ROOT_URL, matching the previous
ENABLE_MKTG_SITE=True behaviour. The catalog MFE redirect
(ENABLE_CATALOG_MICROFRONTEND) still fires first and is unaffected.
Also fixes a pre-existing bug where log.error() in the NoReverseMatch
handler was passed multiple f-strings as positional args, causing a
TypeError when that branch was reached.

courses() (served at /courses) now resolves in priority order:
1. Redirect to the catalog MFE if ENABLE_CATALOG_MICROFRONTEND is set
2. Permanent redirect to marketing_link('COURSES') from MKTG_URLS
3. Render the local course list via courseware.views.views.courses()

The old ENABLE_MKTG_SITE gate and COURSES_ARE_BROWSABLE guard are
removed. The local course list fallback (case 3) preserves behaviour
for operators who have neither the catalog MFE nor MKTG_URLS['COURSES']
configured.

The stanford-style theme footer is updated to use marketing_link()
instead of reverse() for the 'about' and 'tos' URL routes, which are
removed in a later commit.

Tests drop their @override_settings(ENABLE_MKTG_SITE=True) decorators;
the test names and assertions are otherwise unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… API

- course_about_url in enrollment emails now uses
  get_link_for_about_page(), resolving the marketing URL, social sharing
  URL, or LMS about URL as a fallback
- get_link_for_about_page() uses course.marketing_url unconditionally
- has_marketing_url() returns bool(self.marketing_url) directly
- url_to_enroll() always returns marketing_link('COURSES')
- _course_home_redirect_enabled() only checks ENABLE_COURSE_HOME_REDIRECT;
  default changed from True to False because the flag previously had no
  effect when ENABLE_MKTG_SITE=False (the old default) — operators who
  relied on ENABLE_MKTG_SITE=True to activate the course-about redirect
  must now set ENABLE_COURSE_HOME_REDIRECT=True explicitly
- Remove ENABLE_MKTG_SITE patches from courseware, instructor, and
  mobile API tests; merge redundant test cases where applicable

BREAKING CHANGE: ENABLE_COURSE_HOME_REDIRECT now defaults to False.
Operators who previously had ENABLE_MKTG_SITE=True and want the
course about page to redirect to the learning MFE must explicitly set
ENABLE_COURSE_HOME_REDIRECT=True in their configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Email change confirmation contact link now always comes from
marketing_link('CONTACT') via MKTG_URLS. Remove the fallback that
constructed a local URL when the flag was False.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- contentstore utils: marketing_enabled is always True
- registration tests: remove ENABLE_MKTG_SITE=False test cases that
  tested now-dead local URL generation for honor code and TOS links;
  remove ENABLE_MKTG_SITE=True decorators from the remaining tests
- site_configuration test mixin: remove ENABLE_MKTG_SITE example key
- schedules README: remove ENABLE_MKTG_SITE from example config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Marketing navigation links (HOW_IT_WORKS, COURSES, SCHOOLS) are now
always rendered in the unauthenticated nav bars. marketing_link() falls
back to '#' when a URL is not configured in MKTG_URLS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This setting was the fallback URL resolution mechanism used when
ENABLE_MKTG_SITE=False. Now that the flag is gone, only MKTG_URLS is
used for marketing link resolution.

- Remove MKTG_URL_LINK_MAP definition from openedx/envs/common.py
- Remove LMS-specific update block from lms/envs/common.py
- Remove test environment override from lms/envs/test.py
- Remove YAML loading and dict-update from lms and cms production envs
- Remove the dynamic URL registration loop from static_template_view
- Remove MKTG_URL_LINK_MAP.keys() union from the marketing link
  context processor in edxmako/shortcuts.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the 14 template files that were served locally when
ENABLE_MKTG_SITE=False. These pages (about, blog, contact, donate, faq,
help, honor, jobs, media-kit, news, press, privacy, tos, sitemap.xml)
should now only live on the external marketing site configured via
MKTG_URLS.

Also remove the stanford-style theme overrides for about.html and
tos.html, the hardcoded URL patterns for the deleted templates, the
honor.html iframing special case from the render view, and the tests
that exercised these now-deleted routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The E003 check required MKTG_URLS['ROOT'] to be defined, but this key
is optional. The index() view in branding/views.py already handles the
case where ROOT is absent:

    root_url = marketing_urls.get("ROOT")
    if root_url and root_url != getattr(settings, "LMS_ROOT_URL", None):
        return redirect(root_url)

Previously, E003 was only triggered when ENABLE_MKTG_SITE=True (the
check was guarded by that flag). After removing ENABLE_MKTG_SITE in
#38720, the check runs
unconditionally and fires in any environment where MKTG_URLS has no
ROOT key (including test and CMS environments). Since ROOT is genuinely
optional, the check is incorrect and should not be enforced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ails

With ENABLE_MKTG_SITE removed, get_email_params() now always sets
course_about_url via get_link_for_about_page(), which never returns None
(it falls back to the LMS course about URL). The instructor "allowed
enroll" and "add beta tester" ACE email templates branched on
`course_about_url is not None`, keeping an else-branch for the old
marketing-site-enabled case where course_about_url was None.

Those conditionals are now always true and their else-branches are dead:
- allowedenroll: "You can then enroll in {{ course_name }}."
- addbetatester: "Visit {{ site_name }} to enroll in this course and
  begin the beta test."

Collapse the always-true `course_about_url is not None` guards to the
unconditional branch and delete the unreachable text (both .txt and .html
variants). No behavior change for any reachable input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit d83c598 added display_number and display_org to
CourseCommonSerializerV2 (which CourseHomeTabSerializerV4 aliases),
but only updated the v2 test. The v4 test_home_page_response was
left expecting the old 10-field format and now fails.
rpenido and others added 30 commits October 2, 2026 15:45
…issing (#39152)

The clipboard holds a *copy* of a block, so its XBlock plugin may no
longer be installed (e.g. a custom XBlock was removed from the
platform).  StagedContentSerializer.get_block_type_display() called
xblock_type_display_name(), which raises PluginMissingError in that
case, so reading the clipboard raised, and because the clipboard is
embedded in the course index response (initial_user_clipboard), the
whole course index request 500s -- for every course, for any user who
happens to have such a clipboard.  The same applies to the
/api/content-staging/v1/clipboard/ endpoint, and there is no API to
discard the stale clipboard, so authors get stuck.

Fall back to the raw block type, mirroring
openedx.core.djangoapps.xblock.api.xblock_type_display_name, which
already handles a missing plugin this way.
* refactor: migrate direct drf-yasg usage to drf-spectacular

Converts @swagger_auto_schema to @extend_schema in the five modules that
import drf_yasg directly, following the drf-spectacular migration guide:
https://drf-spectacular.readthedocs.io/en/latest/drf_yasg.html

Structured openapi.Schema objects become inline_serializer so they get
named components in the generated schema; untyped ones become
OpenApiTypes.OBJECT. openapi.Parameter becomes OpenApiParameter.

The other four drf_yasg users go through edx-api-doc-tools and will be
migrated with it.

* refactor: migrate openedx/core from edx-api-doc-tools to drf-spectacular

Converts @apidocs.schema to @extend_schema across openedx/core, replacing
the parameter helpers with OpenApiParameter and string response
descriptions with OpenApiResponse.

bookmarks/serializers.py inlines is_schema_request, which has no
drf-spectacular equivalent, and extends it to recognise drf-spectacular's
swagger_fake_view alongside drf-yasg's format=openapi.

* refactor: migrate lms from edx-api-doc-tools to drf-spectacular

Converts @apidocs.schema and @Schema to @extend_schema across the lms
app, excluding instructor. Parameter helpers become OpenApiParameter and
string response descriptions become OpenApiResponse.

discussion/rest_api/views.py also drops its remaining direct drf_yasg
import, which was interleaved with the apidocs decorators.

* refactor: migrate instructor from edx-api-doc-tools to drf-spectacular

Converts the 26 @apidocs.schema decorators in the instructor v1 and v2
APIs to @extend_schema.

The course_id, problem, and exam_id path parameters were repeated
verbatim across 29 decorators; those are now module-level constants.

* refactor: migrate cms from edx-api-doc-tools to drf-spectacular

Converts the remaining @apidocs.schema decorators across contentstore
and modulestore_migrator to @extend_schema. The three class-level
@apidocs.schema_for decorators become @extend_schema_view, splitting
each docstring into summary and description as schema_for did.

Files that already imported drf-spectacular for the FC-0118 work have
their import lines merged rather than duplicated.

* feat: add drf-spectacular extensions for BaseSerializer subclasses

Six serializers subclass BaseSerializer, which has no `fields` attribute,
so drf-spectacular raises AttributeError when generating a schema that
covers them. Each extension declares the type its serializer produces.

Registered from CommonInitializationConfig.ready() so they load in both
services regardless of which schema is being generated.

* refactor: serve /api-docs with drf-spectacular

Replaces make_docs_urls with SpectacularAPIView, SpectacularSwaggerView
and SpectacularRedocView, preserving the swagger.json, swagger.yaml,
api-docs/ and swagger/ routes and their URL names. The UI views reverse
their schema URL without arguments, so api-docs/schema/ is registered
alongside the format-suffixed routes.

/api-docs serves the full API surface via custom_settings, leaving
SPECTACULAR_SETTINGS to the narrower Authoring and Enrollment schemas
the SDK consumes.

Also removes drf_yasg from INSTALLED_APPS, drops SWAGGER_SETTINGS, and
converts the docs security definitions to OpenAPI 3 form. `make swagger`
now runs `manage.py lms spectacular`, since generate_swagger came from
drf_yasg; docs_settings applies the same unfiltered configuration as
/api-docs so the generated file still covers the whole surface.

edx-api-doc-tools and drf-yasg remain installed as transitive
dependencies of openedx-authz and django-user-tasks respectively.

* fix: resolve review comments

- restore server-side caching on the OpenAPI schema endpoints, which
  edx-api-doc-tools provided via SchemaView.as_cached_view
- point the api-docs test at /api-docs/schema/ so it exercises schema
  generation again, and add the CMS equivalent
- serve Swagger UI and ReDoc assets from drf-spectacular-sidecar instead
  of drf-spectacular's unpinned jsdelivr CDN defaults
- correct the /api-docs comment: edx-api-doc-tools was /api/-only, so
  this widens the documented surface rather than being "the opposite"
- drop the LMS-only claim about API_ACCESS_MANAGER_EMAIL, which lives in
  openedx/envs/common.py and is shared by both services
- point the schema_extensions docstring at CommonInitializationConfig,
  the actual registration site
- remove the dead format=openapi branch from is_schema_request, since
  nothing in the platform serves drf-yasg any more
- delete the now-empty Django Rest Framework banner in lms/envs/common.py

* fix: cache the api-docs schema compressed

cache_page pickles the whole Response, and this schema is over memcached's
1MB item limit. CACHES['default'] has ignore_exc set, so the oversized set
failed silently and Django dropped the key — the endpoint looked cached while
regenerating on every request.

cached_schema_view() stores the zlib-compressed body instead, keyed on the
path and the negotiated representation, which brings it to about a tenth of
the limit. add_never_cache_headers restores the no-store that drf-yasg sent,
and a falsy OPENAPI_CACHE_TIMEOUT still goes straight to the view so devstack
is unchanged.

* fix: key the api-docs schema cache on method and language

The cached branch stored anything that came back 200, so DRF's OPTIONS
metadata document landed under the GET key and was served to the next GET.
cache_page only ever stored GET and HEAD; do the same.

drf-spectacular renders the document under whatever language is active, and
LocaleMiddleware sets that from the request before the view runs, so one
locale's document could be served to another's. Add the active language to
the key.

Adds a test to both suites for the two cases. The suite runs with
OPENAPI_CACHE_TIMEOUT = 0 and a DummyCache, so it overrides both to reach the
cached branch at all.

* fix: key the api-docs schema cache per service

The LMS and the CMS serve this view at the same path, under the same Accept
and language, and share one cache by default, so both landed on the same key
and whichever generated first served its document at the other's URL.
cache_page avoided this by hashing build_absolute_uri(), host included.

Add ROOT_URLCONF to the key.

* test: cover the URLconf in the api-docs schema cache key

LMS and CMS serve /api-docs/schema/ at the same path and share one cache, so
the key must include ROOT_URLCONF. The existing cache test runs under a single
URLconf and could not detect that line being dropped; this test serves the view
from a second URLconf and fails without it.
* feat: add GH workflow to generate openapi schema

Add GH workflow to automatically generate openapi schema whenever view file tagged with the "openedx-platform-sdk" @extend_schema tag changes

* feat: address comments

* fix: address comments on generate schemas workflow

address comments on generate_openapi_schemas workflow
- weekly schedule
- use team-reviewers param

* fix: write the generated schemas to docs/

- write to docs/lms-openapi.yaml and docs/cms-openapi.yaml instead of
  creating new schema files at the repo root
- generate the LMS schema under docs.docs_settings so the workflow writes
  the same full API surface `make swagger` does, rather than overwriting
  the docs schema with the narrow SDK-facing one

* fix: address comments on the schema generation workflow

uv sync installed no groups, so ora2 was missing and both schema steps died
with ModuleNotFoundError. Use the docs group, which pulls in bundled, the same
set .readthedocs.yaml installs.

manage.py cms defaults to cms.envs.devstack, which needs a CMS_CFG file CI
does not have. Run it under cms.envs.development, and give cms/envs/common.py
the schema title and version — the Authoring API's SPECTACULAR_SETTINGS lives
in devstack and production, so development would otherwise emit an untitled
0.0.0 document.

Also align the setup-uv pin with the other workflows, and add branch-suffix
and workflow_ref provenance to match the other PR-opening ones.

* fix: give the schema job a database and the CMS schema its filtering

Generation needs migrated tables, not just a server: drf-spectacular evaluates
a queryset while building a warning for edxval's VideoList. Add the mysql
service and a migrate step the way migrations-check.yml does.

cms.envs.development inherited the Authoring API's title and version but none
of its filtering, so docs/cms-openapi.yaml came out at 235 paths with the rest
of the service in it. Only SERVERS and the long DESCRIPTION depend on CMS_BASE
and AUTHORING_API_URL, so the hooks and the path prefix move down to
cms/envs/common.py as well. 57 paths.

* fix: give the migrate steps mongo and their settings

split_modulestore_django's 0002_data_migration reads the modulestore, so
migrating from empty needs Mongo even though generation doesn't. Add the
service and the user setup step from migrations-check.yml.

manage.py falls back to devstack without DJANGO_SETTINGS_MODULE, and devstack's
DATABASES is empty without an LMS_CFG, so both migrate steps failed before
reaching the service. Run each under the same settings as its generate step.
Commit generated by workflow `openedx/openedx-platform/.github/workflows/generate_openapi_schemas.yml@refs/heads/master`
…am-reviewers

fix: use the requirements bot token to open the OpenAPI schema PR
…8d1866

chore: update OpenAPI schemas for SDK generation
…39156)

Several log statements uses usernames, emails, or full
names without checking SQUELCH_PII_IN_LOGS, so PII reached the logs
even though LMS enables the setting. These now log the
numeric user ID when the setting is on. When it is off, the log output
is unchanged.

Adds helpers to log_utils so the check lives in one place. 
Logging user.id directly is always considered safe; the helpers are only
needed where the username or email is preferred in deployments that allow
PII in logs.
Discussion topic titles come from course content: a unit's display name,
or "section|subsection|unit" when a unit is no longer discussable. Either
can exceed DiscussionTopicLink.title's 255-character limit. On strict
MySQL that raises a DataError, and because the sync runs in a single
transaction (and the error is swallowed by send_robust), one oversized
title silently blocks the entire course's topic sync: new units get no
topics and removed units are never disabled.

Truncate the title wherever the sync sets it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…39192)

CourseEnrollmentsApiListSerializer's Meta subclasses its parent's, so it
inherited ref_name = "CourseEnrollment". Both serializers were published
under one schema component, and in the full /api-docs schema the subset
serializer won: the enrollment create and retrieve endpoints were documented
with course_id instead of course_details, and clients generated from the
schema were typed accordingly.
Commit generated by workflow `openedx/openedx-platform/.github/workflows/generate_openapi_schemas.yml@refs/heads/master`
Guard the LMS "Skip to main content" link in main.html behind
`not disable_header`, so it renders only on full-chrome pages and is
omitted from chromeless/iframed contexts (Learning MFE render_xblock,
LTI provider, plugin_api fragments) where no in-page header exists to
skip to and a global skip link is already present.
Have the CCX Coach v2 metadata endpoint include the student_grades tab
only when the writable gradebook is available for the CCX — both the
grades.writable_gradebook waffle flag (evaluated against the CCX key) and
the WRITABLE_GRADEBOOK_URL setting (read through site configuration).
…9a94d1

chore: update OpenAPI schemas for SDK generation
* feat: apply FC-0118 ADRs to youtube transcripts APIs

Add a conforming v1 surface for the two YouTube transcript endpoints at
/api/authoring/v1/courses/{course_key}/youtube_transcript_{checks,imports}/.
The import operation becomes a POST because the legacy GET performs four
domain writes. Responses are serializer-produced and errors use the
standardized envelope. The v0 routes are untouched and keep resolving at
all four of their live spellings; they are marked deprecated in the schema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review feedback on the youtube transcripts APIs

- Publish full paths from cms/envs/common.py as well. cms.envs.development
  inherits these settings to generate the committed docs/cms-openapi.yaml,
  which otherwise mixed trimmed /v0/... paths with untrimmed
  /api/authoring/v1/... ones and marked nothing deprecated. Drop
  SCHEMA_PATH_PREFIX_TRIM, widen SCHEMA_PATH_PREFIX to both mounts, and add
  the hook that marks the superseded v0 paths deprecated.
- Require a "youtube" entry in the import's videos, so a missing one is
  reported on videos rather than as a failure on locator.
- Test a locator that is neither a course nor a V2 library block, which the
  handlers reject with TranscriptsRequestValidationException.

* fix: stop registering error types in the youtube transcripts service

The registry in edx_rest_framework_extensions.errors is process-wide, so
registering MethodNotAllowed and ParseError here changed the error type of
every standardized Studio endpoint, not only these two. Left to an
edx-drf-extensions follow-up, as in #39102; the 405 tests assert the status
only.

* docs: note why a YouTube failure is a 400 in the transcripts service

A failure on YouTube's side is reported as a 400, matching v0. It can
become a 502 once edx-drf-extensions registers an error type for
upstream failures.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: apply FC-0118 ADRs to course content APIs

Add a conforming v2 surface for the textbooks and video-settings
endpoints at /api/authoring/v2/courses/{course_key}/{textbooks,video_settings}/.
The video endpoint's read path no longer reconciles stale upload status,
fixing a runtime-verified write-on-GET bug; that reconciliation stays
on the unchanged legacy route. The v1 routes are untouched and keep
resolving exactly as they do today.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: address review feedback on the course content APIs

- Publish full paths from cms/envs/common.py as well. cms.envs.development
  inherits these settings to generate the committed docs/cms-openapi.yaml,
  which otherwise mixed trimmed paths (/v1/textbooks/{course_id}) with full
  ones (/api/authoring/v2/courses/{course_key}/textbooks/). Drop
  SCHEMA_PATH_PREFIX_TRIM and widen SCHEMA_PATH_PREFIX to both mounts.
- Note when CourseTextbookPagination can be removed in favour of
  DefaultPagination.

* fix: address second round of review feedback on the course content APIs

- Stop registering MethodNotAllowed and NotAcceptable error types. The
  registry in edx_rest_framework_extensions.errors is process-wide, so the
  registrations changed the error type of every standardized Studio endpoint.
  Left to an edx-drf-extensions follow-up, as in #39102.
- Serve stored textbook chapters missing title or url, or holding null for
  either, with an empty string, and a stored null chapter list as an empty
  list, instead of a 500 and a null the schema does not allow.
- Drop ?view=full and previous_uploads from video_settings. #39102 adds a
  paginated videos/ collection with the same uploads and status logic, and
  nothing calls the full view, which was new in v2.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The start_date_on_or_after and start_date_on_or_before params of
GET /api/contentstore/v2/home/courses take a full ISO 8601 datetime with
a UTC offset or Z, for example 2027-07-01T00:00:00+04:00. The filter
compares them to CourseOverview.start as instants, inclusive at both
ends, so the caller decides where its own calendar day begins and ends.
A user east or west of UTC now finds the courses Studio shows on the day
they picked.

The params used to take a YYYY-MM-DD date that the backend treated as a
UTC day. A user in another time zone who picked a local day could miss a
course Studio shows on that day: a course starting at 2027-06-30T22:00Z
is July 1 in UTC+4, but the old filter only returned it for June 30.

Naive datetimes, bare dates, and unparseable values return 400 with a
message that includes the received value and asks for an offset. The
backend does not assume UTC for a naive value, because that would
reproduce the original bug without any visible error.

A literal + in a query string decodes to a space, so a client that does
not encode an offset sends "10:00:00 04:00". A space directly before a
trailing HH:MM or HHMM offset is read as +, so that request still works.
Clients should still percent-encode + as %2B.

Both params are parsed before the early return in
get_courses_accessible_to_user, so an invalid value gives a 400 whether
or not the user has accessible courses.

The filter only landed on master after the last named release, so this
is a fix and not a deprecation. Clients that send YYYY-MM-DD now get a
400, which includes the Competency Management course search until it
sends offsets.

Related to openedx/openedx-core#842

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.1.0 to 10.2.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@v10.1.0...c18668a)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…dized APIs (#39078)

* feat: wire shared opaque-key URL path converters from edx-drf-extensions (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.

* feat: apply ADR 0038 to Xblock v1 (/api/authoring/v1/xblocks/)

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.

* feat: apply ADR 0038 to CourseHome v3 (/api/authoring/v3/home/, x-internal 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.

* feat: apply ADR 0038 to CourseHome v4 (/api/authoring/v4/courses/)

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.

* feat: apply ADR 0038 to Course Detail v3 (courses/{course_key}/details/)

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.

* feat: apply ADR 0038 to AuthorGrading v3 (courses/{course_key}/grading/)

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.

* feat: apply ADR 0038 to Enrollment v2 (required trailing slashes + conforming 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.

* chore: reduce redundant docstrings and comments across the ADR 0038 migration

Review feedback: the new authoring_urls.py module docstrings restated ADR 0038's
rules rather than describing the module, and several comments repeated what the
commits already say. The three urls.py modules now carry a one-line docstring
matching their siblings in the same package, the per-rule conformance lists and
the "same view, same contract" notes are gone, and the test-class docstrings,
Enrollment v2 docstring, and spectacular helpers are trimmed; comments that
prevent a mistake are kept but shortened (conforming routes pass a parsed
CourseKey/UsageKey where legacy routes pass the raw string; POSTPROCESSING_HOOKS
replaces rather than extends drf-spectacular's default list). Prose only — with
docstrings stripped, all 19 files parse to ASTs identical to the previous
revision, ruff passes, and no view or serializer docstring is touched.

* chore: drop ADR and OEP names from the code this branch adds

Review feedback on #39037: the code should follow the standards without naming
the documents. The three authoring urls.py modules now read "Authoring API vN
URLs.", the URL-structure test banners and the Enrollment v2 module docstring
lose their rule references, and the remaining comments say what the code does
instead — deprecation window rather than OEP-21 window, "whose course_key path
converter hands views a parsed key" rather than a rule number. Only lines this
branch adds are touched; the pre-existing ADR references elsewhere in these
files are left alone, the one exception being the ADR 0028 line in the
Enrollment v2 module docstring, which this branch was already rewriting.

Prose only apart from one assert message in the new URL-structure test, which
now reads "missing error-envelope field". ruff passes.

* fix: emit full paths in the OpenAPI schemas instead of trimming the prefix

SCHEMA_PATH_PREFIX_TRIM is a boolean that we had set to a string in three
places; it only worked because the string was truthy. More importantly the
trimming itself produced wrong URLs: the LMS spec advertised /v2/enrollments/
against a bare LMS_ROOT_URL server, and adding /api/authoring/ to the CMS spec
left those paths untrimmed beside trimmed contentstore ones. Widening the
prefix regex would collapse /api/contentstore/v3/home/ and
/api/authoring/v3/home/ onto one key. Drop the trim, drop the CMS-contentstore
server that existed only to compensate, keep SCHEMA_PATH_PREFIX for tag
extraction, and update both post-processing hooks to full paths. Adds tests
for the hooks, which had none.

* fix: deterministic operationIds, restore removed URL names, compare view actions

Review feedback on the dual mounts. A legacy mount and its conforming mount
serve the same view and tokenize to the same operationId, so drf-spectacular
was breaking the tie with a numeral suffix in registration order: the CMS
schema put _2 on the legacy path and the LMS schema put it on the conforming
one, so regenerating the SDK would have renamed modules by accident. Each
service now has an AutoSchema subclass wired in as DEFAULT_SCHEMA_CLASS that
suffixes the legacy address with _legacy and leaves the conforming address
with the clean id, so a regenerated SDK follows the same function name onto
the non-deprecated address. The two Studio home viewsets carry their own
schema instances, which bypass the default, so those are rebased onto the CMS
class as well; tests pin the ids for each pair, assert the default wiring,
and assert the per-view schemas chain the CMS class. The LMS legacy predicate
is shared between the schema class and the deprecation hook.

Two URL names had been dropped although every address was kept:
enrollment-v2-roles and the course-only form of enrollment-v2-retrieve. Both
are registered again on their paths after the new names, so reverse() keeps
working for out-of-tree callers and resolution stays on the first entry.

The route-sharing tests compared .func.cls, which is the same object for a
ViewSet whatever actions a mount wires up; they now compare .func.actions as
well, and the v1 test covers the list route too.

* fix: suffix only colliding legacy operationIds; ignore DRF's head in route tests

Only the legacy /api/contentstore/v3/home/ paths and the slashless
/api/enrollment/v2/enrollments tokenize to the same operationId as their
conforming twins, so only they get the _legacy suffix. Every other legacy
operation keeps its existing operationId, and with it the SDK module
generated from it.

DRF adds a head entry to a mount's actions the first time it serves a GET,
so the route-equivalence tests compare the action maps through
routed_actions(), which drops it.

The SCHEMA_PATH_PREFIX comment now says the prefix also derives every
operationId, not just the tags.

* fix: answer 404 for version-only course keys on the conforming routes

The shared course_key converter accepts keys that name only a version
(course-v1:version@..., ccx-v1:version@...+ccx@...). Looking one up raises
InvalidKeyError in CourseKeyField, so the conforming v3 details and grading
routes and the enrollment v2 courses/ and enrollments/{username},{course_key}/
routes answered 500 to every caller. The legacy routes never see these keys:
COURSE_ID_PATTERN rejects them at routing.

resolve_course_key and the enrollment views now treat a key without an org
as not found.

* refactor: use the parsed opaque key from the conforming routes directly

The conforming routes' path converters hand views a parsed key, which
XblockViewSet.initial() and the two enrollment v2 views turned back into a
string only to parse it again. They now use the parsed key as is and parse
only the legacy routes' raw string. XblockViewSet still passes the string to
its actions, which parse it themselves.
* chore: bump openedx-forum to 0.4.8

Picks up openedx/forum#292, which fixes answered questions losing
their endorsed state in MySQL-backed thread lists.

* chore: lock openedx-forum 0.4.8 in uv.lock

---------

Co-authored-by: Peter Pinch <pdpinch@mit.edu>
* fix: give each course import its own working directory

Every import of a given course used a single working directory derived
from the course key alone, shared by the import task and by both upload
views. Two imports of the same course therefore overwrote each other's
archive, and whichever import finished first deleted the other's
extracted OLX mid-run. The surviving task failed with a bare filesystem
error: "[Errno 116] Stale file handle" on an NFS-backed
GITHUB_REPO_ROOT, "[Errno 2]" on a local disk.

Add course_import_working_dir() and remove_course_import_working_dir(),
and use them from import_olx and from both upload views so that every
import and every upload gets a private subdirectory. Both upload views
now drop their staging copy once the archive is in storage; previously
they relied on the import task's cleanup of the shared directory.

Cleanup no longer raises, so a failing cleanup can no longer mask the
error that triggered it.

* fix: rename test helper arg that shadowed the rest_framework status import

* fix: keep course import cleanup safe for concurrent and repeated uploads

Put each import working directory directly under GITHUB_REPO_ROOT instead of
under a shared per-course parent. Removing that parent once it looked empty
could race another import of the same course that had already checked the
parent existed, making its mkdir fail with FileNotFoundError.

Remember a finished chunked upload in the cache before deleting its staging
file. The last chunk is sometimes sent twice (nginx 499); the repeat used to
find the file at full size, but now that the staging copy is removed
straight away it was rejected with a 409 "Some chunks missed" even though the
import had already started.

---------

Co-authored-by: Peter Pinch <pdpinch@mit.edu>
Regenerated via `make upgrade-package package=openedx-core`. Unblocks QA on
the Competency-Based Education (CBE) work this release carries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Updates openedx-core version to 1.11.0

---------

Co-authored-by: javoconsultant <jmeliev_consultant@flex.unicon.net>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…sView` (#39196)

* fix: enforce courses.manage_pages_and_resources on ProctoredExamSettingsView

GET and POST on this endpoint only required studio read access, so a
user who could view a course in Studio could also read or overwrite
its proctored exam settings by calling the API directly, bypassing
the UI's staff-only enforcement. Add the same authz check used
elsewhere for pages-and-resources management, with a write-level
legacy fallback when authz is disabled.

Closes openedx-authz#443

* fix: use AuthZ view/manage permissions for proctored exam settings

Make the AuthZ check the only gate for the proctored exam settings
endpoint instead of stacking it on top of the legacy Studio read check:

- GET requires courses.view_pages_and_resources and POST requires
  courses.manage_pages_and_resources. Without AuthZ, both fall back to
  legacy Studio read access, as before.
- POST checks access before validating the payload.
- Users with is_staff, or with the manage permission when AuthZ is
  enabled, can edit the fields reserved to edX staff.

Replace the mocked AuthZ tests with tests that assign real roles.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Kevyn Suarez <kevyn.suarez@edunext.co>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Security release of the forum app for GHSA-c6fp-f54c-6cv4: the forum HTTP
API is no longer routed unless FORUM_ENABLE_HTTP_API is set, and requires
a staff caller when it is.

GHSA-c6fp-f54c-6cv4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al-sh/setup-uv-10.2.0

build(deps): bump astral-sh/setup-uv from 10.1.0 to 10.2.0
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.