From cb05bd1827fed7719c084823a56b7a509ccf701b Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 11 Mar 2025 12:26:53 -0400 Subject: [PATCH 1/3] feat: Add a new development settings file. Base it directly off of common and try to keep it as small as possible. Regarding plugin loading: Ideally the name of this setting would be "DEVELOPMENT" and not tied to the devstack but since that's an interface change, we use the devstack settings which should be what is setup for development. Regarding the webpack loader settings: WEBPACK_LOADER's STATS_FILE is derived from STATIC_ROOT in openedx/envs/common.py. In the new development.py settings the base STATIC_ROOT default is ENV_ROOT/staticfiles (LMS) and ENV_ROOT/staticfiles/studio (CMS), which point *outside* the repo. That is not where the webpack build writes its stats manifest: with STATIC_ROOT_LMS / STATIC_ROOT_CMS unset, webpack.common.config.js falls back to ./test_root/staticfiles (and .../studio for CMS). As a result render_bundle raised "Error reading .../staticfiles/webpack-stats.json" on every page that renders a webpack bundle. Point STATIC_ROOT at the in-repo test_root location so the derived STATS_FILE lands where webpack actually writes it. This makes the Python default agree with webpack's JS default with no STATIC_ROOT_LMS / STATIC_ROOT_CMS wiring, and mirrors what the test settings already do (openedx/envs/test.py). This is only about locating the stats manifest -- collectstatic is not part of the development flow. With DEBUG=True the staticfiles finders serve assets directly from their source dirs (e.g. the bundles in common/static/bundles), so nothing writes into STATIC_ROOT during normal development. --- cms/envs/common.py | 9 +- cms/envs/development.py | 127 ++++++++++++++++++++++++++++ lms/envs/development.py | 178 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 cms/envs/development.py create mode 100644 lms/envs/development.py diff --git a/cms/envs/common.py b/cms/envs/common.py index 4d7f4d5d26e9..fbd535ff712d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -276,8 +276,15 @@ ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/cms +REPO_ROOT = PROJECT_ROOT.dirname() +COMMON_ROOT = REPO_ROOT / "common" +OPENEDX_ROOT = REPO_ROOT / "openedx" CMS_ROOT = REPO_ROOT / "cms" # noqa: F405 LMS_ROOT = REPO_ROOT / "lms" # noqa: F405 +ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /edx-platform is in +COURSES_ROOT = ENV_ROOT / "data" +XMODULE_ROOT = REPO_ROOT / "xmodule" +MEDIA_ROOT = ENV_ROOT / "media_root" GITHUB_REPO_ROOT = ENV_ROOT / "data" # noqa: F405 @@ -1129,7 +1136,7 @@ def make_lms_template_path(settings): # STORAGE_CLASS='storages.backends.s3boto3.S3Boto3Storage', # STORAGE_KWARGS=dict(bucket='video-image-bucket'), STORAGE_KWARGS=dict( - location=MEDIA_ROOT, # noqa: F405 + location=Derived(lambda settings: settings.MEDIA_ROOT), ), DIRECTORY_PREFIX='video-images/', BASE_URL=MEDIA_URL, # noqa: F405 diff --git a/cms/envs/development.py b/cms/envs/development.py new file mode 100644 index 000000000000..8ae563abeb59 --- /dev/null +++ b/cms/envs/development.py @@ -0,0 +1,127 @@ +""" +This settings file is optimized for local development. It should work equally well for bare-metal development and for +running inside of development environments such as tutor. + +This file is currently in development itself and so may not work for everyone out of the box. More updates, including +updated documentation will be added as we get closer to removing devstack.py +""" + +#Helpers for loading plugins and their settings. +from edx_django_utils.plugins import add_plugins + +from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType +from openedx.core.lib.derived import derive_settings + +# Use the common file as the starting point. +# pylint: disable=wildcard-import +from .common import * # noqa: F403 + +DEBUG = True + +STORAGES['default']['BACKEND'] = 'django.core.files.storage.FileSystemStorage' # noqa: F405 +STORAGES['staticfiles']['BACKEND'] = 'openedx.core.storage.DevelopmentStorage' # noqa: F405 + +# Disable pipeline compression in development +PIPELINE['PIPELINE_ENABLED'] = False # noqa: F405 + +# Revert to the default set of finders as we don't want the production pipeline +STATICFILES_FINDERS = [ + 'openedx.core.djangoapps.theming.finders.ThemeFilesFinder', + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'pipeline.finders.PipelineFinder', +] + +# Point STATIC_ROOT at test_root/staticfiles/studio so that WEBPACK_LOADER's STATS_FILE resolves +# here. STATS_FILE is derived from STATIC_ROOT (see openedx/envs/common.py), and this is the +# directory that `npm run build-dev` / `npm run watch` write the Studio webpack-stats.json into by +# default (see webpack.common.config.js, whose staticRootCms falls back to +# ./test_root/staticfiles/studio when STATIC_ROOT_CMS is unset). The base default of +# ENV_ROOT/staticfiles/studio points *outside* the repo and does not match where webpack writes, +# so the loader can't find the stats file. +# +# NOTE: This is purely so the webpack stats manifest can be located. You are NOT expected to run +# collectstatic in development -- with DEBUG=True the staticfiles finders serve assets directly +# from their source dirs (e.g. the bundles in common/static/bundles). The '/studio' suffix mirrors +# the production convention (cms/envs/production.py). +STATIC_ROOT = REPO_ROOT / 'test_root' / 'staticfiles' / 'studio' # noqa: F405 + +# Whether to run django-require in debug mode. +REQUIRE_DEBUG = DEBUG + +# Run Celery tasks synchronously in-process so local development needs no message broker or worker. +# The base default (CELERY_ALWAYS_EAGER = False, openedx/envs/common.py) makes task-enqueuing code +# paths -- e.g. the event fired on xblock creation -- try to reach a broker and 500 with +# "Connection refused". This matches the old devstack behavior. +CELERY_ALWAYS_EAGER = True + +LMS_BASE = 'local.openedx.io:8000' +LMS_ROOT_URL = f'http://{LMS_BASE}' + +CMS_BASE = 'studio.local.openedx.io:8001' +CMS_ROOT_URL = f'http://{CMS_BASE}' +ALLOWED_HOSTS = ['studio.local.openedx.io'] + +# Dealing with CORS +CORS_ALLOW_CREDENTIALS = True +# Each development MFE is served under apps.local.openedx.io on its own port. In practice the CMS +# only needs to accept cross-origin requests from the authoring MFE (Studio's frontend); the other +# MFEs talk to the LMS, not Studio. The rest are listed but commented out -- uncomment an origin if +# that MFE turns out to need to call Studio APIs directly. +CORS_ORIGIN_WHITELIST = ( + "http://apps.local.openedx.io:2001", # authoring (Studio) + # "http://apps.local.openedx.io:1984", # communications + # "http://apps.local.openedx.io:1993", # ora-grading + # "http://apps.local.openedx.io:1994", # gradebook + # "http://apps.local.openedx.io:1995", # profile + # "http://apps.local.openedx.io:1996", # learner-dashboard + # "http://apps.local.openedx.io:1997", # account + # "http://apps.local.openedx.io:1998", # catalog + # "http://apps.local.openedx.io:1999", # authn + # "http://apps.local.openedx.io:2000", # learning + # "http://apps.local.openedx.io:2002", # discussions + # "http://apps.local.openedx.io:2025", # admin-console +) + +# Unsafe (POST/PUT/DELETE) requests from the authoring MFE undergo Django's CSRF origin check, so +# the MFE origin must be trusted here or Studio rejects writes with a 403 ("Origin checking +# failed"). Scoped to the authoring MFE for the same reason as CORS_ORIGIN_WHITELIST above; +# uncomment another origin if that MFE needs to make write requests to Studio. +CSRF_TRUSTED_ORIGINS = [ + "http://apps.local.openedx.io:2001", # authoring (Studio) + # "http://apps.local.openedx.io:1984", # communications + # "http://apps.local.openedx.io:1993", # ora-grading + # "http://apps.local.openedx.io:1994", # gradebook + # "http://apps.local.openedx.io:1995", # profile + # "http://apps.local.openedx.io:1996", # learner-dashboard + # "http://apps.local.openedx.io:1997", # account + # "http://apps.local.openedx.io:1998", # catalog + # "http://apps.local.openedx.io:1999", # authn + # "http://apps.local.openedx.io:2000", # learning + # "http://apps.local.openedx.io:2002", # discussions + # "http://apps.local.openedx.io:2025", # admin-console +] + +# Cookie Related Settings +SESSION_COOKIE_DOMAIN = '.local.openedx.io' + +# MFE Development URLs +# This one needs a trailing slash to load correctly right now. +LEARNER_HOME_MICROFRONTEND_URL = 'http://apps.local.openedx.io:1996/learner-dashboard/' +# This one explicitly needs to not have a trailing slash because of how it's used to make other +# urls. +LEARNING_MICROFRONTEND_URL = "http://apps.local.openedx.io:2000/learning" + +# The course-authoring MFE (frontend-app-authoring) now serves Studio's course outline, pages & +# resources, etc. The base default is None (openedx/envs/common.py), so Studio's course_index view +# builds a redirect to None and 500s (`get_course_outline_url` -> `redirect(None)`). Point it at +# the authoring MFE, which runs on port 2001 under /authoring. +COURSE_AUTHORING_MICROFRONTEND_URL = "http://apps.local.openedx.io:2001/authoring" +CATALOG_MICROFRONTEND_URL = "http://apps.local.openedx.io:1998/catalog" + +####################################################################################################################### +#### DERIVE ANY DERIVED SETTINGS +#### + +derive_settings(__name__) +add_plugins(__name__, ProjectType.LMS, SettingsType.DEVSTACK) diff --git a/lms/envs/development.py b/lms/envs/development.py new file mode 100644 index 000000000000..eae245a9d63f --- /dev/null +++ b/lms/envs/development.py @@ -0,0 +1,178 @@ +""" +This settings file is optimized for local development. It should work equally well for bare-metal development and for +running inside of development environments such as tutor. + +This file is currently in development itself and so may not work for everyone out of the box. More updates, including +updated documentation will be added as we get closer to removing devstack.py +""" + +#Helpers for loading plugins and their settings. +from edx_django_utils.plugins import add_plugins + +from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType +from openedx.core.lib.derived import derive_settings + +# Use the common file as the starting point. +# pylint: disable=wildcard-import +from .common import * # noqa: F403 + +DEBUG = True + +STORAGES['default']['BACKEND'] = 'django.core.files.storage.FileSystemStorage' # noqa: F405 +STORAGES['staticfiles']['BACKEND'] = 'openedx.core.storage.DevelopmentStorage' # noqa: F405 + +# Disable pipeline compression in development +PIPELINE['PIPELINE_ENABLED'] = False # noqa: F405 + +# Revert to the default set of finders as we don't want the production pipeline +STATICFILES_FINDERS = [ + 'openedx.core.djangoapps.theming.finders.ThemeFilesFinder', + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'pipeline.finders.PipelineFinder', +] + +# Point STATIC_ROOT at test_root/staticfiles so that WEBPACK_LOADER's STATS_FILE resolves here. +# STATS_FILE is derived from STATIC_ROOT (see openedx/envs/common.py), and this is the directory +# that `npm run build-dev` / `npm run watch` write webpack-stats.json into by default (see +# webpack.common.config.js, which falls back to ./test_root/staticfiles when STATIC_ROOT_LMS is +# unset). The base default of ENV_ROOT/staticfiles points *outside* the repo and does not match +# where webpack writes, so the loader can't find the stats file. +# +# NOTE: This is purely so the webpack stats manifest can be located. You are NOT expected to run +# collectstatic in development -- with DEBUG=True the staticfiles finders serve assets directly +# from their source dirs (e.g. the bundles in common/static/bundles). This mirrors what the test +# settings already do (openedx/envs/test.py). +STATIC_ROOT = REPO_ROOT / 'test_root' / 'staticfiles' # noqa: F405 + +# Whether to run django-require in debug mode. +REQUIRE_DEBUG = DEBUG + +# Run Celery tasks synchronously in-process so local development needs no message broker or worker. +# The base default (CELERY_ALWAYS_EAGER = False, openedx/envs/common.py) makes task-enqueuing code +# paths try to reach a broker and 500 with "Connection refused". This matches the old devstack +# behavior. +CELERY_ALWAYS_EAGER = True + +LMS_BASE = 'local.openedx.io:8000' +LMS_ROOT_URL = f'http://{LMS_BASE}' +ALLOWED_HOSTS = ['local.openedx.io'] + +# Add JWTs so we can get reliable session keys +# TODO: It would be nice to link to how we generate these secrets. +JWT_AUTH.update({ # noqa: F405 + 'JWT_PRIVATE_SIGNING_JWK': """ + { + "kid": "devstack_key", + "kty": "RSA", + "key_ops": [ + "sign" + ], + "n": "smKFSYowG6nNUAdeqH1jQQnH1PmIHphzBmwJ5vRf1vu48BUI5VcVtUWIPqzRK_LDSlZYh9D0YFL0ZTxIrlb6Tn3Xz7pYvpIAeYuQv3_H5p8tbz7Fb8r63c1828wXPITVTv8f7oxx5W3lFFgpFAyYMmROC4Ee9qG5T38LFe8_oAuFCEntimWxN9F3P-FJQy43TL7wG54WodgiM0EgzkeLr5K6cDnyckWjTuZbWI-4ffcTgTZsL_Kq1owa_J2ngEfxMCObnzGy5ZLcTUomo4rZLjghVpq6KZxfS6I1Vz79ZsMVUWEdXOYePCKKsrQG20ogQEkmTf9FT_SouC6jPcHLXw", + "e": "AQAB", + "d": "RQ6k4NpRU3RB2lhwCbQ452W86bMMQiPsa7EJiFJUg-qBJthN0FMNQVbArtrCQ0xA1BdnQHThFiUnHcXfsTZUwmwvTuiqEGR_MI6aI7h5D8vRj_5x-pxOz-0MCB8TY8dcuK9FkljmgtYvV9flVzCk_uUb3ZJIBVyIW8En7n7nV7JXpS9zey1yVLld2AbRG6W5--Pgqr9JCI5-bLdc2otCLuen2sKyuUDHO5NIj30qGTaKUL-OW_PgVmxrwKwccF3w5uGNEvMQ-IcicosCOvzBwdIm1uhdm9rnHU1-fXz8VLRHNhGVv7z6moghjNI0_u4smhUkEsYeshPv7RQEWTdkOQ", + "p": "7KWj7l-ZkfCElyfvwsl7kiosvi-ppOO7Imsv90cribf88DexcO67xdMPesjM9Nh5X209IT-TzbsOtVTXSQyEsy42NY72WETnd1_nAGLAmfxGdo8VV4ZDnRsA8N8POnWjRDwYlVBUEEeuT_MtMWzwIKU94bzkWVnHCY5vbhBYLeM", + "q": "wPkfnjavNV1Hqb5Qqj2crBS9HQS6GDQIZ7WF9hlBb2ofDNe2K2dunddFqCOdvLXr7ydRcK51ZwSeHjcjgD1aJkHA9i1zqyboxgd0uAbxVDo6ohnlVqYLtap2tXXcavKm4C9MTpob_rk6FBfEuq4uSsuxFvCER4yG3CYBBa4gZVU", + "dp": "MO9Ppss-Bl-mC1vGyJDBbMgr2GgivGYbHFLt6ERfTGsvcr0RhDjZu16ZpNpBB6B7-K-uJGHxPmmf8P9KRWDBUAwOSaT2a-pTsuux6PKCwVTZfUq5LxAkiyg6WZTGoWASEtoae0XRHEy2TvIKNl5AiX-h_DwDPDbEYcWCZVAb6-E", + "dq": "m03j7GkGSWRxMGNCeEBtvvBR4vDS9Her7AtjbNSWnRxDMQrKSdRMaiu-m7tOT3n6D9cM7Cr7wZUtzBOENskprHBu47FgzfXakMWfYhv0TV0voxZERKAN_H7cWt4oLsprEzH9r6THsxFPdKxMYBGeoAOe2l9nlk26m6LaX7_rwqE", + "qi": "jnJ0nfARyAcHsezENNrXKnDM-LrMJWMHPh_70ZM_pF5iRMOLojHkTVsUIzYi6Uj2ohX9Jz1zsV207kCuPqQXURbhlt1xEaktwCmySeWU4qkMTptWp4ya2jEwGn8EKJ1iEc0GhDkRyLrgm4ol-sq9DMaKEkhTGy4Y3-8mMCBVqeQ" + } +""", + 'JWT_PUBLIC_SIGNING_JWK_SET': ( + '{"keys": [{"kid": "devstack_key", "e": "AQAB", "kty": "RSA", "n": "smKFSYowG6nNUAdeqH1jQQnH1PmIHphzBmwJ5vRf1vu' + '48BUI5VcVtUWIPqzRK_LDSlZYh9D0YFL0ZTxIrlb6Tn3Xz7pYvpIAeYuQv3_H5p8tbz7Fb8r63c1828wXPITVTv8f7oxx5W3lFFgpFAyYMmROC' + '4Ee9qG5T38LFe8_oAuFCEntimWxN9F3P-FJQy43TL7wG54WodgiM0EgzkeLr5K6cDnyckWjTuZbWI-4ffcTgTZsL_Kq1owa_J2ngEfxMCObnzG' + 'y5ZLcTUomo4rZLjghVpq6KZxfS6I1Vz79ZsMVUWEdXOYePCKKsrQG20ogQEkmTf9FT_SouC6jPcHLXw"}]}' + ), +}) + +# Dealing with CORS +CORS_ALLOW_CREDENTIALS = True +# Each development MFE is served under apps.local.openedx.io on its own port. Every MFE fetches its +# config from the LMS MFE Config API, so each origin must be allowed here for that cross-origin +# request to succeed. +CORS_ORIGIN_WHITELIST = ( + "http://apps.local.openedx.io:1984", # communications + "http://apps.local.openedx.io:1993", # ora-grading + "http://apps.local.openedx.io:1994", # gradebook + "http://apps.local.openedx.io:1995", # profile + "http://apps.local.openedx.io:1996", # learner-dashboard + "http://apps.local.openedx.io:1997", # account + "http://apps.local.openedx.io:1998", # catalog + "http://apps.local.openedx.io:1999", # authn + "http://apps.local.openedx.io:2000", # learning + "http://apps.local.openedx.io:2001", # authoring (Studio) + "http://apps.local.openedx.io:2002", # discussions + "http://apps.local.openedx.io:2025", # admin-console +) + +# Post-login/logout redirects back to an MFE are only honored when the target host is whitelisted +# here (entries are host:port, no scheme). Mirrors the MFE origins allowed for CORS above. +LOGIN_REDIRECT_WHITELIST = [ + "apps.local.openedx.io:1984", # communications + "apps.local.openedx.io:1993", # ora-grading + "apps.local.openedx.io:1994", # gradebook + "apps.local.openedx.io:1995", # profile + "apps.local.openedx.io:1996", # learner-dashboard + "apps.local.openedx.io:1997", # account + "apps.local.openedx.io:1998", # catalog + "apps.local.openedx.io:1999", # authn + "apps.local.openedx.io:2000", # learning + "apps.local.openedx.io:2001", # authoring (Studio) + "apps.local.openedx.io:2002", # discussions + "apps.local.openedx.io:2025", # admin-console +] + +# Cookie Related Settings +SESSION_COOKIE_DOMAIN = '.local.openedx.io' + +# MFE Development URLs +# One URL per development MFE, ordered by port, each served under apps.local.openedx.io. +COMMUNICATIONS_MICROFRONTEND_URL = "http://apps.local.openedx.io:1984/communications" +ORA_GRADING_MICROFRONTEND_URL = "http://apps.local.openedx.io:1993/ora-grading" +WRITABLE_GRADEBOOK_URL = "http://apps.local.openedx.io:1994/gradebook" +PROFILE_MICROFRONTEND_URL = "http://apps.local.openedx.io:1995/profile/u/" +# This one needs a trailing slash to load correctly right now. +LEARNER_HOME_MICROFRONTEND_URL = "http://apps.local.openedx.io:1996/learner-dashboard/" +ACCOUNT_MICROFRONTEND_URL = "http://apps.local.openedx.io:1997/account/" +CATALOG_MICROFRONTEND_URL = "http://apps.local.openedx.io:1998/catalog" +AUTHN_MICROFRONTEND_URL = "http://apps.local.openedx.io:1999/authn" +# Host (no scheme) used to build password-reset / account-recovery email links when the authn MFE +# is enabled (ENABLE_AUTHN_MICROFRONTEND). Paired with AUTHN_MICROFRONTEND_URL above. +AUTHN_MICROFRONTEND_DOMAIN = "apps.local.openedx.io:1999/authn" +# This one explicitly needs to not have a trailing slash because of how it's used to make other +# urls. +LEARNING_MICROFRONTEND_URL = "http://apps.local.openedx.io:2000/learning" +COURSE_AUTHORING_MICROFRONTEND_URL = "http://apps.local.openedx.io:2001/authoring" +DISCUSSIONS_MICROFRONTEND_URL = "http://apps.local.openedx.io:2002/discussions" +ADMIN_CONSOLE_MICROFRONTEND_URL = "http://apps.local.openedx.io:2025/admin-console" + +# Temporarily enable the MFE Config API so the dev MFEs can fetch their runtime config from the +# LMS. It is off by default in common.py (ENABLE_MFE_CONFIG_API), which makes /api/mfe_config/v1 +# return a 404. The flag itself is being deprecated -- see +# https://github.com/openedx/openedx-platform/issues/38959. Remove this override once that DEPR +# lands, or fold its removal into the DEPR if this development.py work merges first. +ENABLE_MFE_CONFIG_API = True + +# Shared backend URLs served to all MFEs via the MFE Config API (GET /api/mfe_config/v1). An MFE +# that sets MFE_CONFIG_API_URL fetches this at startup and merges it *over* its build-time .env +# defaults (which point at localhost), so we don't have to hand-edit each MFE's env to use the +# local.openedx.io DNS. Keys use the SCREAMING_SNAKE names that @edx/frontend-platform reads +# straight into getConfig(). Values common to every MFE go here; per-MFE values go in +# MFE_CONFIG_OVERRIDES keyed by MFE name (e.g. 'learning'). +MFE_CONFIG = { + "LMS_BASE_URL": LMS_ROOT_URL, # noqa: F405 + "LOGIN_URL": f"{LMS_ROOT_URL}/login", # noqa: F405 + "LOGOUT_URL": f"{LMS_ROOT_URL}/logout", # noqa: F405 + "REFRESH_ACCESS_TOKEN_ENDPOINT": f"{LMS_ROOT_URL}/login_refresh", # noqa: F405 + # Studio runs on its own subdomain and port (8001) in this setup. CMS_BASE in the LMS common + # settings still points at the production default, so set the dev Studio URL explicitly here. + "STUDIO_BASE_URL": "http://studio.local.openedx.io:8001", +} + +####################################################################################################################### +#### DERIVE ANY DERIVED SETTINGS +#### + +derive_settings(__name__) +add_plugins(__name__, ProjectType.LMS, SettingsType.DEVSTACK) From 7baaa5fcd8357062cb3a91c4a28c7953aac25bac Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 6 Aug 2026 12:29:39 -0400 Subject: [PATCH 2/3] docs: add experimental how-to for the development.py settings Document the new bare-metal development workflow that runs the LMS and CMS with the dedicated `development.py` settings module (building directly on `common.py`) and the `local.openedx.io` domains, rather than the legacy `devstack.py` settings on `localhost`. The how-to covers the `--settings=development` runserver commands (local.openedx.io:8000 for LMS, studio.local.openedx.io:8001 for CMS), migrations, the webpack asset build (and why collectstatic is not needed), CMS SSO setup, running the MFEs against the LMS MFE Config API, and the notable differences from devstack (eager Celery, MFE config served by the LMS, pre-declared CORS/CSRF/login-redirect origins). It is linked from the README as an experimental item under "For Development"; it is intentionally not yet the recommended default while the base README and workflow are still being updated. Co-Authored-By: Claude Opus 4.8 --- README.rst | 9 ++ docs/how-tos/using_development_settings.rst | 157 ++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 docs/how-tos/using_development_settings.rst diff --git a/README.rst b/README.rst index 3dd3930626f0..3a032cc3b046 100644 --- a/README.rst +++ b/README.rst @@ -58,6 +58,14 @@ Tutor also features a `development mode`_ which will also help you modify, test, and extend openedx-platform. We recommend this method for all Open edX developers. +.. note:: + + **Experimental:** We are trialing a new bare-metal development workflow based + on a dedicated ``development.py`` settings module that builds directly on + ``common.py`` and uses ``local.openedx.io`` domains. It is not yet the + recommended default, but you can try it by following `Using the development.py + settings`_. + Bare Metal (Advanced) ===================== @@ -200,6 +208,7 @@ A full list of the MFEs expected to run by default are listed below. .. _Learner Home MFE: https://github.com/openedx/frontend-app-learner-dashboard .. _Learning MFE: https://github.com/openedx/frontend-app-learning/ .. _Authoring MFE: https://github.com/openedx/frontend-app-authoring/ +.. _Using the development.py settings: ./docs/how-tos/using_development_settings.rst Expected MFEs and Default Ports ------------------------------- diff --git a/docs/how-tos/using_development_settings.rst b/docs/how-tos/using_development_settings.rst new file mode 100644 index 000000000000..4247f0637f4a --- /dev/null +++ b/docs/how-tos/using_development_settings.rst @@ -0,0 +1,157 @@ +Using the ``development.py`` settings (experimental) +#################################################### +.. contents:: + +Overview +======== + +This guide describes an **experimental** way to run the LMS and CMS for local +development. Instead of the legacy ``devstack.py`` settings (which build on top +of ``production.py``), it uses a dedicated ``development.py`` settings module for +each service that builds directly on top of the ``common.py`` defaults. + +Two things differ from the older bare-metal / devstack instructions: + +* **Settings module:** you pass ``--settings=development`` to ``manage.py`` + instead of relying on ``devstack.py``. +* **Domains:** services are addressed through ``local.openedx.io`` subdomains + (which resolve to ``127.0.0.1``) rather than ``localhost:``. This gives + nicer, production-like hostnames and lets cookie, CORS, and CSRF behavior be + exercised more realistically across services and MFEs. + +.. warning:: + + This workflow is under active development and is **not** yet the recommended + default. Some steps may change. If you want a supported development + environment today, use `Tutor's development mode`_. + +Prerequisites +============= + +Follow the **System Dependencies** and initial **Build Steps** from the +``Bare Metal (Advanced)`` section of the `openedx-platform README`_ (Python +3.12, Node, MySQL, Mongo, Memcached, a virtualenv, ``npm clean-install``, and +``pip install -r requirements/edx/development.txt``). The steps below replace +only the "Run the Platform" portion of those instructions. + +Domain names +============ + +``local.openedx.io`` and its subdomains resolve to the loopback address +(``127.0.0.1``), so no web server or proxy is required. This guide uses: + +* ``local.openedx.io`` — LMS +* ``studio.local.openedx.io`` — CMS / Studio +* ``apps.local.openedx.io`` — Micro-frontends (MFEs) + +Database and migrations +======================= + +Create the databases and run migrations exactly as in the bare-metal +instructions, but pass ``--settings=development``:: + + python manage.py lms --settings=development migrate + python manage.py lms --settings=development migrate --database=student_module_history + python manage.py cms --settings=development migrate + +Build frontend assets +====================== + +Build the webpack bundles once (or run the watcher for a live edit/rebuild +loop):: + + npm run build-dev # one-time build + # or, for an auto-rebuilding dev loop: + npm run watch + +You do **not** need to run ``collectstatic``. With ``DEBUG = True`` the +staticfiles finders serve assets directly from their source directories. The +``development.py`` settings point ``STATIC_ROOT`` at ``test_root/staticfiles`` +only so that the webpack stats manifest (``webpack-stats.json``) can be located. + +Run the LMS and CMS +=================== + +First, ensure MySQL, Mongo, and Memcached are running. Then start each service +with the ``development`` settings, bound to its ``local.openedx.io`` host: + +Start the LMS:: + + python manage.py lms --settings=development runserver local.openedx.io:8000 + +Start the CMS:: + + python manage.py cms --settings=development runserver studio.local.openedx.io:8001 + +Set up CMS SSO +============== + +Studio authenticates against the LMS via OAuth. Create the worker user and +OAuth application (as in the bare-metal instructions), using the Studio +``local.openedx.io`` redirect URI:: + + python manage.py lms --settings=development manage_user studio_worker studio_worker@example.com --unusable-password + # DO NOT DO THIS IN PRODUCTION. It will make your auth insecure. + python manage.py lms --settings=development create_dot_application studio-sso-id studio_worker \ + --grant-type authorization-code \ + --skip-authorization \ + --redirect-uris 'http://studio.local.openedx.io:8001/complete/edx-oauth2/' \ + --scopes user_id \ + --client-id 'studio-sso-key' \ + --client-secret 'studio-sso-secret' + +Run the MFEs +============ + +Most of the UI now lives in Micro-frontends, which run separately. Each MFE is +served under ``apps.local.openedx.io`` on its own port. Clone the MFE repo(s) +you need next to ``openedx-platform``, install dependencies (``npm +clean-install``), and start each one with its ``dev`` script. That script points +``MFE_CONFIG_API_URL`` at the LMS MFE Config API +(``http://local.openedx.io:8000/api/mfe_config/v1``), so the MFE fetches its +runtime configuration (LMS/Studio URLs, etc.) from the running LMS:: + + npm run dev + +At a minimum you will want the Authoring, Learning, and Learner Home MFEs. The +``development.py`` settings already configure URLs for the full default set: + +.. list-table:: + :header-rows: 1 + + * - MFE + - Location + - Setting + * - frontend-app-learning + - apps.local.openedx.io:2000/learning + - ``LEARNING_MICROFRONTEND_URL`` + * - frontend-app-authoring + - apps.local.openedx.io:2001/authoring + - ``COURSE_AUTHORING_MICROFRONTEND_URL`` + * - frontend-app-learner-dashboard + - apps.local.openedx.io:1996/learner-dashboard + - ``LEARNER_HOME_MICROFRONTEND_URL`` + * - frontend-app-account + - apps.local.openedx.io:1997/account + - ``ACCOUNT_MICROFRONTEND_URL`` + * - frontend-app-profile + - apps.local.openedx.io:1995/profile + - ``PROFILE_MICROFRONTEND_URL`` + +The remaining default MFE URLs (authn, discussions, communications, +ora-grading, gradebook, catalog, admin-console) are also set in +``lms/envs/development.py``; see that file for the complete list and ports. + +Notes and differences from devstack +=================================== + +* **No broker required.** ``CELERY_ALWAYS_EAGER = True`` runs Celery tasks + in-process, so you do not need to run a message broker or worker. +* **MFE configuration is served by the LMS.** The MFE Config API + (``/api/mfe_config/v1``) is enabled and populated so MFEs pick up the + ``local.openedx.io`` URLs instead of their built-in ``localhost`` defaults. +* **CORS / CSRF / login redirects** for the default MFE origins are pre-declared + in the ``development.py`` files. + +.. _Tutor's development mode: https://docs.tutor.edly.io/dev.html +.. _openedx-platform README: https://github.com/openedx/edx-platform/blob/master/README.rst From d16729a8e2a0e21a5df4050303f0e8167a865c84 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 8 Oct 2025 13:20:55 -0400 Subject: [PATCH 3/3] fix: Make MEDIA_ROOT and DATA_DIR overridable. The two settings were previously hard-coded in the lms and openedx common.py making it hard to make reasonable defaults using the ENV_ROOT setting. --- lms/envs/common.py | 14 ++++++++++++-- lms/envs/production.py | 3 +++ openedx/envs/common.py | 7 +++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index e8a185c1d49c..e9d476a833b5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -806,7 +806,14 @@ ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms +REPO_ROOT = PROJECT_ROOT.dirname() +COMMON_ROOT = REPO_ROOT / "common" +OPENEDX_ROOT = REPO_ROOT / "openedx" +XMODULE_ROOT = REPO_ROOT / "xmodule" +ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /edx-platform is in +COURSES_ROOT = ENV_ROOT / "data" NODE_MODULES_ROOT = REPO_ROOT / "node_modules" # noqa: F405 +MEDIA_ROOT = ENV_ROOT / "media_root" # Where to look for a status message STATUS_MESSAGE_PATH = ENV_ROOT / "status_message.json" # noqa: F405 @@ -925,8 +932,11 @@ ALTERNATE_WORKER_QUEUES = 'cms' -DATA_DIR = '/edx/var/edxapp/data' - +# .. setting_name: MAINTENANCE_BANNER_TEXT +# .. setting_default: None +# .. setting_description: Specifies the text that is rendered on the maintenance banner. +# .. setting_warning: Depends on the `open_edx_util.display_maintenance_warning` waffle switch. +# The banner is only rendered when the switch is activated. MAINTENANCE_BANNER_TEXT = None # Set certificate issued date format. It supports all formats supported by diff --git a/lms/envs/production.py b/lms/envs/production.py index b13f232f5bb7..b0a897b62627 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -38,6 +38,9 @@ # A proxy for feature flags stored in the settings namespace FEATURES = FeaturesProxy(globals()) +# Settings moved from common.py +DATA_DIR = '/edx/var/edxapp/data' +MEDIA_ROOT = '/edx/var/edxapp/media/' def get_env_setting(setting): """ Get the environment setting or return exception """ diff --git a/openedx/envs/common.py b/openedx/envs/common.py index f7734120b79a..3ced827eb0ad 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -111,7 +111,6 @@ def _make_locale_paths(settings): TIME_ZONE = 'UTC' # User-uploaded content -MEDIA_ROOT = '/edx/var/edxapp/media/' MEDIA_URL = '/media/' # Dummy secret key for dev/test @@ -1597,7 +1596,7 @@ def add_optional_apps(optional_apps, installed_apps): # STORAGE_CLASS='storages.backends.s3boto3.S3Boto3Storage', # STORAGE_KWARGS=dict(bucket='video-image-bucket'), STORAGE_KWARGS=dict( - location=MEDIA_ROOT, + location=Derived(lambda settings: settings.MEDIA_ROOT), ), DIRECTORY_PREFIX='video-images/', BASE_URL=MEDIA_URL, @@ -1614,7 +1613,7 @@ def add_optional_apps(optional_apps, installed_apps): # STORAGE_CLASS='storages.backends.s3boto3.S3Boto3Storage', # STORAGE_KWARGS=dict(bucket='video-transcripts-bucket'), STORAGE_KWARGS=dict( - location=MEDIA_ROOT, + location=Derived(lambda settings: settings.MEDIA_ROOT), ), DIRECTORY_PREFIX='video-transcripts/', BASE_URL=MEDIA_URL, @@ -2004,7 +2003,7 @@ def add_optional_apps(optional_apps, installed_apps): PROFILE_IMAGE_BACKEND = { 'class': 'openedx.core.storage.OverwriteStorage', 'options': { - 'location': os.path.join(MEDIA_ROOT, 'profile-images/'), + 'location': Derived(lambda settings: os.path.join(settings.MEDIA_ROOT, 'profile-images/')), 'base_url': os.path.join(MEDIA_URL, 'profile-images/'), }, }