From 4035aa3487d5d22db181e580b3743f3c8f204e51 Mon Sep 17 00:00:00 2001 From: Robert Raposa Date: Fri, 17 Jan 2025 11:16:38 -0500 Subject: [PATCH 1/9] Revert "refactor: Clean up lms/envs/production.py cruft (#36115)" (#36129) This reverts commit 15939232d547b751a06e9cb0c1c39ba78144bade. --- lms/envs/common.py | 28 +- lms/envs/production.py | 875 +++++++++++++++++++++++++++++++---------- 2 files changed, 678 insertions(+), 225 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 23e0d12b3dde..cb7643c3668e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3392,34 +3392,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring CSRF_COOKIE_SECURE = False CSRF_TRUSTED_ORIGINS = [] CSRF_TRUSTED_ORIGINS_WITH_SCHEME = [] - -# If setting a cross-domain cookie, it's really important to choose -# a name for the cookie that is DIFFERENT than the cookies used -# by each subdomain. For example, suppose the applications -# at these subdomains are configured to use the following cookie names: -# -# 1) foo.example.com --> "csrftoken" -# 2) baz.example.com --> "csrftoken" -# 3) bar.example.com --> "csrftoken" -# -# For the cross-domain version of the CSRF cookie, you need to choose -# a name DIFFERENT than "csrftoken"; otherwise, the new token configured -# for ".example.com" could conflict with the other cookies, -# non-deterministically causing 403 responses. -CROSS_DOMAIN_CSRF_COOKIE_NAME = '' - -# When setting the domain for the "cross-domain" version of the CSRF -# cookie, you should choose something like: ".example.com" -# (note the leading dot), where both the referer and the host -# are subdomains of "example.com". -# -# Browser security rules require that -# the cookie domain matches the domain of the server; otherwise -# the cookie won't get set. And once the cookie gets set, the client -# needs to be on a domain that matches the cookie domain, otherwise -# the client won't be able to read the cookie. CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = '' - +CROSS_DOMAIN_CSRF_COOKIE_NAME = '' ######################### Django Rest Framework ######################## diff --git a/lms/envs/production.py b/lms/envs/production.py index 3ea1c952e479..addcea0e6028 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -22,6 +22,7 @@ import os import yaml +import django from django.core.exceptions import ImproperlyConfigured from edx_django_utils.plugins import add_plugins from openedx_events.event_bus import merge_producer_configs @@ -43,11 +44,7 @@ def get_env_setting(setting): error_msg = "Set the %s env variable" % setting raise ImproperlyConfigured(error_msg) # lint-amnesty, pylint: disable=raise-missing-from - -################################################# PRODUCTION DEFAULTS ################################################ -# We configure some defaults (beyond what has already been configured in common.py) before loading the YAML file below. -# DO NOT ADD NEW DEFAULTS HERE! Put any new setting defaults in common.py instead, along with a setting annotation. -# TODO: Move all these defaults into common.py. +################################ ALWAYS THE SAME ############################## DEBUG = False DEFAULT_TEMPLATE_ENGINE['OPTIONS']['debug'] = False @@ -61,72 +58,7 @@ def get_env_setting(setting): # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header # for other warnings. SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - -CELERY_RESULT_BACKEND = 'django-cache' -BROKER_HEARTBEAT = 60.0 -BROKER_HEARTBEAT_CHECKRATE = 2 -STATIC_ROOT_BASE = None -STATIC_URL_BASE = None -EMAIL_HOST = 'localhost' -EMAIL_PORT = 25 -EMAIL_USE_TLS = False -SESSION_COOKIE_DOMAIN = None -SESSION_COOKIE_HTTPONLY = True -AWS_SES_REGION_NAME = 'us-east-1' -AWS_SES_REGION_ENDPOINT = 'email.us-east-1.amazonaws.com' -REGISTRATION_EMAIL_PATTERNS_ALLOWED = None -LMS_ROOT_URL = None -CMS_BASE = 'studio.edx.org' -CELERY_EVENT_QUEUE_TTL = None -COMPREHENSIVE_THEME_LOCALE_PATHS = [] -PREPEND_LOCALE_PATHS = [] -COURSE_LISTINGS = {} -COMMENTS_SERVICE_URL = '' -COMMENTS_SERVICE_KEY = '' -CERT_QUEUE = 'test-pull' -PYTHON_LIB_FILENAME = 'python_lib.zip' -VIDEO_CDN_URL = {} -HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = {} -AWS_STORAGE_BUCKET_NAME = 'edxuploads' -# Disabling querystring auth instructs Boto to exclude the querystring parameters (e.g. signature, access key) it -# normally appends to every returned URL. -AWS_QUERYSTRING_AUTH = True -AWS_S3_CUSTOM_DOMAIN = 'edxuploads.s3.amazonaws.com' -MONGODB_LOG = {} -ZENDESK_USER = None -ZENDESK_API_KEY = None -EDX_API_KEY = None -CELERY_BROKER_TRANSPORT = "" -CELERY_BROKER_HOSTNAME = "" -CELERY_BROKER_VHOST = "" -CELERY_BROKER_USER = "" -CELERY_BROKER_PASSWORD = "" -BROKER_USE_SSL = False -SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = None -ENABLE_REQUIRE_THIRD_PARTY_AUTH = False -GOOGLE_ANALYTICS_TRACKING_ID = None -GOOGLE_ANALYTICS_LINKEDIN = None -GOOGLE_SITE_VERIFICATION_ID = None -BRANCH_IO_KEY = None -REGISTRATION_CODE_LENGTH = 8 -FACEBOOK_API_VERSION = None -FACEBOOK_APP_SECRET = None -FACEBOOK_APP_ID = None -API_ACCESS_MANAGER_EMAIL = None -API_ACCESS_FROM_EMAIL = None -CHAT_COMPLETION_API = '' -CHAT_COMPLETION_API_KEY = '' -OPENAPI_CACHE_TIMEOUT = 60 * 60 -MAINTENANCE_BANNER_TEXT = None -DASHBOARD_COURSE_LIMIT = None - -# TODO: We believe these were part of the DEPR'd sysadmin dashboard, and can likely be removed. -SSL_AUTH_EMAIL_DOMAIN = "MIT.EDU" -SSL_AUTH_DN_FORMAT_STRING = ( - "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" -) - -####################################################################################################################### +################################ END ALWAYS THE SAME ############################## # A file path to a YAML file from which to load all the configuration for the edx platform CONFIG_FILE = get_env_setting('LMS_CFG') @@ -134,13 +66,10 @@ def get_env_setting(setting): with codecs.open(CONFIG_FILE, encoding='utf-8') as f: __config__ = yaml.safe_load(f) - # _YAML_TOKENS contains the exact contents of the LMS_CFG YAML file. - # We do splat the entirety of the LMS_CFG YAML file (except KEYS_WITH_MERGED_VALUES) into this module. - # However, for precise backwards compatibility, we need to reference _YAML_TOKENS directly a few times, - # particularly we need to derive Django setting values from YAML values. - # This pattern is confusing and we discourage it. Rather than adding more _YAML_TOKENS references, please - # consider just referencing this module's variables directly. - _YAML_TOKENS = __config__ + # ENV_TOKENS and AUTH_TOKENS are included for reverse compatibility. + # Removing them may break plugins that rely on them. + ENV_TOKENS = __config__ + AUTH_TOKENS = __config__ # Add the key/values from config into the global namespace of this module. # But don't override the FEATURES dict because we do that in an additive way. @@ -153,6 +82,7 @@ def get_env_setting(setting): 'JWT_AUTH', 'CELERY_QUEUES', 'MKTG_URL_LINK_MAP', + 'MKTG_URL_OVERRIDES', 'REST_FRAMEWORK', 'EVENT_BUS_PRODUCER_CONFIG', ] @@ -181,11 +111,20 @@ def get_env_setting(setting): BROKER_POOL_LIMIT = 0 BROKER_CONNECTION_TIMEOUT = 1 +# Allow env to configure celery result backend with default set to django-cache +CELERY_RESULT_BACKEND = ENV_TOKENS.get('CELERY_RESULT_BACKEND', 'django-cache') + +# When the broker is behind an ELB, use a heartbeat to refresh the +# connection and to detect if it has been dropped. +BROKER_HEARTBEAT = ENV_TOKENS.get('BROKER_HEARTBEAT', 60.0) +BROKER_HEARTBEAT_CHECKRATE = ENV_TOKENS.get('BROKER_HEARTBEAT_CHECKRATE', 2) + # Each worker should only fetch one message at a time CELERYD_PREFETCH_MULTIPLIER = 1 # STATIC_ROOT specifies the directory where static files are # collected +STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None) if STATIC_ROOT_BASE: STATIC_ROOT = path(STATIC_ROOT_BASE) WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json" @@ -193,34 +132,83 @@ def get_env_setting(setting): # STATIC_URL_BASE specifies the base url to use for static files +STATIC_URL_BASE = ENV_TOKENS.get('STATIC_URL_BASE', None) if STATIC_URL_BASE: STATIC_URL = STATIC_URL_BASE if not STATIC_URL.endswith("/"): STATIC_URL += "/" -DATA_DIR = path(DATA_DIR) -CC_MERCHANT_NAME = _YAML_TOKENS.get('CC_MERCHANT_NAME', PLATFORM_NAME) -EMAIL_FILE_PATH = _YAML_TOKENS.get('EMAIL_FILE_PATH', DATA_DIR / "emails" / "lms") - -# TODO: This was for backwards compatibility back when installed django-cookie-samesite (not since 2022). -# The DCS_ version of the setting can be DEPR'd at this point. +# Allow overriding build profile used by RequireJS with one +# contained on a custom theme +REQUIRE_BUILD_PROFILE = ENV_TOKENS.get('REQUIRE_BUILD_PROFILE', REQUIRE_BUILD_PROFILE) + +# The following variables use (or) instead of the default value inside (get). This is to enforce using the Lazy Text +# values when the variable is an empty string. Therefore, setting these variable as empty text in related +# json files will make the system reads their values from django translation files +PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME') or PLATFORM_NAME +PLATFORM_DESCRIPTION = ENV_TOKENS.get('PLATFORM_DESCRIPTION') or PLATFORM_DESCRIPTION + +DATA_DIR = path(ENV_TOKENS.get('DATA_DIR', DATA_DIR)) +CC_MERCHANT_NAME = ENV_TOKENS.get('CC_MERCHANT_NAME', PLATFORM_NAME) +EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', DATA_DIR / "emails" / "lms") +EMAIL_HOST = ENV_TOKENS.get('EMAIL_HOST', 'localhost') # django default is localhost +EMAIL_PORT = ENV_TOKENS.get('EMAIL_PORT', 25) # django default is 25 +EMAIL_USE_TLS = ENV_TOKENS.get('EMAIL_USE_TLS', False) # django default is False +SITE_NAME = ENV_TOKENS.get('SITE_NAME', SITE_NAME) +SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') +SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True) + +DCS_SESSION_COOKIE_SAMESITE = ENV_TOKENS.get('DCS_SESSION_COOKIE_SAMESITE', DCS_SESSION_COOKIE_SAMESITE) +DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL = ENV_TOKENS.get('DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL', DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL) # lint-amnesty, pylint: disable=line-too-long + +# As django-cookies-samesite package is set to be removed from base requirements when we upgrade to Django 3.2, +# we should follow the settings name provided by Django. +# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-samesite SESSION_COOKIE_SAMESITE = DCS_SESSION_COOKIE_SAMESITE -LMS_INTERNAL_ROOT_URL = _YAML_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) +AWS_SES_REGION_NAME = ENV_TOKENS.get('AWS_SES_REGION_NAME', 'us-east-1') +AWS_SES_REGION_ENDPOINT = ENV_TOKENS.get('AWS_SES_REGION_ENDPOINT', 'email.us-east-1.amazonaws.com') + +REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') -for feature, value in _YAML_TOKENS.get('FEATURES', {}).items(): +LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL') +LMS_INTERNAL_ROOT_URL = ENV_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) + +# List of logout URIs for each IDA that the learner should be logged out of when they logout of the LMS. Only applies to +# IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = ENV_TOKENS.get('IDA_LOGOUT_URI_LIST', []) + +ENV_FEATURES = ENV_TOKENS.get('FEATURES', {}) +for feature, value in ENV_FEATURES.items(): FEATURES[feature] = value +CMS_BASE = ENV_TOKENS.get('CMS_BASE', 'studio.edx.org') + ALLOWED_HOSTS = [ "*", - _YAML_TOKENS.get('LMS_BASE'), + ENV_TOKENS.get('LMS_BASE'), FEATURES['PREVIEW_LMS_BASE'], ] +# Sometimes, OAuth2 clients want the user to redirect back to their site after logout. But to determine if the given +# redirect URL/path is safe for redirection, the following variable is used by edX. +LOGIN_REDIRECT_WHITELIST = ENV_TOKENS.get( + 'LOGIN_REDIRECT_WHITELIST', + LOGIN_REDIRECT_WHITELIST +) + +# allow for environments to specify what cookie name our login subsystem should use +# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can +# happen with some browsers (e.g. Firefox) +if ENV_TOKENS.get('SESSION_COOKIE_NAME', None): + # NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str() + SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME')) + # This is the domain that is used to set shared cookies between various sub-domains. # By default, it's set to the same thing as the SESSION_COOKIE_DOMAIN, but we want to make it overrideable. -SHARED_COOKIE_DOMAIN = _YAML_TOKENS.get('SHARED_kCOOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) +SHARED_COOKIE_DOMAIN = ENV_TOKENS.get('SHARED_COOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) +CACHES = ENV_TOKENS.get('CACHES', CACHES) # Cache used for location mapping -- called many times with the same key/value # in a given request. if 'loc_cache' not in CACHES: @@ -236,76 +224,206 @@ def get_env_setting(setting): # we need to run asset collection twice, once for local disk and once for S3. # Once we have migrated to service assets off S3, then we can convert this back to # managed by the yaml file contents +STATICFILES_STORAGE = os.environ.get('STATICFILES_STORAGE', ENV_TOKENS.get('STATICFILES_STORAGE', STATICFILES_STORAGE)) + +# Load all AWS_ prefixed variables to allow an S3Boto3Storage to be configured +_locals = locals() +for key, value in ENV_TOKENS.items(): + if key.startswith('AWS_'): + _locals[key] = value + +# Currency +PAID_COURSE_REGISTRATION_CURRENCY = ENV_TOKENS.get('PAID_COURSE_REGISTRATION_CURRENCY', + PAID_COURSE_REGISTRATION_CURRENCY) # We want Bulk Email running on the high-priority queue, so we define the # routing key that points to it. At the moment, the name is the same. # We have to reset the value here, since we have changed the value of the queue name. -BULK_EMAIL_ROUTING_KEY = _YAML_TOKENS.get('BULK_EMAIL_ROUTING_KEY', HIGH_PRIORITY_QUEUE) +BULK_EMAIL_ROUTING_KEY = ENV_TOKENS.get('BULK_EMAIL_ROUTING_KEY', HIGH_PRIORITY_QUEUE) # We can run smaller jobs on the low priority queue. See note above for why # we have to reset the value here. -BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = _YAML_TOKENS.get('BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', DEFAULT_PRIORITY_QUEUE) +BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = ENV_TOKENS.get('BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', DEFAULT_PRIORITY_QUEUE) # Queue to use for expiring old entitlements -ENTITLEMENTS_EXPIRATION_ROUTING_KEY = _YAML_TOKENS.get('ENTITLEMENTS_EXPIRATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) +ENTITLEMENTS_EXPIRATION_ROUTING_KEY = ENV_TOKENS.get('ENTITLEMENTS_EXPIRATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -# Build a CELERY_QUEUES dict the way that celery expects, based on a couple lists of queue names from the YAML. -_YAML_CELERY_QUEUES = _YAML_TOKENS.get('CELERY_QUEUES', None) -if _YAML_CELERY_QUEUES: - CELERY_QUEUES = {queue: {} for queue in _YAML_CELERY_QUEUES} +# Message expiry time in seconds +CELERY_EVENT_QUEUE_TTL = ENV_TOKENS.get('CELERY_EVENT_QUEUE_TTL', None) + +# Allow CELERY_QUEUES to be overwritten by ENV_TOKENS, +ENV_CELERY_QUEUES = ENV_TOKENS.get('CELERY_QUEUES', None) +if ENV_CELERY_QUEUES: + CELERY_QUEUES = {queue: {} for queue in ENV_CELERY_QUEUES} # Then add alternate environment queues -_YAML_ALTERNATE_WORKER_QUEUES = _YAML_TOKENS.get('ALTERNATE_WORKER_QUEUES', '').split() +ALTERNATE_QUEUE_ENVS = ENV_TOKENS.get('ALTERNATE_WORKER_QUEUES', '').split() ALTERNATE_QUEUES = [ DEFAULT_PRIORITY_QUEUE.replace(QUEUE_VARIANT, alternate + '.') - for alternate in _YAML_ALTERNATE_WORKER_QUEUES + for alternate in ALTERNATE_QUEUE_ENVS ] - CELERY_QUEUES.update( { alternate: {} for alternate in ALTERNATE_QUEUES - if alternate not in CELERY_QUEUES.keys() + if alternate not in list(CELERY_QUEUES.keys()) } ) -MKTG_URL_LINK_MAP.update(_YAML_TOKENS.get('MKTG_URL_LINK_MAP', {})) +# following setting is for backward compatibility +if ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', None): + COMPREHENSIVE_THEME_DIR = ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR') + + +# COMPREHENSIVE_THEME_LOCALE_PATHS contain the paths to themes locale directories e.g. +# "COMPREHENSIVE_THEME_LOCALE_PATHS" : [ +# "/edx/src/edx-themes/conf/locale" +# ], +COMPREHENSIVE_THEME_LOCALE_PATHS = ENV_TOKENS.get('COMPREHENSIVE_THEME_LOCALE_PATHS', []) + + +# PREPEND_LOCALE_PATHS contain the paths to locale directories to load first e.g. +# "PREPEND_LOCALE_PATHS" : [ +# "/edx/my-locale" +# ], +PREPEND_LOCALE_PATHS = ENV_TOKENS.get('PREPEND_LOCALE_PATHS', []) + + +MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {})) +ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS = ENV_TOKENS.get( + 'ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS', + ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS +) +# Marketing link overrides +MKTG_URL_OVERRIDES.update(ENV_TOKENS.get('MKTG_URL_OVERRIDES', MKTG_URL_OVERRIDES)) # Intentional defaults. -ID_VERIFICATION_SUPPORT_LINK = _YAML_TOKENS.get('ID_VERIFICATION_SUPPORT_LINK', SUPPORT_SITE_LINK) -PASSWORD_RESET_SUPPORT_LINK = _YAML_TOKENS.get('PASSWORD_RESET_SUPPORT_LINK', SUPPORT_SITE_LINK) -ACTIVATION_EMAIL_SUPPORT_LINK = _YAML_TOKENS.get('ACTIVATION_EMAIL_SUPPORT_LINK', SUPPORT_SITE_LINK) -LOGIN_ISSUE_SUPPORT_LINK = _YAML_TOKENS.get('LOGIN_ISSUE_SUPPORT_LINK', SUPPORT_SITE_LINK) +ID_VERIFICATION_SUPPORT_LINK = ENV_TOKENS.get('ID_VERIFICATION_SUPPORT_LINK', SUPPORT_SITE_LINK) +PASSWORD_RESET_SUPPORT_LINK = ENV_TOKENS.get('PASSWORD_RESET_SUPPORT_LINK', SUPPORT_SITE_LINK) +ACTIVATION_EMAIL_SUPPORT_LINK = ENV_TOKENS.get('ACTIVATION_EMAIL_SUPPORT_LINK', SUPPORT_SITE_LINK) +LOGIN_ISSUE_SUPPORT_LINK = ENV_TOKENS.get('LOGIN_ISSUE_SUPPORT_LINK', SUPPORT_SITE_LINK) # Timezone overrides -TIME_ZONE = CELERY_TIMEZONE +TIME_ZONE = ENV_TOKENS.get('CELERY_TIMEZONE', CELERY_TIMEZONE) # Translation overrides LANGUAGE_DICT = dict(LANGUAGES) -LANGUAGE_COOKIE_NAME = _YAML_TOKENS.get('LANGUAGE_COOKIE') or LANGUAGE_COOKIE_NAME +LANGUAGE_COOKIE_NAME = ENV_TOKENS.get('LANGUAGE_COOKIE', None) or ENV_TOKENS.get( + 'LANGUAGE_COOKIE_NAME', LANGUAGE_COOKIE_NAME) # Additional installed apps -for app in _YAML_TOKENS.get('ADDL_INSTALLED_APPS', []): +for app in ENV_TOKENS.get('ADDL_INSTALLED_APPS', []): INSTALLED_APPS.append(app) -LOGGING = get_logger_config( - LOG_DIR, - logging_env=LOGGING_ENV, - local_loglevel=LOCAL_LOGLEVEL, - service_variant=SERVICE_VARIANT, + +local_loglevel = ENV_TOKENS.get('LOCAL_LOGLEVEL', 'INFO') +LOG_DIR = ENV_TOKENS.get('LOG_DIR', LOG_DIR) + +LOGGING = get_logger_config(LOG_DIR, + logging_env=ENV_TOKENS.get('LOGGING_ENV', LOGGING_ENV), + local_loglevel=local_loglevel, + service_variant=SERVICE_VARIANT) + +COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {}) +COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '') +COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '') +CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull') + +# Python lib settings +PYTHON_LIB_FILENAME = ENV_TOKENS.get('PYTHON_LIB_FILENAME', 'python_lib.zip') + +# Code jail settings +for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items(): + oldvalue = CODE_JAIL.get(name) + if isinstance(oldvalue, dict): + for subname, subvalue in value.items(): + oldvalue[subname] = subvalue + else: + CODE_JAIL[name] = value + +COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) + +# Event Tracking +if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS: + TRACKING_IGNORE_URL_PATTERNS = ENV_TOKENS.get("TRACKING_IGNORE_URL_PATTERNS") + +# SSL external authentication settings +SSL_AUTH_EMAIL_DOMAIN = ENV_TOKENS.get("SSL_AUTH_EMAIL_DOMAIN", "MIT.EDU") +SSL_AUTH_DN_FORMAT_STRING = ENV_TOKENS.get( + "SSL_AUTH_DN_FORMAT_STRING", + "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" ) +# Video Caching. Pairing country codes with CDN URLs. +# Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='} +VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {}) + +# Determines whether the CSRF token can be transported on +# unencrypted channels. It is set to False here for backward compatibility, +# but it is highly recommended that this is True for environments accessed +# by end users. +CSRF_COOKIE_SECURE = ENV_TOKENS.get('CSRF_COOKIE_SECURE', False) + # Determines which origins are trusted for unsafe requests eg. POST requests. -CSRF_TRUSTED_ORIGINS = _YAML_TOKENS.get('CSRF_TRUSTED_ORIGINS_WITH_SCHEME', []) +CSRF_TRUSTED_ORIGINS = ENV_TOKENS.get('CSRF_TRUSTED_ORIGINS', []) +# values are already updated above with default CSRF_TRUSTED_ORIGINS values but in +# case of new django version these values will override. +if django.VERSION[0] >= 4: # for greater than django 3.2 use schemes. + CSRF_TRUSTED_ORIGINS = ENV_TOKENS.get('CSRF_TRUSTED_ORIGINS_WITH_SCHEME', []) -if FEATURES['ENABLE_CORS_HEADERS'] or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'): - CORS_ALLOW_CREDENTIALS = True - CORS_ORIGIN_WHITELIST = _YAML_TOKENS.get('CORS_ORIGIN_WHITELIST', ()) - CORS_ORIGIN_ALLOW_ALL = _YAML_TOKENS.get('CORS_ORIGIN_ALLOW_ALL', False) - CORS_ALLOW_INSECURE = _YAML_TOKENS.get('CORS_ALLOW_INSECURE', False) - CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = _YAML_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_DOMAIN') +############# CORS headers for cross-domain requests ################# +if FEATURES.get('ENABLE_CORS_HEADERS') or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'): + CORS_ALLOW_CREDENTIALS = True + CORS_ORIGIN_WHITELIST = ENV_TOKENS.get('CORS_ORIGIN_WHITELIST', ()) + + CORS_ORIGIN_ALLOW_ALL = ENV_TOKENS.get('CORS_ORIGIN_ALLOW_ALL', False) + CORS_ALLOW_INSECURE = ENV_TOKENS.get('CORS_ALLOW_INSECURE', False) + + # If setting a cross-domain cookie, it's really important to choose + # a name for the cookie that is DIFFERENT than the cookies used + # by each subdomain. For example, suppose the applications + # at these subdomains are configured to use the following cookie names: + # + # 1) foo.example.com --> "csrftoken" + # 2) baz.example.com --> "csrftoken" + # 3) bar.example.com --> "csrftoken" + # + # For the cross-domain version of the CSRF cookie, you need to choose + # a name DIFFERENT than "csrftoken"; otherwise, the new token configured + # for ".example.com" could conflict with the other cookies, + # non-deterministically causing 403 responses. + # + # Because of the way Django stores cookies, the cookie name MUST + # be a `str`, not unicode. Otherwise there will `TypeError`s will be raised + # when Django tries to call the unicode `translate()` method with the wrong + # number of parameters. + CROSS_DOMAIN_CSRF_COOKIE_NAME = str(ENV_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_NAME')) + + # When setting the domain for the "cross-domain" version of the CSRF + # cookie, you should choose something like: ".example.com" + # (note the leading dot), where both the referer and the host + # are subdomains of "example.com". + # + # Browser security rules require that + # the cookie domain matches the domain of the server; otherwise + # the cookie won't get set. And once the cookie gets set, the client + # needs to be on a domain that matches the cookie domain, otherwise + # the client won't be able to read the cookie. + CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = ENV_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_DOMAIN') + + +# Field overrides. To use the IDDE feature, add +# 'courseware.student_field_overrides.IndividualStudentOverrideProvider'. +FIELD_OVERRIDE_PROVIDERS = tuple(ENV_TOKENS.get('FIELD_OVERRIDE_PROVIDERS', [])) + +############### XBlock filesystem field config ########## +if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None: + DJFS = AUTH_TOKENS['DJFS'] + +############### Module Store Items ########## +HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENV_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', {}) # PREVIEW DOMAIN must be present in HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS for the preview to show draft changes if 'PREVIEW_LMS_BASE' in FEATURES and FEATURES['PREVIEW_LMS_BASE'] != '': PREVIEW_DOMAIN = FEATURES['PREVIEW_LMS_BASE'].split(':')[0] @@ -314,11 +432,26 @@ def get_env_setting(setting): PREVIEW_DOMAIN: 'draft-preferred' }) +MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ENV_TOKENS.get( + 'MODULESTORE_FIELD_OVERRIDE_PROVIDERS', + MODULESTORE_FIELD_OVERRIDE_PROVIDERS +) + +XBLOCK_FIELD_DATA_WRAPPERS = ENV_TOKENS.get( + 'XBLOCK_FIELD_DATA_WRAPPERS', + XBLOCK_FIELD_DATA_WRAPPERS +) + ############### Mixed Related(Secure/Not-Secure) Items ########## -LMS_SEGMENT_KEY = _YAML_TOKENS.get('SEGMENT_KEY') +LMS_SEGMENT_KEY = AUTH_TOKENS.get('SEGMENT_KEY') + +SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] +AWS_ACCESS_KEY_ID = AUTH_TOKENS.get("AWS_ACCESS_KEY_ID", AWS_ACCESS_KEY_ID) if AWS_ACCESS_KEY_ID == "": AWS_ACCESS_KEY_ID = None + +AWS_SECRET_ACCESS_KEY = AUTH_TOKENS.get("AWS_SECRET_ACCESS_KEY", AWS_SECRET_ACCESS_KEY) if AWS_SECRET_ACCESS_KEY == "": AWS_SECRET_ACCESS_KEY = None @@ -327,10 +460,24 @@ def get_env_setting(setting): # same with upcoming version setting it to `public-read`. AWS_DEFAULT_ACL = 'public-read' AWS_BUCKET_ACL = AWS_DEFAULT_ACL +AWS_STORAGE_BUCKET_NAME = AUTH_TOKENS.get('AWS_STORAGE_BUCKET_NAME', 'edxuploads') -# Change to S3Boto3 if we haven't specified another default storage AND we have specified AWS creds. -if (not _YAML_TOKENS.get('DEFAULT_FILE_STORAGE')) and AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: +# Disabling querystring auth instructs Boto to exclude the querystring parameters (e.g. signature, access key) it +# normally appends to every returned URL. +AWS_QUERYSTRING_AUTH = AUTH_TOKENS.get('AWS_QUERYSTRING_AUTH', True) +AWS_S3_CUSTOM_DOMAIN = AUTH_TOKENS.get('AWS_S3_CUSTOM_DOMAIN', 'edxuploads.s3.amazonaws.com') + +if AUTH_TOKENS.get('DEFAULT_FILE_STORAGE'): + DEFAULT_FILE_STORAGE = AUTH_TOKENS.get('DEFAULT_FILE_STORAGE') +elif AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' +else: + DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' + + +# If there is a database called 'read_replica', you can use the use_read_replica_if_available +# function in util/query.py, which is useful for very large database reads +DATABASES = AUTH_TOKENS.get('DATABASES', DATABASES) # The normal database user does not have enough permissions to run migrations. # Migrations are run with separate credentials, given as DB_MIGRATION_* @@ -346,9 +493,11 @@ def get_env_setting(setting): 'PORT': os.environ.get('DB_MIGRATION_PORT', database['PORT']), }) +XQUEUE_INTERFACE = AUTH_TOKENS.get('XQUEUE_INTERFACE', XQUEUE_INTERFACE) + # Get the MODULESTORE from auth.json, but if it doesn't exist, # use the one from common.py -MODULESTORE = convert_module_store_setting_if_needed(_YAML_TOKENS.get('MODULESTORE', MODULESTORE)) +MODULESTORE = convert_module_store_setting_if_needed(AUTH_TOKENS.get('MODULESTORE', MODULESTORE)) # After conversion above, the modulestore will have a "stores" list with all defined stores, for all stores, add the # fs_root entry to derived collection so that if it's a callable it can be resolved. We need to do this because the @@ -361,34 +510,137 @@ def get_env_setting(setting): if 'OPTIONS' in store and 'fs_root' in store["OPTIONS"]: derived_collection_entry('MODULESTORE', 'default', 'OPTIONS', 'stores', idx, 'OPTIONS', 'fs_root') +MONGODB_LOG = AUTH_TOKENS.get('MONGODB_LOG', {}) + +EMAIL_HOST_USER = AUTH_TOKENS.get('EMAIL_HOST_USER', '') # django default is '' +EMAIL_HOST_PASSWORD = AUTH_TOKENS.get('EMAIL_HOST_PASSWORD', '') # django default is '' + +# Analytics API +ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", ANALYTICS_API_KEY) +ANALYTICS_API_URL = ENV_TOKENS.get("ANALYTICS_API_URL", ANALYTICS_API_URL) + +# Zendesk +ZENDESK_USER = AUTH_TOKENS.get("ZENDESK_USER") +ZENDESK_API_KEY = AUTH_TOKENS.get("ZENDESK_API_KEY") + +# API Key for inbound requests from Notifier service +EDX_API_KEY = AUTH_TOKENS.get("EDX_API_KEY") + +# Celery Broker +CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "") +CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "") +CELERY_BROKER_VHOST = ENV_TOKENS.get("CELERY_BROKER_VHOST", "") +CELERY_BROKER_USER = AUTH_TOKENS.get("CELERY_BROKER_USER", "") +CELERY_BROKER_PASSWORD = AUTH_TOKENS.get("CELERY_BROKER_PASSWORD", "") + BROKER_URL = "{}://{}:{}@{}/{}".format(CELERY_BROKER_TRANSPORT, CELERY_BROKER_USER, CELERY_BROKER_PASSWORD, CELERY_BROKER_HOSTNAME, CELERY_BROKER_VHOST) +BROKER_USE_SSL = ENV_TOKENS.get('CELERY_BROKER_USE_SSL', False) + try: BROKER_TRANSPORT_OPTIONS = { 'fanout_patterns': True, 'fanout_prefix': True, - **_YAML_TOKENS.get('CELERY_BROKER_TRANSPORT_OPTIONS', {}) + **ENV_TOKENS.get('CELERY_BROKER_TRANSPORT_OPTIONS', {}) } except TypeError as exc: raise ImproperlyConfigured('CELERY_BROKER_TRANSPORT_OPTIONS must be a dict') from exc +# Block Structures + +# upload limits +STUDENT_FILEUPLOAD_MAX_SIZE = ENV_TOKENS.get("STUDENT_FILEUPLOAD_MAX_SIZE", STUDENT_FILEUPLOAD_MAX_SIZE) + # Event tracking -TRACKING_BACKENDS.update(_YAML_TOKENS.get("TRACKING_BACKENDS", {})) -EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update( - _YAML_TOKENS.get("EVENT_TRACKING_BACKENDS", {}) -) +TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {})) +EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update(AUTH_TOKENS.get("EVENT_TRACKING_BACKENDS", {})) EVENT_TRACKING_BACKENDS['segmentio']['OPTIONS']['processors'][0]['OPTIONS']['whitelist'].extend( - EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST + AUTH_TOKENS.get("EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST", [])) +TRACKING_SEGMENTIO_WEBHOOK_SECRET = AUTH_TOKENS.get( + "TRACKING_SEGMENTIO_WEBHOOK_SECRET", + TRACKING_SEGMENTIO_WEBHOOK_SECRET +) +TRACKING_SEGMENTIO_ALLOWED_TYPES = ENV_TOKENS.get("TRACKING_SEGMENTIO_ALLOWED_TYPES", TRACKING_SEGMENTIO_ALLOWED_TYPES) +TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES = ENV_TOKENS.get( + "TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES", + TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES +) +TRACKING_SEGMENTIO_SOURCE_MAP = ENV_TOKENS.get("TRACKING_SEGMENTIO_SOURCE_MAP", TRACKING_SEGMENTIO_SOURCE_MAP) + +# Heartbeat +HEARTBEAT_CELERY_ROUTING_KEY = ENV_TOKENS.get('HEARTBEAT_CELERY_ROUTING_KEY', HEARTBEAT_CELERY_ROUTING_KEY) + +# Student identity verification settings +VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", VERIFY_STUDENT) +DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH = ENV_TOKENS.get( + "DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH", + DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH ) # Grades download -GRADES_DOWNLOAD_ROUTING_KEY = _YAML_TOKENS.get('GRADES_DOWNLOAD_ROUTING_KEY', HIGH_MEM_QUEUE) +GRADES_DOWNLOAD_ROUTING_KEY = ENV_TOKENS.get('GRADES_DOWNLOAD_ROUTING_KEY', HIGH_MEM_QUEUE) + +GRADES_DOWNLOAD = ENV_TOKENS.get("GRADES_DOWNLOAD", GRADES_DOWNLOAD) + +# Rate limit for regrading tasks that a grading policy change can kick off + +# financial reports +FINANCIAL_REPORTS = ENV_TOKENS.get("FINANCIAL_REPORTS", FINANCIAL_REPORTS) + +##### ORA2 ###### +# Prefix for uploads of example-based assessment AI classifiers +# This can be used to separate uploads for different environments +# within the same S3 bucket. +ORA2_FILE_PREFIX = ENV_TOKENS.get("ORA2_FILE_PREFIX", ORA2_FILE_PREFIX) + +##### ACCOUNT LOCKOUT DEFAULT PARAMETERS ##### +MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = ENV_TOKENS.get( + "MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED", MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED +) + +MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get( + "MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS", MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS +) + +##### LOGISTRATION RATE LIMIT SETTINGS ##### +LOGISTRATION_RATELIMIT_RATE = ENV_TOKENS.get('LOGISTRATION_RATELIMIT_RATE', LOGISTRATION_RATELIMIT_RATE) +LOGISTRATION_API_RATELIMIT = ENV_TOKENS.get('LOGISTRATION_API_RATELIMIT', LOGISTRATION_API_RATELIMIT) +LOGIN_AND_REGISTER_FORM_RATELIMIT = ENV_TOKENS.get( + 'LOGIN_AND_REGISTER_FORM_RATELIMIT', LOGIN_AND_REGISTER_FORM_RATELIMIT +) +RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = ENV_TOKENS.get( + 'RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT', RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT +) +RESET_PASSWORD_API_RATELIMIT = ENV_TOKENS.get('RESET_PASSWORD_API_RATELIMIT', RESET_PASSWORD_API_RATELIMIT) + +##### REGISTRATION RATE LIMIT SETTINGS ##### +REGISTRATION_VALIDATION_RATELIMIT = ENV_TOKENS.get( + 'REGISTRATION_VALIDATION_RATELIMIT', REGISTRATION_VALIDATION_RATELIMIT +) + +REGISTRATION_RATELIMIT = ENV_TOKENS.get('REGISTRATION_RATELIMIT', REGISTRATION_RATELIMIT) + +#### PASSWORD POLICY SETTINGS ##### +AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", AUTH_PASSWORD_VALIDATORS) + +### INACTIVITY SETTINGS #### +SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIMEOUT_IN_SECONDS") + +##### LMS DEADLINE DISPLAY TIME_ZONE ####### +TIME_ZONE_DISPLAYED_FOR_DEADLINES = ENV_TOKENS.get("TIME_ZONE_DISPLAYED_FOR_DEADLINES", + TIME_ZONE_DISPLAYED_FOR_DEADLINES) + +#### PROCTORED EXAM SETTINGS #### +PROCTORED_EXAM_VIEWABLE_PAST_DUE = ENV_TOKENS.get('PROCTORED_EXAM_VIEWABLE_PAST_DUE', False) + +##### Third-party auth options ################################################ +ENABLE_REQUIRE_THIRD_PARTY_AUTH = ENV_TOKENS.get('ENABLE_REQUIRE_THIRD_PARTY_AUTH', False) if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): - AUTHENTICATION_BACKENDS = _YAML_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [ + tmp_backends = ENV_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [ 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.linkedin.LinkedinOAuth2', 'social_core.backends.facebook.FacebookOAuth2', @@ -397,66 +649,136 @@ def get_env_setting(setting): 'common.djangoapps.third_party_auth.identityserver3.IdentityServer3', 'common.djangoapps.third_party_auth.saml.SAMLAuthBackend', 'common.djangoapps.third_party_auth.lti.LTIAuthBackend', - ]) + list(AUTHENTICATION_BACKENDS) + ]) - # The reduced session expiry time during the third party login pipeline. (Value in seconds) - SOCIAL_AUTH_PIPELINE_TIMEOUT = _YAML_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600) + AUTHENTICATION_BACKENDS = list(tmp_backends) + list(AUTHENTICATION_BACKENDS) + del tmp_backends - # TODO: Would it be safe to just set this default in common.py, even if ENABLE_THIRD_PARTY_AUTH is False? - SOCIAL_AUTH_LTI_CONSUMER_SECRETS = _YAML_TOKENS.get('SOCIAL_AUTH_LTI_CONSUMER_SECRETS', {}) + # The reduced session expiry time during the third party login pipeline. (Value in seconds) + SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600) + + # Most provider configuration is done via ConfigurationModels but for a few sensitive values + # we allow configuration via AUTH_TOKENS instead (optionally). + # The SAML private/public key values do not need the delimiter lines (such as + # "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----" etc.) but they may be included + # if you want (though it's easier to format the key values as JSON without the delimiters). + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', '') + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', '') + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY_DICT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PRIVATE_KEY_DICT', {}) + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT_DICT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PUBLIC_CERT_DICT', {}) + SOCIAL_AUTH_OAUTH_SECRETS = AUTH_TOKENS.get('SOCIAL_AUTH_OAUTH_SECRETS', {}) + SOCIAL_AUTH_LTI_CONSUMER_SECRETS = AUTH_TOKENS.get('SOCIAL_AUTH_LTI_CONSUMER_SECRETS', {}) # third_party_auth config moved to ConfigurationModels. This is for data migration only: - THIRD_PARTY_AUTH_OLD_CONFIG = _YAML_TOKENS.get('THIRD_PARTY_AUTH', None) + THIRD_PARTY_AUTH_OLD_CONFIG = AUTH_TOKENS.get('THIRD_PARTY_AUTH', None) - # TODO: This logic is somewhat insane. We're not sure if it's intentional or not. We've left it - # as-is for strict backwards compatibility, but it's worth revisiting. - if hours := _YAML_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24): - # If we didn't override the value in YAML, OR we overrode it to a truthy value, - # then update CELERYBEAT_SCHEDULE. + if ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24) is not None: CELERYBEAT_SCHEDULE['refresh-saml-metadata'] = { 'task': 'common.djangoapps.third_party_auth.fetch_saml_metadata', - 'schedule': datetime.timedelta(hours=hours), + 'schedule': datetime.timedelta(hours=ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24)), } # The following can be used to integrate a custom login form with third_party_auth. # It should be a dict where the key is a word passed via ?auth_entry=, and the value is a # dict with an arbitrary 'secret_key' and a 'url'. - THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = _YAML_TOKENS.get('THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS', {}) + THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = AUTH_TOKENS.get('THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS', {}) ##### OAUTH2 Provider ############## -if FEATURES['ENABLE_OAUTH2_PROVIDER']: - OAUTH_ENFORCE_SECURE = True - OAUTH_ENFORCE_CLIENT_SECURE = True +if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): + OAUTH_ENFORCE_SECURE = ENV_TOKENS.get('OAUTH_ENFORCE_SECURE', True) + OAUTH_ENFORCE_CLIENT_SECURE = ENV_TOKENS.get('OAUTH_ENFORCE_CLIENT_SECURE', True) # Defaults for the following are defined in lms.envs.common - OAUTH_EXPIRE_DELTA = datetime.timedelta(days=OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS) - OAUTH_EXPIRE_DELTA_PUBLIC = datetime.timedelta(days=OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS) - -if ( - FEATURES['ENABLE_COURSEWARE_SEARCH'] or - FEATURES['ENABLE_DASHBOARD_SEARCH'] or - FEATURES['ENABLE_COURSE_DISCOVERY'] or - FEATURES['ENABLE_TEAMS'] - ): + OAUTH_EXPIRE_DELTA = datetime.timedelta( + days=ENV_TOKENS.get('OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS', OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS) + ) + OAUTH_EXPIRE_DELTA_PUBLIC = datetime.timedelta( + days=ENV_TOKENS.get('OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS', OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS) + ) + + +##### GOOGLE ANALYTICS IDS ##### +GOOGLE_ANALYTICS_ACCOUNT = AUTH_TOKENS.get('GOOGLE_ANALYTICS_ACCOUNT') +GOOGLE_ANALYTICS_TRACKING_ID = AUTH_TOKENS.get('GOOGLE_ANALYTICS_TRACKING_ID') +GOOGLE_ANALYTICS_LINKEDIN = AUTH_TOKENS.get('GOOGLE_ANALYTICS_LINKEDIN') +GOOGLE_SITE_VERIFICATION_ID = ENV_TOKENS.get('GOOGLE_SITE_VERIFICATION_ID') +GOOGLE_ANALYTICS_4_ID = AUTH_TOKENS.get('GOOGLE_ANALYTICS_4_ID') + +##### BRANCH.IO KEY ##### +BRANCH_IO_KEY = AUTH_TOKENS.get('BRANCH_IO_KEY') + +#### Course Registration Code length #### +REGISTRATION_CODE_LENGTH = ENV_TOKENS.get('REGISTRATION_CODE_LENGTH', 8) + +# Which access.py permission names to check; +# We default this to the legacy permission 'see_exists'. +COURSE_CATALOG_VISIBILITY_PERMISSION = ENV_TOKENS.get( + 'COURSE_CATALOG_VISIBILITY_PERMISSION', + COURSE_CATALOG_VISIBILITY_PERMISSION +) +COURSE_ABOUT_VISIBILITY_PERMISSION = ENV_TOKENS.get( + 'COURSE_ABOUT_VISIBILITY_PERMISSION', + COURSE_ABOUT_VISIBILITY_PERMISSION +) + +DEFAULT_COURSE_VISIBILITY_IN_CATALOG = ENV_TOKENS.get( + 'DEFAULT_COURSE_VISIBILITY_IN_CATALOG', + DEFAULT_COURSE_VISIBILITY_IN_CATALOG +) + +DEFAULT_MOBILE_AVAILABLE = ENV_TOKENS.get( + 'DEFAULT_MOBILE_AVAILABLE', + DEFAULT_MOBILE_AVAILABLE +) + +# Enrollment API Cache Timeout +ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = ENV_TOKENS.get('ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60) + +# Ecommerce Orders API Cache Timeout +ECOMMERCE_ORDERS_API_CACHE_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_ORDERS_API_CACHE_TIMEOUT', 3600) + +if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \ + FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \ + FEATURES.get('ENABLE_COURSE_DISCOVERY') or \ + FEATURES.get('ENABLE_TEAMS'): # Use ElasticSearch as the search engine herein SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" + SEARCH_FILTER_GENERATOR = ENV_TOKENS.get('SEARCH_FILTER_GENERATOR', SEARCH_FILTER_GENERATOR) + +SEARCH_SKIP_INVITATION_ONLY_FILTERING = ENV_TOKENS.get( + 'SEARCH_SKIP_INVITATION_ONLY_FILTERING', + SEARCH_SKIP_INVITATION_ONLY_FILTERING, +) +SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING = ENV_TOKENS.get( + 'SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING', + SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING, +) + +SEARCH_COURSEWARE_CONTENT_LOG_PARAMS = ENV_TOKENS.get( + 'SEARCH_COURSEWARE_CONTENT_LOG_PARAMS', + SEARCH_COURSEWARE_CONTENT_LOG_PARAMS, +) # TODO: Once we have successfully upgraded to ES7, switch this back to ELASTIC_SEARCH_CONFIG. -ELASTIC_SEARCH_CONFIG = _YAML_TOKENS.get('ELASTIC_SEARCH_CONFIG_ES7', [{}]) +ELASTIC_SEARCH_CONFIG = ENV_TOKENS.get('ELASTIC_SEARCH_CONFIG_ES7', [{}]) + +# Facebook app +FACEBOOK_API_VERSION = AUTH_TOKENS.get("FACEBOOK_API_VERSION") +FACEBOOK_APP_SECRET = AUTH_TOKENS.get("FACEBOOK_APP_SECRET") +FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID") -XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES["LICENSING"] -XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = YOUTUBE_API_KEY +XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) +XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) +XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) ##### Custom Courses for EdX ##### -if FEATURES['CUSTOM_COURSES_EDX']: +if FEATURES.get('CUSTOM_COURSES_EDX'): INSTALLED_APPS += ['lms.djangoapps.ccx', 'openedx.core.djangoapps.ccxcon.apps.CCXConnectorConfig'] MODULESTORE_FIELD_OVERRIDE_PROVIDERS += ( 'lms.djangoapps.ccx.overrides.CustomCoursesForEdxOverrideProvider', ) -FIELD_OVERRIDE_PROVIDERS = tuple(FIELD_OVERRIDE_PROVIDERS) - ##### Individual Due Date Extensions ##### -if FEATURES['INDIVIDUAL_DUE_DATES']: +if FEATURES.get('INDIVIDUAL_DUE_DATES'): FIELD_OVERRIDE_PROVIDERS += ( 'lms.djangoapps.courseware.student_field_overrides.IndividualStudentOverrideProvider', ) @@ -477,30 +799,52 @@ def get_env_setting(setting): # PROFILE IMAGE CONFIG PROFILE_IMAGE_DEFAULT_FILENAME = 'images/profiles/default' +PROFILE_IMAGE_SIZES_MAP = ENV_TOKENS.get( + 'PROFILE_IMAGE_SIZES_MAP', + PROFILE_IMAGE_SIZES_MAP +) ##### Credit Provider Integration ##### +CREDIT_PROVIDER_SECRET_KEYS = AUTH_TOKENS.get("CREDIT_PROVIDER_SECRET_KEYS", {}) + ##################### LTI Provider ##################### -if FEATURES['ENABLE_LTI_PROVIDER']: +if FEATURES.get('ENABLE_LTI_PROVIDER'): INSTALLED_APPS.append('lms.djangoapps.lti_provider.apps.LtiProviderConfig') AUTHENTICATION_BACKENDS.append('lms.djangoapps.lti_provider.users.LtiBackend') +LTI_USER_EMAIL_DOMAIN = ENV_TOKENS.get('LTI_USER_EMAIL_DOMAIN', 'lti.example.com') + +# For more info on this, see the notes in common.py +LTI_AGGREGATE_SCORE_PASSBACK_DELAY = ENV_TOKENS.get( + 'LTI_AGGREGATE_SCORE_PASSBACK_DELAY', LTI_AGGREGATE_SCORE_PASSBACK_DELAY +) + ##################### Credit Provider help link #################### #### JWT configuration #### -JWT_AUTH.update(_YAML_TOKENS.get('JWT_AUTH', {})) +JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {})) +JWT_AUTH.update(AUTH_TOKENS.get('JWT_AUTH', {})) -################################ Settings for Credentials Service ################################ +# Offset for pk of courseware.StudentModuleHistoryExtended +STUDENTMODULEHISTORYEXTENDED_OFFSET = ENV_TOKENS.get( + 'STUDENTMODULEHISTORYEXTENDED_OFFSET', STUDENTMODULEHISTORYEXTENDED_OFFSET +) -CREDENTIALS_GENERATION_ROUTING_KEY = _YAML_TOKENS.get('CREDENTIALS_GENERATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) +################################ Settings for Credentials Service ################################ -PROGRAM_CERTIFICATES_ROUTING_KEY = _YAML_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) +CREDENTIALS_GENERATION_ROUTING_KEY = ENV_TOKENS.get('CREDENTIALS_GENERATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = _YAML_TOKENS.get( +# Queue to use for award program certificates +PROGRAM_CERTIFICATES_ROUTING_KEY = ENV_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) +SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = ENV_TOKENS.get( 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', HIGH_PRIORITY_QUEUE ) +API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get('API_ACCESS_MANAGER_EMAIL') +API_ACCESS_FROM_EMAIL = ENV_TOKENS.get('API_ACCESS_FROM_EMAIL') + ############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ###################### # The Open edX Enterprise service is currently hosted via the LMS container/process. # However, for all intents and purposes this service is treated as a standalone IDA. @@ -508,17 +852,47 @@ def get_env_setting(setting): # not find references to them within the edx-platform project. # Publicly-accessible enrollment URL, for use on the client side. -ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = _YAML_TOKENS.get( +ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENV_TOKENS.get( 'ENTERPRISE_PUBLIC_ENROLLMENT_API_URL', (LMS_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH ) # Enrollment URL used on the server-side. -ENTERPRISE_ENROLLMENT_API_URL = _YAML_TOKENS.get( +ENTERPRISE_ENROLLMENT_API_URL = ENV_TOKENS.get( 'ENTERPRISE_ENROLLMENT_API_URL', (LMS_INTERNAL_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH ) +# Enterprise logo image size limit in KB's +ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE = ENV_TOKENS.get( + 'ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE', + ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE +) + +# Course enrollment modes to be hidden in the Enterprise enrollment page +# if the "Hide audit track" flag is enabled for an EnterpriseCustomer +ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES = ENV_TOKENS.get( + 'ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES', + ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES +) + +# A support URL used on Enterprise landing pages for when a warning +# message goes off. +ENTERPRISE_SUPPORT_URL = ENV_TOKENS.get( + 'ENTERPRISE_SUPPORT_URL', + ENTERPRISE_SUPPORT_URL +) + +# A default dictionary to be used for filtering out enterprise customer catalog. +ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER = ENV_TOKENS.get( + 'ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER', + ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER +) +INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT = ENV_TOKENS.get( + 'INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT', + INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT +) + ############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### # The LMS communicates with the Enterprise service via the requests.Session() client # The below environmental settings are utilized by the LMS when interacting with @@ -527,24 +901,97 @@ def get_env_setting(setting): DEFAULT_ENTERPRISE_API_URL = None if LMS_INTERNAL_ROOT_URL is not None: DEFAULT_ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' -ENTERPRISE_API_URL = _YAML_TOKENS.get('ENTERPRISE_API_URL', DEFAULT_ENTERPRISE_API_URL) +ENTERPRISE_API_URL = ENV_TOKENS.get('ENTERPRISE_API_URL', DEFAULT_ENTERPRISE_API_URL) DEFAULT_ENTERPRISE_CONSENT_API_URL = None if LMS_INTERNAL_ROOT_URL is not None: DEFAULT_ENTERPRISE_CONSENT_API_URL = LMS_INTERNAL_ROOT_URL + '/consent/api/v1/' -ENTERPRISE_CONSENT_API_URL = _YAML_TOKENS.get('ENTERPRISE_CONSENT_API_URL', DEFAULT_ENTERPRISE_CONSENT_API_URL) +ENTERPRISE_CONSENT_API_URL = ENV_TOKENS.get('ENTERPRISE_CONSENT_API_URL', DEFAULT_ENTERPRISE_CONSENT_API_URL) + +ENTERPRISE_SERVICE_WORKER_USERNAME = ENV_TOKENS.get( + 'ENTERPRISE_SERVICE_WORKER_USERNAME', + ENTERPRISE_SERVICE_WORKER_USERNAME +) +ENTERPRISE_API_CACHE_TIMEOUT = ENV_TOKENS.get( + 'ENTERPRISE_API_CACHE_TIMEOUT', + ENTERPRISE_API_CACHE_TIMEOUT +) +ENTERPRISE_CATALOG_INTERNAL_ROOT_URL = ENV_TOKENS.get( + 'ENTERPRISE_CATALOG_INTERNAL_ROOT_URL', + ENTERPRISE_CATALOG_INTERNAL_ROOT_URL +) + +CHAT_COMPLETION_API = ENV_TOKENS.get('CHAT_COMPLETION_API', '') +CHAT_COMPLETION_API_KEY = ENV_TOKENS.get('CHAT_COMPLETION_API_KEY', '') +LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT', '') +LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get( + 'LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT', + '' +) +LEARNER_PROGRESS_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_PROGRESS_PROMPT_FOR_ACTIVE_CONTRACT', '') +LEARNER_PROGRESS_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_PROGRESS_PROMPT_FOR_NON_ACTIVE_CONTRACT', '') ############## ENTERPRISE SERVICE LMS CONFIGURATION ################################## # The LMS has some features embedded that are related to the Enterprise service, but # which are not provided by the Enterprise service. These settings override the # base values for the parameters as defined in common.py -ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS = set(ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS) +ENTERPRISE_PLATFORM_WELCOME_TEMPLATE = ENV_TOKENS.get( + 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', + ENTERPRISE_PLATFORM_WELCOME_TEMPLATE +) +ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE = ENV_TOKENS.get( + 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', + ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE +) +ENTERPRISE_TAGLINE = ENV_TOKENS.get( + 'ENTERPRISE_TAGLINE', + ENTERPRISE_TAGLINE +) +ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS = set( + ENV_TOKENS.get( + 'ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS', + ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS + ) +) +BASE_COOKIE_DOMAIN = ENV_TOKENS.get( + 'BASE_COOKIE_DOMAIN', + BASE_COOKIE_DOMAIN +) +SYSTEM_TO_FEATURE_ROLE_MAPPING = ENV_TOKENS.get( + 'SYSTEM_TO_FEATURE_ROLE_MAPPING', + SYSTEM_TO_FEATURE_ROLE_MAPPING +) + +# Add an ICP license for serving content in China if your organization is registered to do so +ICP_LICENSE = ENV_TOKENS.get('ICP_LICENSE', None) +ICP_LICENSE_INFO = ENV_TOKENS.get('ICP_LICENSE_INFO', {}) + +# How long to cache OpenAPI schemas and UI, in seconds. +OPENAPI_CACHE_TIMEOUT = ENV_TOKENS.get('OPENAPI_CACHE_TIMEOUT', 60 * 60) + +########################## Parental controls config ####################### + +# The age at which a learner no longer requires parental consent, or None +# if parental consent is never required. +PARENTAL_CONSENT_AGE_LIMIT = ENV_TOKENS.get( + 'PARENTAL_CONSENT_AGE_LIMIT', + PARENTAL_CONSENT_AGE_LIMIT +) ########################## Extra middleware classes ####################### # Allow extra middleware classes to be added to the app through configuration. -MIDDLEWARE.extend(_YAML_TOKENS.get('EXTRA_MIDDLEWARE_CLASSES', [])) +MIDDLEWARE.extend(ENV_TOKENS.get('EXTRA_MIDDLEWARE_CLASSES', [])) + +################# Settings for the maintenance banner ################# +MAINTENANCE_BANNER_TEXT = ENV_TOKENS.get('MAINTENANCE_BANNER_TEXT', None) + +########################## limiting dashboard courses ###################### +DASHBOARD_COURSE_LIMIT = ENV_TOKENS.get('DASHBOARD_COURSE_LIMIT', None) + +######################## Setting for content libraries ######################## +MAX_BLOCKS_PER_CONTENT_LIBRARY = ENV_TOKENS.get('MAX_BLOCKS_PER_CONTENT_LIBRARY', MAX_BLOCKS_PER_CONTENT_LIBRARY) ########################## Derive Any Derived Settings ####################### @@ -554,15 +1001,23 @@ def get_env_setting(setting): # This is at the bottom because it is going to load more settings after base settings are loaded -# ENV_TOKENS and AUTH_TOKENS are included for reverse compatibility. -# Removing them may break plugins that rely on them. -# Please do not add new references to them... just use `django.conf.settings` instead. -ENV_TOKENS = __config__ -AUTH_TOKENS = __config__ - # Load production.py in plugins add_plugins(__name__, ProjectType.LMS, SettingsType.PRODUCTION) +############## Settings for Completion API ######################### + +# Once a user has watched this percentage of a video, mark it as complete: +# (0.0 = 0%, 1.0 = 100%) +COMPLETION_VIDEO_COMPLETE_PERCENTAGE = ENV_TOKENS.get('COMPLETION_VIDEO_COMPLETE_PERCENTAGE', + COMPLETION_VIDEO_COMPLETE_PERCENTAGE) +COMPLETION_BY_VIEWING_DELAY_MS = ENV_TOKENS.get('COMPLETION_BY_VIEWING_DELAY_MS', + COMPLETION_BY_VIEWING_DELAY_MS) + +################# Settings for brand logos. ################# +LOGO_URL = ENV_TOKENS.get('LOGO_URL', LOGO_URL) +LOGO_URL_PNG = ENV_TOKENS.get('LOGO_URL_PNG', LOGO_URL_PNG) +LOGO_TRADEMARK_URL = ENV_TOKENS.get('LOGO_TRADEMARK_URL', LOGO_TRADEMARK_URL) +FAVICON_URL = ENV_TOKENS.get('FAVICON_URL', FAVICON_URL) ######################## CELERY ROUTING ######################## @@ -622,32 +1077,56 @@ def get_env_setting(setting): } +LOGO_IMAGE_EXTRA_TEXT = ENV_TOKENS.get('LOGO_IMAGE_EXTRA_TEXT', '') + ############## XBlock extra mixins ############################ XBLOCK_MIXINS += tuple(XBLOCK_EXTRA_MIXINS) +############## Settings for course import olx validation ############################ +COURSE_OLX_VALIDATION_STAGE = ENV_TOKENS.get('COURSE_OLX_VALIDATION_STAGE', COURSE_OLX_VALIDATION_STAGE) +COURSE_OLX_VALIDATION_IGNORE_LIST = ENV_TOKENS.get( + 'COURSE_OLX_VALIDATION_IGNORE_LIST', + COURSE_OLX_VALIDATION_IGNORE_LIST +) + +################# show account activate cta after register ######################## +SHOW_ACCOUNT_ACTIVATION_CTA = ENV_TOKENS.get('SHOW_ACCOUNT_ACTIVATION_CTA', SHOW_ACCOUNT_ACTIVATION_CTA) + +################# Discussions micro frontend URL ######################## +DISCUSSIONS_MICROFRONTEND_URL = ENV_TOKENS.get('DISCUSSIONS_MICROFRONTEND_URL', DISCUSSIONS_MICROFRONTEND_URL) + +################### Discussions micro frontend Feedback URL################### +DISCUSSIONS_MFE_FEEDBACK_URL = ENV_TOKENS.get('DISCUSSIONS_MFE_FEEDBACK_URL', DISCUSSIONS_MFE_FEEDBACK_URL) + +############################ AI_TRANSLATIONS URL ################################## +AI_TRANSLATIONS_API_URL = ENV_TOKENS.get('AI_TRANSLATIONS_API_URL', AI_TRANSLATIONS_API_URL) + ############## DRF overrides ############## -REST_FRAMEWORK.update(_YAML_TOKENS.get('REST_FRAMEWORK', {})) +REST_FRAMEWORK.update(ENV_TOKENS.get('REST_FRAMEWORK', {})) ############################# CELERY ############################ -CELERY_IMPORTS.extend(_YAML_TOKENS.get('CELERY_EXTRA_IMPORTS', [])) +CELERY_IMPORTS.extend(ENV_TOKENS.get('CELERY_EXTRA_IMPORTS', [])) # keys for big blue button live provider -# TODO: This should not be in the core platform. If it has to stay for now, though, then we should move these -# defaults into common.py COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = { - "KEY": _YAML_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_KEY'), - "SECRET": _YAML_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_SECRET'), - "URL": _YAML_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL'), + "KEY": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_KEY', None), + "SECRET": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_SECRET', None), + "URL": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL', None), } -############## Event bus producer ############## -EVENT_BUS_PRODUCER_CONFIG = merge_producer_configs( - EVENT_BUS_PRODUCER_CONFIG, - _YAML_TOKENS.get('EVENT_BUS_PRODUCER_CONFIG', {}) -) +AVAILABLE_DISCUSSION_TOURS = ENV_TOKENS.get('AVAILABLE_DISCUSSION_TOURS', []) -##################################################################################################### -# HEY! Don't add anything to the end of this file. -# Add your defaults to common.py instead! -# If you really need to add post-YAML logic, add it above the "Derive Any Derived Settings" section. -###################################################################################################### +############## NOTIFICATIONS EXPIRY ############## +NOTIFICATIONS_EXPIRY = ENV_TOKENS.get('NOTIFICATIONS_EXPIRY', NOTIFICATIONS_EXPIRY) + +############## Event bus producer ############## +EVENT_BUS_PRODUCER_CONFIG = merge_producer_configs(EVENT_BUS_PRODUCER_CONFIG, + ENV_TOKENS.get('EVENT_BUS_PRODUCER_CONFIG', {})) +BEAMER_PRODUCT_ID = ENV_TOKENS.get('BEAMER_PRODUCT_ID', BEAMER_PRODUCT_ID) + +# .. setting_name: DISABLED_COUNTRIES +# .. setting_default: [] +# .. setting_description: List of country codes that should be disabled +# .. for now it wil impact country listing in auth flow and user profile. +# .. eg ['US', 'CA'] +DISABLED_COUNTRIES = ENV_TOKENS.get('DISABLED_COUNTRIES', []) From d9621ba9564744d85f2b9229636889e75c2b78e6 Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Thu, 16 Jan 2025 15:19:47 -0500 Subject: [PATCH 2/9] revert: revert: refactor: Clean up lms/envs/production.py cruft This reintroduces commit 15939232d547b751a06e9cb0c1c39ba78144bade, which was reverted due to a typo. The typo is fixed in the commit immediately following this one. Co-Authored-By: Feanil Patel --- lms/envs/common.py | 28 +- lms/envs/production.py | 875 ++++++++++------------------------------- 2 files changed, 225 insertions(+), 678 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index cb7643c3668e..23e0d12b3dde 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3392,9 +3392,35 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring CSRF_COOKIE_SECURE = False CSRF_TRUSTED_ORIGINS = [] CSRF_TRUSTED_ORIGINS_WITH_SCHEME = [] -CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = '' + +# If setting a cross-domain cookie, it's really important to choose +# a name for the cookie that is DIFFERENT than the cookies used +# by each subdomain. For example, suppose the applications +# at these subdomains are configured to use the following cookie names: +# +# 1) foo.example.com --> "csrftoken" +# 2) baz.example.com --> "csrftoken" +# 3) bar.example.com --> "csrftoken" +# +# For the cross-domain version of the CSRF cookie, you need to choose +# a name DIFFERENT than "csrftoken"; otherwise, the new token configured +# for ".example.com" could conflict with the other cookies, +# non-deterministically causing 403 responses. CROSS_DOMAIN_CSRF_COOKIE_NAME = '' +# When setting the domain for the "cross-domain" version of the CSRF +# cookie, you should choose something like: ".example.com" +# (note the leading dot), where both the referer and the host +# are subdomains of "example.com". +# +# Browser security rules require that +# the cookie domain matches the domain of the server; otherwise +# the cookie won't get set. And once the cookie gets set, the client +# needs to be on a domain that matches the cookie domain, otherwise +# the client won't be able to read the cookie. +CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = '' + + ######################### Django Rest Framework ######################## REST_FRAMEWORK = { diff --git a/lms/envs/production.py b/lms/envs/production.py index addcea0e6028..3ea1c952e479 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -22,7 +22,6 @@ import os import yaml -import django from django.core.exceptions import ImproperlyConfigured from edx_django_utils.plugins import add_plugins from openedx_events.event_bus import merge_producer_configs @@ -44,7 +43,11 @@ def get_env_setting(setting): error_msg = "Set the %s env variable" % setting raise ImproperlyConfigured(error_msg) # lint-amnesty, pylint: disable=raise-missing-from -################################ ALWAYS THE SAME ############################## + +################################################# PRODUCTION DEFAULTS ################################################ +# We configure some defaults (beyond what has already been configured in common.py) before loading the YAML file below. +# DO NOT ADD NEW DEFAULTS HERE! Put any new setting defaults in common.py instead, along with a setting annotation. +# TODO: Move all these defaults into common.py. DEBUG = False DEFAULT_TEMPLATE_ENGINE['OPTIONS']['debug'] = False @@ -58,7 +61,72 @@ def get_env_setting(setting): # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header # for other warnings. SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -################################ END ALWAYS THE SAME ############################## + +CELERY_RESULT_BACKEND = 'django-cache' +BROKER_HEARTBEAT = 60.0 +BROKER_HEARTBEAT_CHECKRATE = 2 +STATIC_ROOT_BASE = None +STATIC_URL_BASE = None +EMAIL_HOST = 'localhost' +EMAIL_PORT = 25 +EMAIL_USE_TLS = False +SESSION_COOKIE_DOMAIN = None +SESSION_COOKIE_HTTPONLY = True +AWS_SES_REGION_NAME = 'us-east-1' +AWS_SES_REGION_ENDPOINT = 'email.us-east-1.amazonaws.com' +REGISTRATION_EMAIL_PATTERNS_ALLOWED = None +LMS_ROOT_URL = None +CMS_BASE = 'studio.edx.org' +CELERY_EVENT_QUEUE_TTL = None +COMPREHENSIVE_THEME_LOCALE_PATHS = [] +PREPEND_LOCALE_PATHS = [] +COURSE_LISTINGS = {} +COMMENTS_SERVICE_URL = '' +COMMENTS_SERVICE_KEY = '' +CERT_QUEUE = 'test-pull' +PYTHON_LIB_FILENAME = 'python_lib.zip' +VIDEO_CDN_URL = {} +HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = {} +AWS_STORAGE_BUCKET_NAME = 'edxuploads' +# Disabling querystring auth instructs Boto to exclude the querystring parameters (e.g. signature, access key) it +# normally appends to every returned URL. +AWS_QUERYSTRING_AUTH = True +AWS_S3_CUSTOM_DOMAIN = 'edxuploads.s3.amazonaws.com' +MONGODB_LOG = {} +ZENDESK_USER = None +ZENDESK_API_KEY = None +EDX_API_KEY = None +CELERY_BROKER_TRANSPORT = "" +CELERY_BROKER_HOSTNAME = "" +CELERY_BROKER_VHOST = "" +CELERY_BROKER_USER = "" +CELERY_BROKER_PASSWORD = "" +BROKER_USE_SSL = False +SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = None +ENABLE_REQUIRE_THIRD_PARTY_AUTH = False +GOOGLE_ANALYTICS_TRACKING_ID = None +GOOGLE_ANALYTICS_LINKEDIN = None +GOOGLE_SITE_VERIFICATION_ID = None +BRANCH_IO_KEY = None +REGISTRATION_CODE_LENGTH = 8 +FACEBOOK_API_VERSION = None +FACEBOOK_APP_SECRET = None +FACEBOOK_APP_ID = None +API_ACCESS_MANAGER_EMAIL = None +API_ACCESS_FROM_EMAIL = None +CHAT_COMPLETION_API = '' +CHAT_COMPLETION_API_KEY = '' +OPENAPI_CACHE_TIMEOUT = 60 * 60 +MAINTENANCE_BANNER_TEXT = None +DASHBOARD_COURSE_LIMIT = None + +# TODO: We believe these were part of the DEPR'd sysadmin dashboard, and can likely be removed. +SSL_AUTH_EMAIL_DOMAIN = "MIT.EDU" +SSL_AUTH_DN_FORMAT_STRING = ( + "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" +) + +####################################################################################################################### # A file path to a YAML file from which to load all the configuration for the edx platform CONFIG_FILE = get_env_setting('LMS_CFG') @@ -66,10 +134,13 @@ def get_env_setting(setting): with codecs.open(CONFIG_FILE, encoding='utf-8') as f: __config__ = yaml.safe_load(f) - # ENV_TOKENS and AUTH_TOKENS are included for reverse compatibility. - # Removing them may break plugins that rely on them. - ENV_TOKENS = __config__ - AUTH_TOKENS = __config__ + # _YAML_TOKENS contains the exact contents of the LMS_CFG YAML file. + # We do splat the entirety of the LMS_CFG YAML file (except KEYS_WITH_MERGED_VALUES) into this module. + # However, for precise backwards compatibility, we need to reference _YAML_TOKENS directly a few times, + # particularly we need to derive Django setting values from YAML values. + # This pattern is confusing and we discourage it. Rather than adding more _YAML_TOKENS references, please + # consider just referencing this module's variables directly. + _YAML_TOKENS = __config__ # Add the key/values from config into the global namespace of this module. # But don't override the FEATURES dict because we do that in an additive way. @@ -82,7 +153,6 @@ def get_env_setting(setting): 'JWT_AUTH', 'CELERY_QUEUES', 'MKTG_URL_LINK_MAP', - 'MKTG_URL_OVERRIDES', 'REST_FRAMEWORK', 'EVENT_BUS_PRODUCER_CONFIG', ] @@ -111,20 +181,11 @@ def get_env_setting(setting): BROKER_POOL_LIMIT = 0 BROKER_CONNECTION_TIMEOUT = 1 -# Allow env to configure celery result backend with default set to django-cache -CELERY_RESULT_BACKEND = ENV_TOKENS.get('CELERY_RESULT_BACKEND', 'django-cache') - -# When the broker is behind an ELB, use a heartbeat to refresh the -# connection and to detect if it has been dropped. -BROKER_HEARTBEAT = ENV_TOKENS.get('BROKER_HEARTBEAT', 60.0) -BROKER_HEARTBEAT_CHECKRATE = ENV_TOKENS.get('BROKER_HEARTBEAT_CHECKRATE', 2) - # Each worker should only fetch one message at a time CELERYD_PREFETCH_MULTIPLIER = 1 # STATIC_ROOT specifies the directory where static files are # collected -STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None) if STATIC_ROOT_BASE: STATIC_ROOT = path(STATIC_ROOT_BASE) WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json" @@ -132,83 +193,34 @@ def get_env_setting(setting): # STATIC_URL_BASE specifies the base url to use for static files -STATIC_URL_BASE = ENV_TOKENS.get('STATIC_URL_BASE', None) if STATIC_URL_BASE: STATIC_URL = STATIC_URL_BASE if not STATIC_URL.endswith("/"): STATIC_URL += "/" -# Allow overriding build profile used by RequireJS with one -# contained on a custom theme -REQUIRE_BUILD_PROFILE = ENV_TOKENS.get('REQUIRE_BUILD_PROFILE', REQUIRE_BUILD_PROFILE) - -# The following variables use (or) instead of the default value inside (get). This is to enforce using the Lazy Text -# values when the variable is an empty string. Therefore, setting these variable as empty text in related -# json files will make the system reads their values from django translation files -PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME') or PLATFORM_NAME -PLATFORM_DESCRIPTION = ENV_TOKENS.get('PLATFORM_DESCRIPTION') or PLATFORM_DESCRIPTION - -DATA_DIR = path(ENV_TOKENS.get('DATA_DIR', DATA_DIR)) -CC_MERCHANT_NAME = ENV_TOKENS.get('CC_MERCHANT_NAME', PLATFORM_NAME) -EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', DATA_DIR / "emails" / "lms") -EMAIL_HOST = ENV_TOKENS.get('EMAIL_HOST', 'localhost') # django default is localhost -EMAIL_PORT = ENV_TOKENS.get('EMAIL_PORT', 25) # django default is 25 -EMAIL_USE_TLS = ENV_TOKENS.get('EMAIL_USE_TLS', False) # django default is False -SITE_NAME = ENV_TOKENS.get('SITE_NAME', SITE_NAME) -SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') -SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True) - -DCS_SESSION_COOKIE_SAMESITE = ENV_TOKENS.get('DCS_SESSION_COOKIE_SAMESITE', DCS_SESSION_COOKIE_SAMESITE) -DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL = ENV_TOKENS.get('DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL', DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL) # lint-amnesty, pylint: disable=line-too-long - -# As django-cookies-samesite package is set to be removed from base requirements when we upgrade to Django 3.2, -# we should follow the settings name provided by Django. -# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-samesite -SESSION_COOKIE_SAMESITE = DCS_SESSION_COOKIE_SAMESITE - -AWS_SES_REGION_NAME = ENV_TOKENS.get('AWS_SES_REGION_NAME', 'us-east-1') -AWS_SES_REGION_ENDPOINT = ENV_TOKENS.get('AWS_SES_REGION_ENDPOINT', 'email.us-east-1.amazonaws.com') +DATA_DIR = path(DATA_DIR) +CC_MERCHANT_NAME = _YAML_TOKENS.get('CC_MERCHANT_NAME', PLATFORM_NAME) +EMAIL_FILE_PATH = _YAML_TOKENS.get('EMAIL_FILE_PATH', DATA_DIR / "emails" / "lms") -REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') - -LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL') -LMS_INTERNAL_ROOT_URL = ENV_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) +# TODO: This was for backwards compatibility back when installed django-cookie-samesite (not since 2022). +# The DCS_ version of the setting can be DEPR'd at this point. +SESSION_COOKIE_SAMESITE = DCS_SESSION_COOKIE_SAMESITE -# List of logout URIs for each IDA that the learner should be logged out of when they logout of the LMS. Only applies to -# IDA for which the social auth flow uses DOT (Django OAuth Toolkit). -IDA_LOGOUT_URI_LIST = ENV_TOKENS.get('IDA_LOGOUT_URI_LIST', []) +LMS_INTERNAL_ROOT_URL = _YAML_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) -ENV_FEATURES = ENV_TOKENS.get('FEATURES', {}) -for feature, value in ENV_FEATURES.items(): +for feature, value in _YAML_TOKENS.get('FEATURES', {}).items(): FEATURES[feature] = value -CMS_BASE = ENV_TOKENS.get('CMS_BASE', 'studio.edx.org') - ALLOWED_HOSTS = [ "*", - ENV_TOKENS.get('LMS_BASE'), + _YAML_TOKENS.get('LMS_BASE'), FEATURES['PREVIEW_LMS_BASE'], ] -# Sometimes, OAuth2 clients want the user to redirect back to their site after logout. But to determine if the given -# redirect URL/path is safe for redirection, the following variable is used by edX. -LOGIN_REDIRECT_WHITELIST = ENV_TOKENS.get( - 'LOGIN_REDIRECT_WHITELIST', - LOGIN_REDIRECT_WHITELIST -) - -# allow for environments to specify what cookie name our login subsystem should use -# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can -# happen with some browsers (e.g. Firefox) -if ENV_TOKENS.get('SESSION_COOKIE_NAME', None): - # NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str() - SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME')) - # This is the domain that is used to set shared cookies between various sub-domains. # By default, it's set to the same thing as the SESSION_COOKIE_DOMAIN, but we want to make it overrideable. -SHARED_COOKIE_DOMAIN = ENV_TOKENS.get('SHARED_COOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) +SHARED_COOKIE_DOMAIN = _YAML_TOKENS.get('SHARED_kCOOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) -CACHES = ENV_TOKENS.get('CACHES', CACHES) # Cache used for location mapping -- called many times with the same key/value # in a given request. if 'loc_cache' not in CACHES: @@ -224,206 +236,76 @@ def get_env_setting(setting): # we need to run asset collection twice, once for local disk and once for S3. # Once we have migrated to service assets off S3, then we can convert this back to # managed by the yaml file contents -STATICFILES_STORAGE = os.environ.get('STATICFILES_STORAGE', ENV_TOKENS.get('STATICFILES_STORAGE', STATICFILES_STORAGE)) - -# Load all AWS_ prefixed variables to allow an S3Boto3Storage to be configured -_locals = locals() -for key, value in ENV_TOKENS.items(): - if key.startswith('AWS_'): - _locals[key] = value - -# Currency -PAID_COURSE_REGISTRATION_CURRENCY = ENV_TOKENS.get('PAID_COURSE_REGISTRATION_CURRENCY', - PAID_COURSE_REGISTRATION_CURRENCY) # We want Bulk Email running on the high-priority queue, so we define the # routing key that points to it. At the moment, the name is the same. # We have to reset the value here, since we have changed the value of the queue name. -BULK_EMAIL_ROUTING_KEY = ENV_TOKENS.get('BULK_EMAIL_ROUTING_KEY', HIGH_PRIORITY_QUEUE) +BULK_EMAIL_ROUTING_KEY = _YAML_TOKENS.get('BULK_EMAIL_ROUTING_KEY', HIGH_PRIORITY_QUEUE) # We can run smaller jobs on the low priority queue. See note above for why # we have to reset the value here. -BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = ENV_TOKENS.get('BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', DEFAULT_PRIORITY_QUEUE) +BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = _YAML_TOKENS.get('BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', DEFAULT_PRIORITY_QUEUE) # Queue to use for expiring old entitlements -ENTITLEMENTS_EXPIRATION_ROUTING_KEY = ENV_TOKENS.get('ENTITLEMENTS_EXPIRATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) +ENTITLEMENTS_EXPIRATION_ROUTING_KEY = _YAML_TOKENS.get('ENTITLEMENTS_EXPIRATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -# Message expiry time in seconds -CELERY_EVENT_QUEUE_TTL = ENV_TOKENS.get('CELERY_EVENT_QUEUE_TTL', None) - -# Allow CELERY_QUEUES to be overwritten by ENV_TOKENS, -ENV_CELERY_QUEUES = ENV_TOKENS.get('CELERY_QUEUES', None) -if ENV_CELERY_QUEUES: - CELERY_QUEUES = {queue: {} for queue in ENV_CELERY_QUEUES} +# Build a CELERY_QUEUES dict the way that celery expects, based on a couple lists of queue names from the YAML. +_YAML_CELERY_QUEUES = _YAML_TOKENS.get('CELERY_QUEUES', None) +if _YAML_CELERY_QUEUES: + CELERY_QUEUES = {queue: {} for queue in _YAML_CELERY_QUEUES} # Then add alternate environment queues -ALTERNATE_QUEUE_ENVS = ENV_TOKENS.get('ALTERNATE_WORKER_QUEUES', '').split() +_YAML_ALTERNATE_WORKER_QUEUES = _YAML_TOKENS.get('ALTERNATE_WORKER_QUEUES', '').split() ALTERNATE_QUEUES = [ DEFAULT_PRIORITY_QUEUE.replace(QUEUE_VARIANT, alternate + '.') - for alternate in ALTERNATE_QUEUE_ENVS + for alternate in _YAML_ALTERNATE_WORKER_QUEUES ] + CELERY_QUEUES.update( { alternate: {} for alternate in ALTERNATE_QUEUES - if alternate not in list(CELERY_QUEUES.keys()) + if alternate not in CELERY_QUEUES.keys() } ) -# following setting is for backward compatibility -if ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', None): - COMPREHENSIVE_THEME_DIR = ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR') - - -# COMPREHENSIVE_THEME_LOCALE_PATHS contain the paths to themes locale directories e.g. -# "COMPREHENSIVE_THEME_LOCALE_PATHS" : [ -# "/edx/src/edx-themes/conf/locale" -# ], -COMPREHENSIVE_THEME_LOCALE_PATHS = ENV_TOKENS.get('COMPREHENSIVE_THEME_LOCALE_PATHS', []) - - -# PREPEND_LOCALE_PATHS contain the paths to locale directories to load first e.g. -# "PREPEND_LOCALE_PATHS" : [ -# "/edx/my-locale" -# ], -PREPEND_LOCALE_PATHS = ENV_TOKENS.get('PREPEND_LOCALE_PATHS', []) - - -MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {})) -ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS = ENV_TOKENS.get( - 'ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS', - ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS -) -# Marketing link overrides -MKTG_URL_OVERRIDES.update(ENV_TOKENS.get('MKTG_URL_OVERRIDES', MKTG_URL_OVERRIDES)) +MKTG_URL_LINK_MAP.update(_YAML_TOKENS.get('MKTG_URL_LINK_MAP', {})) # Intentional defaults. -ID_VERIFICATION_SUPPORT_LINK = ENV_TOKENS.get('ID_VERIFICATION_SUPPORT_LINK', SUPPORT_SITE_LINK) -PASSWORD_RESET_SUPPORT_LINK = ENV_TOKENS.get('PASSWORD_RESET_SUPPORT_LINK', SUPPORT_SITE_LINK) -ACTIVATION_EMAIL_SUPPORT_LINK = ENV_TOKENS.get('ACTIVATION_EMAIL_SUPPORT_LINK', SUPPORT_SITE_LINK) -LOGIN_ISSUE_SUPPORT_LINK = ENV_TOKENS.get('LOGIN_ISSUE_SUPPORT_LINK', SUPPORT_SITE_LINK) +ID_VERIFICATION_SUPPORT_LINK = _YAML_TOKENS.get('ID_VERIFICATION_SUPPORT_LINK', SUPPORT_SITE_LINK) +PASSWORD_RESET_SUPPORT_LINK = _YAML_TOKENS.get('PASSWORD_RESET_SUPPORT_LINK', SUPPORT_SITE_LINK) +ACTIVATION_EMAIL_SUPPORT_LINK = _YAML_TOKENS.get('ACTIVATION_EMAIL_SUPPORT_LINK', SUPPORT_SITE_LINK) +LOGIN_ISSUE_SUPPORT_LINK = _YAML_TOKENS.get('LOGIN_ISSUE_SUPPORT_LINK', SUPPORT_SITE_LINK) # Timezone overrides -TIME_ZONE = ENV_TOKENS.get('CELERY_TIMEZONE', CELERY_TIMEZONE) +TIME_ZONE = CELERY_TIMEZONE # Translation overrides LANGUAGE_DICT = dict(LANGUAGES) -LANGUAGE_COOKIE_NAME = ENV_TOKENS.get('LANGUAGE_COOKIE', None) or ENV_TOKENS.get( - 'LANGUAGE_COOKIE_NAME', LANGUAGE_COOKIE_NAME) +LANGUAGE_COOKIE_NAME = _YAML_TOKENS.get('LANGUAGE_COOKIE') or LANGUAGE_COOKIE_NAME # Additional installed apps -for app in ENV_TOKENS.get('ADDL_INSTALLED_APPS', []): +for app in _YAML_TOKENS.get('ADDL_INSTALLED_APPS', []): INSTALLED_APPS.append(app) - -local_loglevel = ENV_TOKENS.get('LOCAL_LOGLEVEL', 'INFO') -LOG_DIR = ENV_TOKENS.get('LOG_DIR', LOG_DIR) - -LOGGING = get_logger_config(LOG_DIR, - logging_env=ENV_TOKENS.get('LOGGING_ENV', LOGGING_ENV), - local_loglevel=local_loglevel, - service_variant=SERVICE_VARIANT) - -COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {}) -COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '') -COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '') -CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull') - -# Python lib settings -PYTHON_LIB_FILENAME = ENV_TOKENS.get('PYTHON_LIB_FILENAME', 'python_lib.zip') - -# Code jail settings -for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items(): - oldvalue = CODE_JAIL.get(name) - if isinstance(oldvalue, dict): - for subname, subvalue in value.items(): - oldvalue[subname] = subvalue - else: - CODE_JAIL[name] = value - -COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) - -# Event Tracking -if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS: - TRACKING_IGNORE_URL_PATTERNS = ENV_TOKENS.get("TRACKING_IGNORE_URL_PATTERNS") - -# SSL external authentication settings -SSL_AUTH_EMAIL_DOMAIN = ENV_TOKENS.get("SSL_AUTH_EMAIL_DOMAIN", "MIT.EDU") -SSL_AUTH_DN_FORMAT_STRING = ENV_TOKENS.get( - "SSL_AUTH_DN_FORMAT_STRING", - "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" +LOGGING = get_logger_config( + LOG_DIR, + logging_env=LOGGING_ENV, + local_loglevel=LOCAL_LOGLEVEL, + service_variant=SERVICE_VARIANT, ) -# Video Caching. Pairing country codes with CDN URLs. -# Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='} -VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {}) - -# Determines whether the CSRF token can be transported on -# unencrypted channels. It is set to False here for backward compatibility, -# but it is highly recommended that this is True for environments accessed -# by end users. -CSRF_COOKIE_SECURE = ENV_TOKENS.get('CSRF_COOKIE_SECURE', False) - # Determines which origins are trusted for unsafe requests eg. POST requests. -CSRF_TRUSTED_ORIGINS = ENV_TOKENS.get('CSRF_TRUSTED_ORIGINS', []) -# values are already updated above with default CSRF_TRUSTED_ORIGINS values but in -# case of new django version these values will override. -if django.VERSION[0] >= 4: # for greater than django 3.2 use schemes. - CSRF_TRUSTED_ORIGINS = ENV_TOKENS.get('CSRF_TRUSTED_ORIGINS_WITH_SCHEME', []) +CSRF_TRUSTED_ORIGINS = _YAML_TOKENS.get('CSRF_TRUSTED_ORIGINS_WITH_SCHEME', []) -############# CORS headers for cross-domain requests ################# - -if FEATURES.get('ENABLE_CORS_HEADERS') or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'): +if FEATURES['ENABLE_CORS_HEADERS'] or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'): CORS_ALLOW_CREDENTIALS = True - CORS_ORIGIN_WHITELIST = ENV_TOKENS.get('CORS_ORIGIN_WHITELIST', ()) - - CORS_ORIGIN_ALLOW_ALL = ENV_TOKENS.get('CORS_ORIGIN_ALLOW_ALL', False) - CORS_ALLOW_INSECURE = ENV_TOKENS.get('CORS_ALLOW_INSECURE', False) - - # If setting a cross-domain cookie, it's really important to choose - # a name for the cookie that is DIFFERENT than the cookies used - # by each subdomain. For example, suppose the applications - # at these subdomains are configured to use the following cookie names: - # - # 1) foo.example.com --> "csrftoken" - # 2) baz.example.com --> "csrftoken" - # 3) bar.example.com --> "csrftoken" - # - # For the cross-domain version of the CSRF cookie, you need to choose - # a name DIFFERENT than "csrftoken"; otherwise, the new token configured - # for ".example.com" could conflict with the other cookies, - # non-deterministically causing 403 responses. - # - # Because of the way Django stores cookies, the cookie name MUST - # be a `str`, not unicode. Otherwise there will `TypeError`s will be raised - # when Django tries to call the unicode `translate()` method with the wrong - # number of parameters. - CROSS_DOMAIN_CSRF_COOKIE_NAME = str(ENV_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_NAME')) - - # When setting the domain for the "cross-domain" version of the CSRF - # cookie, you should choose something like: ".example.com" - # (note the leading dot), where both the referer and the host - # are subdomains of "example.com". - # - # Browser security rules require that - # the cookie domain matches the domain of the server; otherwise - # the cookie won't get set. And once the cookie gets set, the client - # needs to be on a domain that matches the cookie domain, otherwise - # the client won't be able to read the cookie. - CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = ENV_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_DOMAIN') - - -# Field overrides. To use the IDDE feature, add -# 'courseware.student_field_overrides.IndividualStudentOverrideProvider'. -FIELD_OVERRIDE_PROVIDERS = tuple(ENV_TOKENS.get('FIELD_OVERRIDE_PROVIDERS', [])) - -############### XBlock filesystem field config ########## -if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None: - DJFS = AUTH_TOKENS['DJFS'] - -############### Module Store Items ########## -HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENV_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', {}) + CORS_ORIGIN_WHITELIST = _YAML_TOKENS.get('CORS_ORIGIN_WHITELIST', ()) + CORS_ORIGIN_ALLOW_ALL = _YAML_TOKENS.get('CORS_ORIGIN_ALLOW_ALL', False) + CORS_ALLOW_INSECURE = _YAML_TOKENS.get('CORS_ALLOW_INSECURE', False) + CROSS_DOMAIN_CSRF_COOKIE_DOMAIN = _YAML_TOKENS.get('CROSS_DOMAIN_CSRF_COOKIE_DOMAIN') + # PREVIEW DOMAIN must be present in HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS for the preview to show draft changes if 'PREVIEW_LMS_BASE' in FEATURES and FEATURES['PREVIEW_LMS_BASE'] != '': PREVIEW_DOMAIN = FEATURES['PREVIEW_LMS_BASE'].split(':')[0] @@ -432,26 +314,11 @@ def get_env_setting(setting): PREVIEW_DOMAIN: 'draft-preferred' }) -MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ENV_TOKENS.get( - 'MODULESTORE_FIELD_OVERRIDE_PROVIDERS', - MODULESTORE_FIELD_OVERRIDE_PROVIDERS -) - -XBLOCK_FIELD_DATA_WRAPPERS = ENV_TOKENS.get( - 'XBLOCK_FIELD_DATA_WRAPPERS', - XBLOCK_FIELD_DATA_WRAPPERS -) - ############### Mixed Related(Secure/Not-Secure) Items ########## -LMS_SEGMENT_KEY = AUTH_TOKENS.get('SEGMENT_KEY') - -SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] +LMS_SEGMENT_KEY = _YAML_TOKENS.get('SEGMENT_KEY') -AWS_ACCESS_KEY_ID = AUTH_TOKENS.get("AWS_ACCESS_KEY_ID", AWS_ACCESS_KEY_ID) if AWS_ACCESS_KEY_ID == "": AWS_ACCESS_KEY_ID = None - -AWS_SECRET_ACCESS_KEY = AUTH_TOKENS.get("AWS_SECRET_ACCESS_KEY", AWS_SECRET_ACCESS_KEY) if AWS_SECRET_ACCESS_KEY == "": AWS_SECRET_ACCESS_KEY = None @@ -460,24 +327,10 @@ def get_env_setting(setting): # same with upcoming version setting it to `public-read`. AWS_DEFAULT_ACL = 'public-read' AWS_BUCKET_ACL = AWS_DEFAULT_ACL -AWS_STORAGE_BUCKET_NAME = AUTH_TOKENS.get('AWS_STORAGE_BUCKET_NAME', 'edxuploads') -# Disabling querystring auth instructs Boto to exclude the querystring parameters (e.g. signature, access key) it -# normally appends to every returned URL. -AWS_QUERYSTRING_AUTH = AUTH_TOKENS.get('AWS_QUERYSTRING_AUTH', True) -AWS_S3_CUSTOM_DOMAIN = AUTH_TOKENS.get('AWS_S3_CUSTOM_DOMAIN', 'edxuploads.s3.amazonaws.com') - -if AUTH_TOKENS.get('DEFAULT_FILE_STORAGE'): - DEFAULT_FILE_STORAGE = AUTH_TOKENS.get('DEFAULT_FILE_STORAGE') -elif AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: +# Change to S3Boto3 if we haven't specified another default storage AND we have specified AWS creds. +if (not _YAML_TOKENS.get('DEFAULT_FILE_STORAGE')) and AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' -else: - DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' - - -# If there is a database called 'read_replica', you can use the use_read_replica_if_available -# function in util/query.py, which is useful for very large database reads -DATABASES = AUTH_TOKENS.get('DATABASES', DATABASES) # The normal database user does not have enough permissions to run migrations. # Migrations are run with separate credentials, given as DB_MIGRATION_* @@ -493,11 +346,9 @@ def get_env_setting(setting): 'PORT': os.environ.get('DB_MIGRATION_PORT', database['PORT']), }) -XQUEUE_INTERFACE = AUTH_TOKENS.get('XQUEUE_INTERFACE', XQUEUE_INTERFACE) - # Get the MODULESTORE from auth.json, but if it doesn't exist, # use the one from common.py -MODULESTORE = convert_module_store_setting_if_needed(AUTH_TOKENS.get('MODULESTORE', MODULESTORE)) +MODULESTORE = convert_module_store_setting_if_needed(_YAML_TOKENS.get('MODULESTORE', MODULESTORE)) # After conversion above, the modulestore will have a "stores" list with all defined stores, for all stores, add the # fs_root entry to derived collection so that if it's a callable it can be resolved. We need to do this because the @@ -510,137 +361,34 @@ def get_env_setting(setting): if 'OPTIONS' in store and 'fs_root' in store["OPTIONS"]: derived_collection_entry('MODULESTORE', 'default', 'OPTIONS', 'stores', idx, 'OPTIONS', 'fs_root') -MONGODB_LOG = AUTH_TOKENS.get('MONGODB_LOG', {}) - -EMAIL_HOST_USER = AUTH_TOKENS.get('EMAIL_HOST_USER', '') # django default is '' -EMAIL_HOST_PASSWORD = AUTH_TOKENS.get('EMAIL_HOST_PASSWORD', '') # django default is '' - -# Analytics API -ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", ANALYTICS_API_KEY) -ANALYTICS_API_URL = ENV_TOKENS.get("ANALYTICS_API_URL", ANALYTICS_API_URL) - -# Zendesk -ZENDESK_USER = AUTH_TOKENS.get("ZENDESK_USER") -ZENDESK_API_KEY = AUTH_TOKENS.get("ZENDESK_API_KEY") - -# API Key for inbound requests from Notifier service -EDX_API_KEY = AUTH_TOKENS.get("EDX_API_KEY") - -# Celery Broker -CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "") -CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "") -CELERY_BROKER_VHOST = ENV_TOKENS.get("CELERY_BROKER_VHOST", "") -CELERY_BROKER_USER = AUTH_TOKENS.get("CELERY_BROKER_USER", "") -CELERY_BROKER_PASSWORD = AUTH_TOKENS.get("CELERY_BROKER_PASSWORD", "") - BROKER_URL = "{}://{}:{}@{}/{}".format(CELERY_BROKER_TRANSPORT, CELERY_BROKER_USER, CELERY_BROKER_PASSWORD, CELERY_BROKER_HOSTNAME, CELERY_BROKER_VHOST) -BROKER_USE_SSL = ENV_TOKENS.get('CELERY_BROKER_USE_SSL', False) - try: BROKER_TRANSPORT_OPTIONS = { 'fanout_patterns': True, 'fanout_prefix': True, - **ENV_TOKENS.get('CELERY_BROKER_TRANSPORT_OPTIONS', {}) + **_YAML_TOKENS.get('CELERY_BROKER_TRANSPORT_OPTIONS', {}) } except TypeError as exc: raise ImproperlyConfigured('CELERY_BROKER_TRANSPORT_OPTIONS must be a dict') from exc -# Block Structures - -# upload limits -STUDENT_FILEUPLOAD_MAX_SIZE = ENV_TOKENS.get("STUDENT_FILEUPLOAD_MAX_SIZE", STUDENT_FILEUPLOAD_MAX_SIZE) - # Event tracking -TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {})) -EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update(AUTH_TOKENS.get("EVENT_TRACKING_BACKENDS", {})) -EVENT_TRACKING_BACKENDS['segmentio']['OPTIONS']['processors'][0]['OPTIONS']['whitelist'].extend( - AUTH_TOKENS.get("EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST", [])) -TRACKING_SEGMENTIO_WEBHOOK_SECRET = AUTH_TOKENS.get( - "TRACKING_SEGMENTIO_WEBHOOK_SECRET", - TRACKING_SEGMENTIO_WEBHOOK_SECRET -) -TRACKING_SEGMENTIO_ALLOWED_TYPES = ENV_TOKENS.get("TRACKING_SEGMENTIO_ALLOWED_TYPES", TRACKING_SEGMENTIO_ALLOWED_TYPES) -TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES = ENV_TOKENS.get( - "TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES", - TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES +TRACKING_BACKENDS.update(_YAML_TOKENS.get("TRACKING_BACKENDS", {})) +EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update( + _YAML_TOKENS.get("EVENT_TRACKING_BACKENDS", {}) ) -TRACKING_SEGMENTIO_SOURCE_MAP = ENV_TOKENS.get("TRACKING_SEGMENTIO_SOURCE_MAP", TRACKING_SEGMENTIO_SOURCE_MAP) - -# Heartbeat -HEARTBEAT_CELERY_ROUTING_KEY = ENV_TOKENS.get('HEARTBEAT_CELERY_ROUTING_KEY', HEARTBEAT_CELERY_ROUTING_KEY) - -# Student identity verification settings -VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", VERIFY_STUDENT) -DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH = ENV_TOKENS.get( - "DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH", - DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH +EVENT_TRACKING_BACKENDS['segmentio']['OPTIONS']['processors'][0]['OPTIONS']['whitelist'].extend( + EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST ) # Grades download -GRADES_DOWNLOAD_ROUTING_KEY = ENV_TOKENS.get('GRADES_DOWNLOAD_ROUTING_KEY', HIGH_MEM_QUEUE) - -GRADES_DOWNLOAD = ENV_TOKENS.get("GRADES_DOWNLOAD", GRADES_DOWNLOAD) - -# Rate limit for regrading tasks that a grading policy change can kick off - -# financial reports -FINANCIAL_REPORTS = ENV_TOKENS.get("FINANCIAL_REPORTS", FINANCIAL_REPORTS) - -##### ORA2 ###### -# Prefix for uploads of example-based assessment AI classifiers -# This can be used to separate uploads for different environments -# within the same S3 bucket. -ORA2_FILE_PREFIX = ENV_TOKENS.get("ORA2_FILE_PREFIX", ORA2_FILE_PREFIX) - -##### ACCOUNT LOCKOUT DEFAULT PARAMETERS ##### -MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = ENV_TOKENS.get( - "MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED", MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED -) - -MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get( - "MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS", MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS -) - -##### LOGISTRATION RATE LIMIT SETTINGS ##### -LOGISTRATION_RATELIMIT_RATE = ENV_TOKENS.get('LOGISTRATION_RATELIMIT_RATE', LOGISTRATION_RATELIMIT_RATE) -LOGISTRATION_API_RATELIMIT = ENV_TOKENS.get('LOGISTRATION_API_RATELIMIT', LOGISTRATION_API_RATELIMIT) -LOGIN_AND_REGISTER_FORM_RATELIMIT = ENV_TOKENS.get( - 'LOGIN_AND_REGISTER_FORM_RATELIMIT', LOGIN_AND_REGISTER_FORM_RATELIMIT -) -RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = ENV_TOKENS.get( - 'RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT', RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT -) -RESET_PASSWORD_API_RATELIMIT = ENV_TOKENS.get('RESET_PASSWORD_API_RATELIMIT', RESET_PASSWORD_API_RATELIMIT) - -##### REGISTRATION RATE LIMIT SETTINGS ##### -REGISTRATION_VALIDATION_RATELIMIT = ENV_TOKENS.get( - 'REGISTRATION_VALIDATION_RATELIMIT', REGISTRATION_VALIDATION_RATELIMIT -) - -REGISTRATION_RATELIMIT = ENV_TOKENS.get('REGISTRATION_RATELIMIT', REGISTRATION_RATELIMIT) - -#### PASSWORD POLICY SETTINGS ##### -AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", AUTH_PASSWORD_VALIDATORS) - -### INACTIVITY SETTINGS #### -SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIMEOUT_IN_SECONDS") - -##### LMS DEADLINE DISPLAY TIME_ZONE ####### -TIME_ZONE_DISPLAYED_FOR_DEADLINES = ENV_TOKENS.get("TIME_ZONE_DISPLAYED_FOR_DEADLINES", - TIME_ZONE_DISPLAYED_FOR_DEADLINES) - -#### PROCTORED EXAM SETTINGS #### -PROCTORED_EXAM_VIEWABLE_PAST_DUE = ENV_TOKENS.get('PROCTORED_EXAM_VIEWABLE_PAST_DUE', False) - -##### Third-party auth options ################################################ -ENABLE_REQUIRE_THIRD_PARTY_AUTH = ENV_TOKENS.get('ENABLE_REQUIRE_THIRD_PARTY_AUTH', False) +GRADES_DOWNLOAD_ROUTING_KEY = _YAML_TOKENS.get('GRADES_DOWNLOAD_ROUTING_KEY', HIGH_MEM_QUEUE) if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): - tmp_backends = ENV_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [ + AUTHENTICATION_BACKENDS = _YAML_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [ 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.linkedin.LinkedinOAuth2', 'social_core.backends.facebook.FacebookOAuth2', @@ -649,136 +397,66 @@ def get_env_setting(setting): 'common.djangoapps.third_party_auth.identityserver3.IdentityServer3', 'common.djangoapps.third_party_auth.saml.SAMLAuthBackend', 'common.djangoapps.third_party_auth.lti.LTIAuthBackend', - ]) - - AUTHENTICATION_BACKENDS = list(tmp_backends) + list(AUTHENTICATION_BACKENDS) - del tmp_backends + ]) + list(AUTHENTICATION_BACKENDS) # The reduced session expiry time during the third party login pipeline. (Value in seconds) - SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600) - - # Most provider configuration is done via ConfigurationModels but for a few sensitive values - # we allow configuration via AUTH_TOKENS instead (optionally). - # The SAML private/public key values do not need the delimiter lines (such as - # "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----" etc.) but they may be included - # if you want (though it's easier to format the key values as JSON without the delimiters). - SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', '') - SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', '') - SOCIAL_AUTH_SAML_SP_PRIVATE_KEY_DICT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PRIVATE_KEY_DICT', {}) - SOCIAL_AUTH_SAML_SP_PUBLIC_CERT_DICT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PUBLIC_CERT_DICT', {}) - SOCIAL_AUTH_OAUTH_SECRETS = AUTH_TOKENS.get('SOCIAL_AUTH_OAUTH_SECRETS', {}) - SOCIAL_AUTH_LTI_CONSUMER_SECRETS = AUTH_TOKENS.get('SOCIAL_AUTH_LTI_CONSUMER_SECRETS', {}) + SOCIAL_AUTH_PIPELINE_TIMEOUT = _YAML_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600) + + # TODO: Would it be safe to just set this default in common.py, even if ENABLE_THIRD_PARTY_AUTH is False? + SOCIAL_AUTH_LTI_CONSUMER_SECRETS = _YAML_TOKENS.get('SOCIAL_AUTH_LTI_CONSUMER_SECRETS', {}) # third_party_auth config moved to ConfigurationModels. This is for data migration only: - THIRD_PARTY_AUTH_OLD_CONFIG = AUTH_TOKENS.get('THIRD_PARTY_AUTH', None) + THIRD_PARTY_AUTH_OLD_CONFIG = _YAML_TOKENS.get('THIRD_PARTY_AUTH', None) - if ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24) is not None: + # TODO: This logic is somewhat insane. We're not sure if it's intentional or not. We've left it + # as-is for strict backwards compatibility, but it's worth revisiting. + if hours := _YAML_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24): + # If we didn't override the value in YAML, OR we overrode it to a truthy value, + # then update CELERYBEAT_SCHEDULE. CELERYBEAT_SCHEDULE['refresh-saml-metadata'] = { 'task': 'common.djangoapps.third_party_auth.fetch_saml_metadata', - 'schedule': datetime.timedelta(hours=ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24)), + 'schedule': datetime.timedelta(hours=hours), } # The following can be used to integrate a custom login form with third_party_auth. # It should be a dict where the key is a word passed via ?auth_entry=, and the value is a # dict with an arbitrary 'secret_key' and a 'url'. - THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = AUTH_TOKENS.get('THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS', {}) + THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = _YAML_TOKENS.get('THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS', {}) ##### OAUTH2 Provider ############## -if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): - OAUTH_ENFORCE_SECURE = ENV_TOKENS.get('OAUTH_ENFORCE_SECURE', True) - OAUTH_ENFORCE_CLIENT_SECURE = ENV_TOKENS.get('OAUTH_ENFORCE_CLIENT_SECURE', True) +if FEATURES['ENABLE_OAUTH2_PROVIDER']: + OAUTH_ENFORCE_SECURE = True + OAUTH_ENFORCE_CLIENT_SECURE = True # Defaults for the following are defined in lms.envs.common - OAUTH_EXPIRE_DELTA = datetime.timedelta( - days=ENV_TOKENS.get('OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS', OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS) - ) - OAUTH_EXPIRE_DELTA_PUBLIC = datetime.timedelta( - days=ENV_TOKENS.get('OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS', OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS) - ) - - -##### GOOGLE ANALYTICS IDS ##### -GOOGLE_ANALYTICS_ACCOUNT = AUTH_TOKENS.get('GOOGLE_ANALYTICS_ACCOUNT') -GOOGLE_ANALYTICS_TRACKING_ID = AUTH_TOKENS.get('GOOGLE_ANALYTICS_TRACKING_ID') -GOOGLE_ANALYTICS_LINKEDIN = AUTH_TOKENS.get('GOOGLE_ANALYTICS_LINKEDIN') -GOOGLE_SITE_VERIFICATION_ID = ENV_TOKENS.get('GOOGLE_SITE_VERIFICATION_ID') -GOOGLE_ANALYTICS_4_ID = AUTH_TOKENS.get('GOOGLE_ANALYTICS_4_ID') - -##### BRANCH.IO KEY ##### -BRANCH_IO_KEY = AUTH_TOKENS.get('BRANCH_IO_KEY') - -#### Course Registration Code length #### -REGISTRATION_CODE_LENGTH = ENV_TOKENS.get('REGISTRATION_CODE_LENGTH', 8) - -# Which access.py permission names to check; -# We default this to the legacy permission 'see_exists'. -COURSE_CATALOG_VISIBILITY_PERMISSION = ENV_TOKENS.get( - 'COURSE_CATALOG_VISIBILITY_PERMISSION', - COURSE_CATALOG_VISIBILITY_PERMISSION -) -COURSE_ABOUT_VISIBILITY_PERMISSION = ENV_TOKENS.get( - 'COURSE_ABOUT_VISIBILITY_PERMISSION', - COURSE_ABOUT_VISIBILITY_PERMISSION -) - -DEFAULT_COURSE_VISIBILITY_IN_CATALOG = ENV_TOKENS.get( - 'DEFAULT_COURSE_VISIBILITY_IN_CATALOG', - DEFAULT_COURSE_VISIBILITY_IN_CATALOG -) - -DEFAULT_MOBILE_AVAILABLE = ENV_TOKENS.get( - 'DEFAULT_MOBILE_AVAILABLE', - DEFAULT_MOBILE_AVAILABLE -) - -# Enrollment API Cache Timeout -ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = ENV_TOKENS.get('ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60) - -# Ecommerce Orders API Cache Timeout -ECOMMERCE_ORDERS_API_CACHE_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_ORDERS_API_CACHE_TIMEOUT', 3600) - -if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \ - FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \ - FEATURES.get('ENABLE_COURSE_DISCOVERY') or \ - FEATURES.get('ENABLE_TEAMS'): + OAUTH_EXPIRE_DELTA = datetime.timedelta(days=OAUTH_EXPIRE_CONFIDENTIAL_CLIENT_DAYS) + OAUTH_EXPIRE_DELTA_PUBLIC = datetime.timedelta(days=OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS) + +if ( + FEATURES['ENABLE_COURSEWARE_SEARCH'] or + FEATURES['ENABLE_DASHBOARD_SEARCH'] or + FEATURES['ENABLE_COURSE_DISCOVERY'] or + FEATURES['ENABLE_TEAMS'] + ): # Use ElasticSearch as the search engine herein SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" - SEARCH_FILTER_GENERATOR = ENV_TOKENS.get('SEARCH_FILTER_GENERATOR', SEARCH_FILTER_GENERATOR) - -SEARCH_SKIP_INVITATION_ONLY_FILTERING = ENV_TOKENS.get( - 'SEARCH_SKIP_INVITATION_ONLY_FILTERING', - SEARCH_SKIP_INVITATION_ONLY_FILTERING, -) -SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING = ENV_TOKENS.get( - 'SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING', - SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING, -) - -SEARCH_COURSEWARE_CONTENT_LOG_PARAMS = ENV_TOKENS.get( - 'SEARCH_COURSEWARE_CONTENT_LOG_PARAMS', - SEARCH_COURSEWARE_CONTENT_LOG_PARAMS, -) # TODO: Once we have successfully upgraded to ES7, switch this back to ELASTIC_SEARCH_CONFIG. -ELASTIC_SEARCH_CONFIG = ENV_TOKENS.get('ELASTIC_SEARCH_CONFIG_ES7', [{}]) - -# Facebook app -FACEBOOK_API_VERSION = AUTH_TOKENS.get("FACEBOOK_API_VERSION") -FACEBOOK_APP_SECRET = AUTH_TOKENS.get("FACEBOOK_APP_SECRET") -FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID") +ELASTIC_SEARCH_CONFIG = _YAML_TOKENS.get('ELASTIC_SEARCH_CONFIG_ES7', [{}]) -XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) -XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) -XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) +XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES["LICENSING"] +XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = YOUTUBE_API_KEY ##### Custom Courses for EdX ##### -if FEATURES.get('CUSTOM_COURSES_EDX'): +if FEATURES['CUSTOM_COURSES_EDX']: INSTALLED_APPS += ['lms.djangoapps.ccx', 'openedx.core.djangoapps.ccxcon.apps.CCXConnectorConfig'] MODULESTORE_FIELD_OVERRIDE_PROVIDERS += ( 'lms.djangoapps.ccx.overrides.CustomCoursesForEdxOverrideProvider', ) +FIELD_OVERRIDE_PROVIDERS = tuple(FIELD_OVERRIDE_PROVIDERS) + ##### Individual Due Date Extensions ##### -if FEATURES.get('INDIVIDUAL_DUE_DATES'): +if FEATURES['INDIVIDUAL_DUE_DATES']: FIELD_OVERRIDE_PROVIDERS += ( 'lms.djangoapps.courseware.student_field_overrides.IndividualStudentOverrideProvider', ) @@ -799,52 +477,30 @@ def get_env_setting(setting): # PROFILE IMAGE CONFIG PROFILE_IMAGE_DEFAULT_FILENAME = 'images/profiles/default' -PROFILE_IMAGE_SIZES_MAP = ENV_TOKENS.get( - 'PROFILE_IMAGE_SIZES_MAP', - PROFILE_IMAGE_SIZES_MAP -) ##### Credit Provider Integration ##### -CREDIT_PROVIDER_SECRET_KEYS = AUTH_TOKENS.get("CREDIT_PROVIDER_SECRET_KEYS", {}) - ##################### LTI Provider ##################### -if FEATURES.get('ENABLE_LTI_PROVIDER'): +if FEATURES['ENABLE_LTI_PROVIDER']: INSTALLED_APPS.append('lms.djangoapps.lti_provider.apps.LtiProviderConfig') AUTHENTICATION_BACKENDS.append('lms.djangoapps.lti_provider.users.LtiBackend') -LTI_USER_EMAIL_DOMAIN = ENV_TOKENS.get('LTI_USER_EMAIL_DOMAIN', 'lti.example.com') - -# For more info on this, see the notes in common.py -LTI_AGGREGATE_SCORE_PASSBACK_DELAY = ENV_TOKENS.get( - 'LTI_AGGREGATE_SCORE_PASSBACK_DELAY', LTI_AGGREGATE_SCORE_PASSBACK_DELAY -) - ##################### Credit Provider help link #################### #### JWT configuration #### -JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {})) -JWT_AUTH.update(AUTH_TOKENS.get('JWT_AUTH', {})) - -# Offset for pk of courseware.StudentModuleHistoryExtended -STUDENTMODULEHISTORYEXTENDED_OFFSET = ENV_TOKENS.get( - 'STUDENTMODULEHISTORYEXTENDED_OFFSET', STUDENTMODULEHISTORYEXTENDED_OFFSET -) +JWT_AUTH.update(_YAML_TOKENS.get('JWT_AUTH', {})) ################################ Settings for Credentials Service ################################ -CREDENTIALS_GENERATION_ROUTING_KEY = ENV_TOKENS.get('CREDENTIALS_GENERATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) +CREDENTIALS_GENERATION_ROUTING_KEY = _YAML_TOKENS.get('CREDENTIALS_GENERATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -# Queue to use for award program certificates -PROGRAM_CERTIFICATES_ROUTING_KEY = ENV_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = ENV_TOKENS.get( +PROGRAM_CERTIFICATES_ROUTING_KEY = _YAML_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) + +SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = _YAML_TOKENS.get( 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', HIGH_PRIORITY_QUEUE ) -API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get('API_ACCESS_MANAGER_EMAIL') -API_ACCESS_FROM_EMAIL = ENV_TOKENS.get('API_ACCESS_FROM_EMAIL') - ############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ###################### # The Open edX Enterprise service is currently hosted via the LMS container/process. # However, for all intents and purposes this service is treated as a standalone IDA. @@ -852,47 +508,17 @@ def get_env_setting(setting): # not find references to them within the edx-platform project. # Publicly-accessible enrollment URL, for use on the client side. -ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENV_TOKENS.get( +ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = _YAML_TOKENS.get( 'ENTERPRISE_PUBLIC_ENROLLMENT_API_URL', (LMS_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH ) # Enrollment URL used on the server-side. -ENTERPRISE_ENROLLMENT_API_URL = ENV_TOKENS.get( +ENTERPRISE_ENROLLMENT_API_URL = _YAML_TOKENS.get( 'ENTERPRISE_ENROLLMENT_API_URL', (LMS_INTERNAL_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH ) -# Enterprise logo image size limit in KB's -ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE = ENV_TOKENS.get( - 'ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE', - ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE -) - -# Course enrollment modes to be hidden in the Enterprise enrollment page -# if the "Hide audit track" flag is enabled for an EnterpriseCustomer -ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES = ENV_TOKENS.get( - 'ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES', - ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES -) - -# A support URL used on Enterprise landing pages for when a warning -# message goes off. -ENTERPRISE_SUPPORT_URL = ENV_TOKENS.get( - 'ENTERPRISE_SUPPORT_URL', - ENTERPRISE_SUPPORT_URL -) - -# A default dictionary to be used for filtering out enterprise customer catalog. -ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER = ENV_TOKENS.get( - 'ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER', - ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER -) -INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT = ENV_TOKENS.get( - 'INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT', - INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT -) - ############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### # The LMS communicates with the Enterprise service via the requests.Session() client # The below environmental settings are utilized by the LMS when interacting with @@ -901,97 +527,24 @@ def get_env_setting(setting): DEFAULT_ENTERPRISE_API_URL = None if LMS_INTERNAL_ROOT_URL is not None: DEFAULT_ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' -ENTERPRISE_API_URL = ENV_TOKENS.get('ENTERPRISE_API_URL', DEFAULT_ENTERPRISE_API_URL) +ENTERPRISE_API_URL = _YAML_TOKENS.get('ENTERPRISE_API_URL', DEFAULT_ENTERPRISE_API_URL) DEFAULT_ENTERPRISE_CONSENT_API_URL = None if LMS_INTERNAL_ROOT_URL is not None: DEFAULT_ENTERPRISE_CONSENT_API_URL = LMS_INTERNAL_ROOT_URL + '/consent/api/v1/' -ENTERPRISE_CONSENT_API_URL = ENV_TOKENS.get('ENTERPRISE_CONSENT_API_URL', DEFAULT_ENTERPRISE_CONSENT_API_URL) - -ENTERPRISE_SERVICE_WORKER_USERNAME = ENV_TOKENS.get( - 'ENTERPRISE_SERVICE_WORKER_USERNAME', - ENTERPRISE_SERVICE_WORKER_USERNAME -) -ENTERPRISE_API_CACHE_TIMEOUT = ENV_TOKENS.get( - 'ENTERPRISE_API_CACHE_TIMEOUT', - ENTERPRISE_API_CACHE_TIMEOUT -) -ENTERPRISE_CATALOG_INTERNAL_ROOT_URL = ENV_TOKENS.get( - 'ENTERPRISE_CATALOG_INTERNAL_ROOT_URL', - ENTERPRISE_CATALOG_INTERNAL_ROOT_URL -) - -CHAT_COMPLETION_API = ENV_TOKENS.get('CHAT_COMPLETION_API', '') -CHAT_COMPLETION_API_KEY = ENV_TOKENS.get('CHAT_COMPLETION_API_KEY', '') -LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT', '') -LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get( - 'LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT', - '' -) -LEARNER_PROGRESS_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_PROGRESS_PROMPT_FOR_ACTIVE_CONTRACT', '') -LEARNER_PROGRESS_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_PROGRESS_PROMPT_FOR_NON_ACTIVE_CONTRACT', '') +ENTERPRISE_CONSENT_API_URL = _YAML_TOKENS.get('ENTERPRISE_CONSENT_API_URL', DEFAULT_ENTERPRISE_CONSENT_API_URL) ############## ENTERPRISE SERVICE LMS CONFIGURATION ################################## # The LMS has some features embedded that are related to the Enterprise service, but # which are not provided by the Enterprise service. These settings override the # base values for the parameters as defined in common.py -ENTERPRISE_PLATFORM_WELCOME_TEMPLATE = ENV_TOKENS.get( - 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', - ENTERPRISE_PLATFORM_WELCOME_TEMPLATE -) -ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE = ENV_TOKENS.get( - 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', - ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE -) -ENTERPRISE_TAGLINE = ENV_TOKENS.get( - 'ENTERPRISE_TAGLINE', - ENTERPRISE_TAGLINE -) -ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS = set( - ENV_TOKENS.get( - 'ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS', - ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS - ) -) -BASE_COOKIE_DOMAIN = ENV_TOKENS.get( - 'BASE_COOKIE_DOMAIN', - BASE_COOKIE_DOMAIN -) -SYSTEM_TO_FEATURE_ROLE_MAPPING = ENV_TOKENS.get( - 'SYSTEM_TO_FEATURE_ROLE_MAPPING', - SYSTEM_TO_FEATURE_ROLE_MAPPING -) - -# Add an ICP license for serving content in China if your organization is registered to do so -ICP_LICENSE = ENV_TOKENS.get('ICP_LICENSE', None) -ICP_LICENSE_INFO = ENV_TOKENS.get('ICP_LICENSE_INFO', {}) - -# How long to cache OpenAPI schemas and UI, in seconds. -OPENAPI_CACHE_TIMEOUT = ENV_TOKENS.get('OPENAPI_CACHE_TIMEOUT', 60 * 60) - -########################## Parental controls config ####################### - -# The age at which a learner no longer requires parental consent, or None -# if parental consent is never required. -PARENTAL_CONSENT_AGE_LIMIT = ENV_TOKENS.get( - 'PARENTAL_CONSENT_AGE_LIMIT', - PARENTAL_CONSENT_AGE_LIMIT -) +ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS = set(ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS) ########################## Extra middleware classes ####################### # Allow extra middleware classes to be added to the app through configuration. -MIDDLEWARE.extend(ENV_TOKENS.get('EXTRA_MIDDLEWARE_CLASSES', [])) - -################# Settings for the maintenance banner ################# -MAINTENANCE_BANNER_TEXT = ENV_TOKENS.get('MAINTENANCE_BANNER_TEXT', None) - -########################## limiting dashboard courses ###################### -DASHBOARD_COURSE_LIMIT = ENV_TOKENS.get('DASHBOARD_COURSE_LIMIT', None) - -######################## Setting for content libraries ######################## -MAX_BLOCKS_PER_CONTENT_LIBRARY = ENV_TOKENS.get('MAX_BLOCKS_PER_CONTENT_LIBRARY', MAX_BLOCKS_PER_CONTENT_LIBRARY) +MIDDLEWARE.extend(_YAML_TOKENS.get('EXTRA_MIDDLEWARE_CLASSES', [])) ########################## Derive Any Derived Settings ####################### @@ -1001,23 +554,15 @@ def get_env_setting(setting): # This is at the bottom because it is going to load more settings after base settings are loaded +# ENV_TOKENS and AUTH_TOKENS are included for reverse compatibility. +# Removing them may break plugins that rely on them. +# Please do not add new references to them... just use `django.conf.settings` instead. +ENV_TOKENS = __config__ +AUTH_TOKENS = __config__ + # Load production.py in plugins add_plugins(__name__, ProjectType.LMS, SettingsType.PRODUCTION) -############## Settings for Completion API ######################### - -# Once a user has watched this percentage of a video, mark it as complete: -# (0.0 = 0%, 1.0 = 100%) -COMPLETION_VIDEO_COMPLETE_PERCENTAGE = ENV_TOKENS.get('COMPLETION_VIDEO_COMPLETE_PERCENTAGE', - COMPLETION_VIDEO_COMPLETE_PERCENTAGE) -COMPLETION_BY_VIEWING_DELAY_MS = ENV_TOKENS.get('COMPLETION_BY_VIEWING_DELAY_MS', - COMPLETION_BY_VIEWING_DELAY_MS) - -################# Settings for brand logos. ################# -LOGO_URL = ENV_TOKENS.get('LOGO_URL', LOGO_URL) -LOGO_URL_PNG = ENV_TOKENS.get('LOGO_URL_PNG', LOGO_URL_PNG) -LOGO_TRADEMARK_URL = ENV_TOKENS.get('LOGO_TRADEMARK_URL', LOGO_TRADEMARK_URL) -FAVICON_URL = ENV_TOKENS.get('FAVICON_URL', FAVICON_URL) ######################## CELERY ROUTING ######################## @@ -1077,56 +622,32 @@ def get_env_setting(setting): } -LOGO_IMAGE_EXTRA_TEXT = ENV_TOKENS.get('LOGO_IMAGE_EXTRA_TEXT', '') - ############## XBlock extra mixins ############################ XBLOCK_MIXINS += tuple(XBLOCK_EXTRA_MIXINS) -############## Settings for course import olx validation ############################ -COURSE_OLX_VALIDATION_STAGE = ENV_TOKENS.get('COURSE_OLX_VALIDATION_STAGE', COURSE_OLX_VALIDATION_STAGE) -COURSE_OLX_VALIDATION_IGNORE_LIST = ENV_TOKENS.get( - 'COURSE_OLX_VALIDATION_IGNORE_LIST', - COURSE_OLX_VALIDATION_IGNORE_LIST -) - -################# show account activate cta after register ######################## -SHOW_ACCOUNT_ACTIVATION_CTA = ENV_TOKENS.get('SHOW_ACCOUNT_ACTIVATION_CTA', SHOW_ACCOUNT_ACTIVATION_CTA) - -################# Discussions micro frontend URL ######################## -DISCUSSIONS_MICROFRONTEND_URL = ENV_TOKENS.get('DISCUSSIONS_MICROFRONTEND_URL', DISCUSSIONS_MICROFRONTEND_URL) - -################### Discussions micro frontend Feedback URL################### -DISCUSSIONS_MFE_FEEDBACK_URL = ENV_TOKENS.get('DISCUSSIONS_MFE_FEEDBACK_URL', DISCUSSIONS_MFE_FEEDBACK_URL) - -############################ AI_TRANSLATIONS URL ################################## -AI_TRANSLATIONS_API_URL = ENV_TOKENS.get('AI_TRANSLATIONS_API_URL', AI_TRANSLATIONS_API_URL) - ############## DRF overrides ############## -REST_FRAMEWORK.update(ENV_TOKENS.get('REST_FRAMEWORK', {})) +REST_FRAMEWORK.update(_YAML_TOKENS.get('REST_FRAMEWORK', {})) ############################# CELERY ############################ -CELERY_IMPORTS.extend(ENV_TOKENS.get('CELERY_EXTRA_IMPORTS', [])) +CELERY_IMPORTS.extend(_YAML_TOKENS.get('CELERY_EXTRA_IMPORTS', [])) # keys for big blue button live provider +# TODO: This should not be in the core platform. If it has to stay for now, though, then we should move these +# defaults into common.py COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = { - "KEY": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_KEY', None), - "SECRET": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_SECRET', None), - "URL": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL', None), + "KEY": _YAML_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_KEY'), + "SECRET": _YAML_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_SECRET'), + "URL": _YAML_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL'), } -AVAILABLE_DISCUSSION_TOURS = ENV_TOKENS.get('AVAILABLE_DISCUSSION_TOURS', []) - -############## NOTIFICATIONS EXPIRY ############## -NOTIFICATIONS_EXPIRY = ENV_TOKENS.get('NOTIFICATIONS_EXPIRY', NOTIFICATIONS_EXPIRY) - ############## Event bus producer ############## -EVENT_BUS_PRODUCER_CONFIG = merge_producer_configs(EVENT_BUS_PRODUCER_CONFIG, - ENV_TOKENS.get('EVENT_BUS_PRODUCER_CONFIG', {})) -BEAMER_PRODUCT_ID = ENV_TOKENS.get('BEAMER_PRODUCT_ID', BEAMER_PRODUCT_ID) - -# .. setting_name: DISABLED_COUNTRIES -# .. setting_default: [] -# .. setting_description: List of country codes that should be disabled -# .. for now it wil impact country listing in auth flow and user profile. -# .. eg ['US', 'CA'] -DISABLED_COUNTRIES = ENV_TOKENS.get('DISABLED_COUNTRIES', []) +EVENT_BUS_PRODUCER_CONFIG = merge_producer_configs( + EVENT_BUS_PRODUCER_CONFIG, + _YAML_TOKENS.get('EVENT_BUS_PRODUCER_CONFIG', {}) +) + +##################################################################################################### +# HEY! Don't add anything to the end of this file. +# Add your defaults to common.py instead! +# If you really need to add post-YAML logic, add it above the "Derive Any Derived Settings" section. +###################################################################################################### From c0cb126c3aa30f254241a2517ad2c94bf2d6a156 Mon Sep 17 00:00:00 2001 From: "Kyle D. McCormick" Date: Fri, 17 Jan 2025 14:40:02 -0500 Subject: [PATCH 3/9] fix: SHARED_kCOOKIE_DOMAIN -> SHARED_COOKIE_DOMAIN --- lms/envs/production.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/production.py b/lms/envs/production.py index 3ea1c952e479..cbe10b180af2 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -219,7 +219,7 @@ def get_env_setting(setting): # This is the domain that is used to set shared cookies between various sub-domains. # By default, it's set to the same thing as the SESSION_COOKIE_DOMAIN, but we want to make it overrideable. -SHARED_COOKIE_DOMAIN = _YAML_TOKENS.get('SHARED_kCOOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) +SHARED_COOKIE_DOMAIN = _YAML_TOKENS.get('SHARED_COOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) # Cache used for location mapping -- called many times with the same key/value # in a given request. From e26a9eb163edffdf3810f611ee57003450a4abd7 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 16 Jan 2025 15:35:26 -0500 Subject: [PATCH 4/9] refactor: Use `derived_settings` to lazy load settings. Some of our settings depend on the values of other settings. Rather than explicitly looking up each one in the YAML settings file, we can simply derive them based on the setting in the YAML file after all the YAML settings have been loaded. --- lms/envs/production.py | 161 +++++++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 71 deletions(-) diff --git a/lms/envs/production.py b/lms/envs/production.py index cbe10b180af2..99a69174b7e5 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -126,6 +126,96 @@ def get_env_setting(setting): "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" ) +## Derived Settings +CC_MERCHANT_NAME = lambda settings: settings.PLATFORM_NAME +EMAIL_FILE_PATH = lambda settings: settings.DATA_DIR / "emails" / "lms" +LMS_INTERNAL_ROOT_URL = lambda settings: settings.LMS_ROOT_URL +# This is the domain that is used to set shared cookies between various sub-domains. +# By default, it's set to the same thing as the SESSION_COOKIE_DOMAIN +SHARED_COOKIE_DOMAIN = lambda settings: settings.SESSION_COOKIE_DOMAIN + +# We want Bulk Email running on the high-priority queue, so we define the +# routing key that points to it. At the moment, the name is the same. +# We have to reset the value here, since we have changed the value of the queue name. +BULK_EMAIL_ROUTING_KEY = lambda settings: settings.HIGH_PRIORITY_QUEUE + +# We can run smaller jobs on the low priority queue. See note above for why +# we have to reset the value here. +BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = lambda settings: settings.DEFAULT_PRIORITY_QUEUE + +# Queue to use for expiring old entitlements +ENTITLEMENTS_EXPIRATION_ROUTING_KEY = lambda settings: settings.DEFAULT_PRIORITY_QUEUE + +# Intentional defaults. +ID_VERIFICATION_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK +PASSWORD_RESET_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK +ACTIVATION_EMAIL_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK +LOGIN_ISSUE_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK + +# Default queues for various routes +GRADES_DOWNLOAD_ROUTING_KEY = lambda settings: settings.HIGH_MEM_QUEUE +CREDENTIALS_GENERATION_ROUTING_KEY = lambda settings: settings.DEFAULT_PRIORITY_QUEUE +PROGRAM_CERTIFICATES_ROUTING_KEY = lambda settings: settings.DEFAULT_PRIORITY_QUEUE +SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = lambda settings: settings.HIGH_PRIORITY_QUEUE + +############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ###################### +# The Open edX Enterprise service is currently hosted via the LMS container/process. +# However, for all intents and purposes this service is treated as a standalone IDA. +# These configuration settings are specific to the Enterprise service and you should +# not find references to them within the edx-platform project. + +# Publicly-accessible enrollment URL, for use on the client side. +ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = lambda settings: (settings.LMS_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH + +# Enrollment URL used on the server-side. +ENTERPRISE_ENROLLMENT_API_URL = _YAML_TOKENS.get( + 'ENTERPRISE_ENROLLMENT_API_URL', + (LMS_INTERNAL_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH +) + +############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### +# The LMS communicates with the Enterprise service via the requests.Session() client +# The below environmental settings are utilized by the LMS when interacting with +# the service, and override the default parameters which are defined in common.py + +def _generate_default_enterprise_api_url(settings): + default_enterprise_api_url = None + if settings.LMS_INTERNAL_ROOT_URL is not None: + default_enterprise_api_url = settings.LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' + return default_enterprise_api_url + +ENTERPRISE_API_URL = _generate_default_enterprise_api_url + +def _generate_default_enterprise_consent_api_url(settings): + default_enterprise_consent_api_url = None + if settings.LMS_INTERNAL_ROOT_URL is not None: + default_enterprise_consent_api_url = settings.LMS_INTERNAL_ROOT_URL + '/consent/api/v1/' +ENTERPRISE_CONSENT_API_URL = lambda settings: _generate_default_enterprise_consent_api_url + +# Note the order of this matters, don't sort this list. +derived( + CC_MERCHANT_NAME, + EMAIL_FILE_PATH, + LMS_INTERNAL_ROOT_URL, + SHARED_COOKIE_DOMAIN, + BULK_EMAIL_ROUTING_KEY, + BULK_EMAIL_ROUTING_KEY_SMALL_JOBS, + ENTITLEMENTS_EXPIRATION_ROUTING_KEY, + ID_VERIFICATION_SUPPORT_LINK, + PASSWORD_RESET_SUPPORT_LINK, + ACTIVATION_EMAIL_SUPPORT_LINK, + LOGIN_ISSUE_SUPPORT_LINK, + GRADES_DOWNLOAD_ROUTING_KEY, + CREDENTIALS_GENERATION_ROUTING_KEY, + PROGRAM_CERTIFICATES_ROUTING_KEY, + SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY, + ENTERPRISE_PUBLIC_ENROLLMENT_API_URL, + ENTERPRISE_API_URL, + ENTERPRISE_CONSENT_API_URL, +) + + + ####################################################################################################################### # A file path to a YAML file from which to load all the configuration for the edx platform @@ -199,15 +289,11 @@ def get_env_setting(setting): STATIC_URL += "/" DATA_DIR = path(DATA_DIR) -CC_MERCHANT_NAME = _YAML_TOKENS.get('CC_MERCHANT_NAME', PLATFORM_NAME) -EMAIL_FILE_PATH = _YAML_TOKENS.get('EMAIL_FILE_PATH', DATA_DIR / "emails" / "lms") # TODO: This was for backwards compatibility back when installed django-cookie-samesite (not since 2022). # The DCS_ version of the setting can be DEPR'd at this point. SESSION_COOKIE_SAMESITE = DCS_SESSION_COOKIE_SAMESITE -LMS_INTERNAL_ROOT_URL = _YAML_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) - for feature, value in _YAML_TOKENS.get('FEATURES', {}).items(): FEATURES[feature] = value @@ -217,10 +303,6 @@ def get_env_setting(setting): FEATURES['PREVIEW_LMS_BASE'], ] -# This is the domain that is used to set shared cookies between various sub-domains. -# By default, it's set to the same thing as the SESSION_COOKIE_DOMAIN, but we want to make it overrideable. -SHARED_COOKIE_DOMAIN = _YAML_TOKENS.get('SHARED_COOKIE_DOMAIN', SESSION_COOKIE_DOMAIN) - # Cache used for location mapping -- called many times with the same key/value # in a given request. if 'loc_cache' not in CACHES: @@ -237,17 +319,6 @@ def get_env_setting(setting): # Once we have migrated to service assets off S3, then we can convert this back to # managed by the yaml file contents -# We want Bulk Email running on the high-priority queue, so we define the -# routing key that points to it. At the moment, the name is the same. -# We have to reset the value here, since we have changed the value of the queue name. -BULK_EMAIL_ROUTING_KEY = _YAML_TOKENS.get('BULK_EMAIL_ROUTING_KEY', HIGH_PRIORITY_QUEUE) - -# We can run smaller jobs on the low priority queue. See note above for why -# we have to reset the value here. -BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = _YAML_TOKENS.get('BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', DEFAULT_PRIORITY_QUEUE) - -# Queue to use for expiring old entitlements -ENTITLEMENTS_EXPIRATION_ROUTING_KEY = _YAML_TOKENS.get('ENTITLEMENTS_EXPIRATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) # Build a CELERY_QUEUES dict the way that celery expects, based on a couple lists of queue names from the YAML. _YAML_CELERY_QUEUES = _YAML_TOKENS.get('CELERY_QUEUES', None) @@ -271,12 +342,6 @@ def get_env_setting(setting): MKTG_URL_LINK_MAP.update(_YAML_TOKENS.get('MKTG_URL_LINK_MAP', {})) -# Intentional defaults. -ID_VERIFICATION_SUPPORT_LINK = _YAML_TOKENS.get('ID_VERIFICATION_SUPPORT_LINK', SUPPORT_SITE_LINK) -PASSWORD_RESET_SUPPORT_LINK = _YAML_TOKENS.get('PASSWORD_RESET_SUPPORT_LINK', SUPPORT_SITE_LINK) -ACTIVATION_EMAIL_SUPPORT_LINK = _YAML_TOKENS.get('ACTIVATION_EMAIL_SUPPORT_LINK', SUPPORT_SITE_LINK) -LOGIN_ISSUE_SUPPORT_LINK = _YAML_TOKENS.get('LOGIN_ISSUE_SUPPORT_LINK', SUPPORT_SITE_LINK) - # Timezone overrides TIME_ZONE = CELERY_TIMEZONE @@ -384,9 +449,6 @@ def get_env_setting(setting): EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST ) -# Grades download -GRADES_DOWNLOAD_ROUTING_KEY = _YAML_TOKENS.get('GRADES_DOWNLOAD_ROUTING_KEY', HIGH_MEM_QUEUE) - if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): AUTHENTICATION_BACKENDS = _YAML_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [ 'social_core.backends.google.GoogleOAuth2', @@ -490,49 +552,6 @@ def get_env_setting(setting): #### JWT configuration #### JWT_AUTH.update(_YAML_TOKENS.get('JWT_AUTH', {})) -################################ Settings for Credentials Service ################################ - -CREDENTIALS_GENERATION_ROUTING_KEY = _YAML_TOKENS.get('CREDENTIALS_GENERATION_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) - -PROGRAM_CERTIFICATES_ROUTING_KEY = _YAML_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) - -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = _YAML_TOKENS.get( - 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', - HIGH_PRIORITY_QUEUE -) - -############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ###################### -# The Open edX Enterprise service is currently hosted via the LMS container/process. -# However, for all intents and purposes this service is treated as a standalone IDA. -# These configuration settings are specific to the Enterprise service and you should -# not find references to them within the edx-platform project. - -# Publicly-accessible enrollment URL, for use on the client side. -ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = _YAML_TOKENS.get( - 'ENTERPRISE_PUBLIC_ENROLLMENT_API_URL', - (LMS_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH -) - -# Enrollment URL used on the server-side. -ENTERPRISE_ENROLLMENT_API_URL = _YAML_TOKENS.get( - 'ENTERPRISE_ENROLLMENT_API_URL', - (LMS_INTERNAL_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH -) - -############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### -# The LMS communicates with the Enterprise service via the requests.Session() client -# The below environmental settings are utilized by the LMS when interacting with -# the service, and override the default parameters which are defined in common.py - -DEFAULT_ENTERPRISE_API_URL = None -if LMS_INTERNAL_ROOT_URL is not None: - DEFAULT_ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' -ENTERPRISE_API_URL = _YAML_TOKENS.get('ENTERPRISE_API_URL', DEFAULT_ENTERPRISE_API_URL) - -DEFAULT_ENTERPRISE_CONSENT_API_URL = None -if LMS_INTERNAL_ROOT_URL is not None: - DEFAULT_ENTERPRISE_CONSENT_API_URL = LMS_INTERNAL_ROOT_URL + '/consent/api/v1/' -ENTERPRISE_CONSENT_API_URL = _YAML_TOKENS.get('ENTERPRISE_CONSENT_API_URL', DEFAULT_ENTERPRISE_CONSENT_API_URL) ############## ENTERPRISE SERVICE LMS CONFIGURATION ################################## # The LMS has some features embedded that are related to the Enterprise service, but From 6736e6f57a8f009db8f574aea42f016fb99232be Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 16 Jan 2025 15:40:01 -0500 Subject: [PATCH 5/9] docs: Add some more docs for how to use derived_settings. This was a little hard to grok so I added a little example to make it easier to understand. --- openedx/core/lib/derived.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openedx/core/lib/derived.py b/openedx/core/lib/derived.py index a62731ef5432..47e191614fee 100644 --- a/openedx/core/lib/derived.py +++ b/openedx/core/lib/derived.py @@ -3,6 +3,30 @@ via callable methods/lambdas. The derivation time can be controlled to happen after all other settings have been set. The derived setting can also be overridden by setting the derived setting to an actual value. + +Example + +In `lms/envs/common.py`: + +``` +# Double some other value that might get set later. +VALUE = lambda settings: settings.SOME_OTHER_VALUE * 2 +# Register this value as one that needs to be derived later. +derived(VALUE) +``` + +Later in a settings file that depends on common.py + +``` +from lms.envs.common * + +# Set some other value however you want. +SOME_OTHER_VALUE = 4 + +# Derive any settings and pass them this settings file for reference. +# This will update VALUE so that it is the scaler `8` instead of a lambda. +derive_settings(__name__) +``` """ import sys From 4664f3f6cd044aea8ce13fc8025c069d1ae789c4 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 16 Jan 2025 15:48:31 -0500 Subject: [PATCH 6/9] fixup! refactor: Use `derived_settings` to lazy load settings. --- lms/envs/production.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lms/envs/production.py b/lms/envs/production.py index 99a69174b7e5..555258a0f1f6 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -194,24 +194,24 @@ def _generate_default_enterprise_consent_api_url(settings): # Note the order of this matters, don't sort this list. derived( - CC_MERCHANT_NAME, - EMAIL_FILE_PATH, - LMS_INTERNAL_ROOT_URL, - SHARED_COOKIE_DOMAIN, - BULK_EMAIL_ROUTING_KEY, - BULK_EMAIL_ROUTING_KEY_SMALL_JOBS, - ENTITLEMENTS_EXPIRATION_ROUTING_KEY, - ID_VERIFICATION_SUPPORT_LINK, - PASSWORD_RESET_SUPPORT_LINK, - ACTIVATION_EMAIL_SUPPORT_LINK, - LOGIN_ISSUE_SUPPORT_LINK, - GRADES_DOWNLOAD_ROUTING_KEY, - CREDENTIALS_GENERATION_ROUTING_KEY, - PROGRAM_CERTIFICATES_ROUTING_KEY, - SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY, - ENTERPRISE_PUBLIC_ENROLLMENT_API_URL, - ENTERPRISE_API_URL, - ENTERPRISE_CONSENT_API_URL, + 'CC_MERCHANT_NAME', + 'EMAIL_FILE_PATH', + 'LMS_INTERNAL_ROOT_URL', + 'SHARED_COOKIE_DOMAIN', + 'BULK_EMAIL_ROUTING_KEY', + 'BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', + 'ENTITLEMENTS_EXPIRATION_ROUTING_KEY', + 'ID_VERIFICATION_SUPPORT_LINK', + 'PASSWORD_RESET_SUPPORT_LINK', + 'ACTIVATION_EMAIL_SUPPORT_LINK', + 'LOGIN_ISSUE_SUPPORT_LINK', + 'GRADES_DOWNLOAD_ROUTING_KEY', + 'CREDENTIALS_GENERATION_ROUTING_KEY', + 'PROGRAM_CERTIFICATES_ROUTING_KEY', + 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', + 'ENTERPRISE_PUBLIC_ENROLLMENT_API_URL', + 'ENTERPRISE_API_URL', + 'ENTERPRISE_CONSENT_API_URL', ) From 9d730d6dceac3eaec6b0bd2a9de49b28e2aa12c5 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 16 Jan 2025 15:54:10 -0500 Subject: [PATCH 7/9] fixup! refactor: Use `derived_settings` to lazy load settings. --- lms/envs/production.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lms/envs/production.py b/lms/envs/production.py index 555258a0f1f6..95534d3b1b5b 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -168,10 +168,7 @@ def get_env_setting(setting): ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = lambda settings: (settings.LMS_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH # Enrollment URL used on the server-side. -ENTERPRISE_ENROLLMENT_API_URL = _YAML_TOKENS.get( - 'ENTERPRISE_ENROLLMENT_API_URL', - (LMS_INTERNAL_ROOT_URL or '') + LMS_ENROLLMENT_API_PATH -) +ENTERPRISE_ENROLLMENT_API_URL = lambda settings: (settisgs.LMS_INTERNAL_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH ############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### # The LMS communicates with the Enterprise service via the requests.Session() client @@ -210,6 +207,7 @@ def _generate_default_enterprise_consent_api_url(settings): 'PROGRAM_CERTIFICATES_ROUTING_KEY', 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', 'ENTERPRISE_PUBLIC_ENROLLMENT_API_URL', + 'ENTERPRISE_ENROLLMENT_API_URL', 'ENTERPRISE_API_URL', 'ENTERPRISE_CONSENT_API_URL', ) From edb7a86de6a157156d8a4a80d54eaa70d2ade287 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 16 Jan 2025 15:57:46 -0500 Subject: [PATCH 8/9] fixup! refactor: Use `derived_settings` to lazy load settings. --- lms/envs/production.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/production.py b/lms/envs/production.py index 95534d3b1b5b..32c2d54dc8a2 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -168,7 +168,7 @@ def get_env_setting(setting): ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = lambda settings: (settings.LMS_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH # Enrollment URL used on the server-side. -ENTERPRISE_ENROLLMENT_API_URL = lambda settings: (settisgs.LMS_INTERNAL_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH +ENTERPRISE_ENROLLMENT_API_URL = lambda settings: (settings.LMS_INTERNAL_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH ############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### # The LMS communicates with the Enterprise service via the requests.Session() client From b0fa9aca4cb5e738564e3fa0c40f83a4f17c51cb Mon Sep 17 00:00:00 2001 From: "Kyle D. McCormick" Date: Wed, 29 Jan 2025 10:36:53 -0500 Subject: [PATCH 9/9] refactor: A Better API for Derived Settings --- cms/envs/common.py | 55 ++++--------- lms/envs/common.py | 47 +++-------- lms/envs/docs/README.rst | 2 + lms/envs/production.py | 82 ++++++------------- openedx/core/lib/derived.py | 87 +++++++++------------ openedx/core/lib/tests/test_derived.py | 14 ++-- xmodule/modulestore/modulestore_settings.py | 2 + 7 files changed, 98 insertions(+), 191 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 591247388a9d..bde0ef6d6de2 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -144,7 +144,7 @@ get_theme_base_dirs_from_settings ) from openedx.core.lib.license import LicenseMixin -from openedx.core.lib.derived import derived, derived_collection_entry +from openedx.core.lib.derived import Derived from openedx.core.release import doc_version # pylint: enable=useless-suppression @@ -740,7 +740,7 @@ # Don't look for template source files inside installed applications. 'APP_DIRS': False, # Instead, look for template source files in these dirs. - 'DIRS': _make_mako_template_dirs, + 'DIRS': Derived(_make_mako_template_dirs), # Options specific to this backend. 'OPTIONS': { 'loaders': ( @@ -759,7 +759,7 @@ 'NAME': 'mako', 'BACKEND': 'common.djangoapps.edxmako.backend.Mako', 'APP_DIRS': False, - 'DIRS': _make_mako_template_dirs, + 'DIRS': Derived(_make_mako_template_dirs), 'OPTIONS': { 'context_processors': CONTEXT_PROCESSORS, 'debug': False, @@ -778,8 +778,6 @@ } }, ] -derived_collection_entry('TEMPLATES', 0, 'DIRS') -derived_collection_entry('TEMPLATES', 1, 'DIRS') DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0] #################################### AWS ####################################### @@ -825,8 +823,7 @@ # Warning: Must have trailing slash to activate correct logout view # (auth_backends, not LMS user_authn) FRONTEND_LOGOUT_URL = '/logout/' -FRONTEND_REGISTER_URL = lambda settings: settings.LMS_ROOT_URL + '/register' -derived('FRONTEND_REGISTER_URL') +FRONTEND_REGISTER_URL = Derived(lambda settings: settings.LMS_ROOT_URL + '/register') LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/" ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' @@ -1315,8 +1312,7 @@ STATICI18N_FILENAME_FUNCTION = 'statici18n.utils.legacy_filename' STATICI18N_ROOT = PROJECT_ROOT / "static" -LOCALE_PATHS = _make_locale_paths -derived('LOCALE_PATHS') +LOCALE_PATHS = Derived(_make_locale_paths) # Messages MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' @@ -2086,10 +2082,9 @@ # See annotations in lms/envs/common.py for details. RETIRED_EMAIL_DOMAIN = 'retired.invalid' # See annotations in lms/envs/common.py for details. -RETIRED_USERNAME_FMT = lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}' +RETIRED_USERNAME_FMT = Derived(lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}') # See annotations in lms/envs/common.py for details. -RETIRED_EMAIL_FMT = lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN -derived('RETIRED_USERNAME_FMT', 'RETIRED_EMAIL_FMT') +RETIRED_EMAIL_FMT = Derived(lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN) # See annotations in lms/envs/common.py for details. RETIRED_USER_SALTS = ['abc', '123'] # See annotations in lms/envs/common.py for details. @@ -2366,13 +2361,12 @@ ############## Settings for Studio Context Sensitive Help ############## HELP_TOKENS_INI_FILE = REPO_ROOT / "cms" / "envs" / "help_tokens.ini" -HELP_TOKENS_LANGUAGE_CODE = lambda settings: settings.LANGUAGE_CODE -HELP_TOKENS_VERSION = lambda settings: doc_version() +HELP_TOKENS_LANGUAGE_CODE = Derived(lambda settings: settings.LANGUAGE_CODE) +HELP_TOKENS_VERSION = Derived(lambda settings: doc_version()) HELP_TOKENS_BOOKS = { 'learner': 'https://edx.readthedocs.io/projects/open-edx-learner-guide', 'course_author': 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course', } -derived('HELP_TOKENS_LANGUAGE_CODE', 'HELP_TOKENS_VERSION') # Used with Email sending RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 @@ -2873,15 +2867,15 @@ def _should_send_learning_badge_events(settings): }, 'org.openedx.content_authoring.xblock.published.v1': { 'course-authoring-xblock-lifecycle': - {'event_key_field': 'xblock_info.usage_key', 'enabled': _should_send_xblock_events}, + {'event_key_field': 'xblock_info.usage_key', 'enabled': Derived(_should_send_xblock_events)}, }, 'org.openedx.content_authoring.xblock.deleted.v1': { 'course-authoring-xblock-lifecycle': - {'event_key_field': 'xblock_info.usage_key', 'enabled': _should_send_xblock_events}, + {'event_key_field': 'xblock_info.usage_key', 'enabled': Derived(_should_send_xblock_events)}, }, 'org.openedx.content_authoring.xblock.duplicated.v1': { 'course-authoring-xblock-lifecycle': - {'event_key_field': 'xblock_info.usage_key', 'enabled': _should_send_xblock_events}, + {'event_key_field': 'xblock_info.usage_key', 'enabled': Derived(_should_send_xblock_events)}, }, # LMS events. These have to be copied over here because lms.common adds some derived entries as well, # and the derivation fails if the keys are missing. If we ever remove the import of lms.common, we can remove these. @@ -2896,38 +2890,17 @@ def _should_send_learning_badge_events(settings): "org.openedx.learning.course.passing.status.updated.v1": { "learning-badges-lifecycle": { "event_key_field": "course_passing_status.course.course_key", - "enabled": _should_send_learning_badge_events, + "enabled": Derived(_should_send_learning_badge_events), }, }, "org.openedx.learning.ccx.course.passing.status.updated.v1": { "learning-badges-lifecycle": { "event_key_field": "course_passing_status.course.ccx_course_key", - "enabled": _should_send_learning_badge_events, + "enabled": Derived(_should_send_learning_badge_events), }, }, } - -derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.published.v1', - 'course-authoring-xblock-lifecycle', 'enabled') -derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.duplicated.v1', - 'course-authoring-xblock-lifecycle', 'enabled') -derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.deleted.v1', - 'course-authoring-xblock-lifecycle', 'enabled') - -derived_collection_entry( - "EVENT_BUS_PRODUCER_CONFIG", - "org.openedx.learning.course.passing.status.updated.v1", - "learning-badges-lifecycle", - "enabled", -) -derived_collection_entry( - "EVENT_BUS_PRODUCER_CONFIG", - "org.openedx.learning.ccx.course.passing.status.updated.v1", - "learning-badges-lifecycle", - "enabled", -) - ################### Authoring API ###################### # This affects the Authoring API swagger docs but not the legacy swagger docs under /api-docs/. diff --git a/lms/envs/common.py b/lms/envs/common.py index 23e0d12b3dde..ec2e76622220 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -69,7 +69,7 @@ get_themes_unchecked, get_theme_base_dirs_from_settings ) -from openedx.core.lib.derived import derived, derived_collection_entry +from openedx.core.lib.derived import Derived from openedx.core.release import doc_version from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin @@ -1395,7 +1395,7 @@ def _make_mako_template_dirs(settings): # Don't look for template source files inside installed applications. 'APP_DIRS': False, # Instead, look for template source files in these dirs. - 'DIRS': _make_mako_template_dirs, + 'DIRS': Derived(_make_mako_template_dirs), # Options specific to this backend. 'OPTIONS': { 'context_processors': CONTEXT_PROCESSORS, @@ -1404,7 +1404,6 @@ def _make_mako_template_dirs(settings): } }, ] -derived_collection_entry('TEMPLATES', 1, 'DIRS') DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0] DEFAULT_TEMPLATE_ENGINE_DIRS = DEFAULT_TEMPLATE_ENGINE['DIRS'][:] @@ -1734,7 +1733,7 @@ def _make_mako_template_dirs(settings): 'DOC_STORE_CONFIG': DOC_STORE_CONFIG, 'OPTIONS': { 'default_class': 'xmodule.hidden_block.HiddenBlock', - 'fs_root': lambda settings: settings.DATA_DIR, + 'fs_root': Derived(lambda settings: settings.DATA_DIR), 'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string', } }, @@ -1744,7 +1743,7 @@ def _make_mako_template_dirs(settings): 'DOC_STORE_CONFIG': DOC_STORE_CONFIG, 'OPTIONS': { 'default_class': 'xmodule.hidden_block.HiddenBlock', - 'fs_root': lambda settings: settings.DATA_DIR, + 'fs_root': Derived(lambda settings: settings.DATA_DIR), 'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string', } } @@ -2054,8 +2053,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring for locale_path in settings.COMPREHENSIVE_THEME_LOCALE_PATHS: locale_paths += (path(locale_path), ) return locale_paths -LOCALE_PATHS = _make_locale_paths -derived('LOCALE_PATHS') +LOCALE_PATHS = Derived(_make_locale_paths) # Messages MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' @@ -4651,13 +4649,12 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring ############## Settings for LMS Context Sensitive Help ############## HELP_TOKENS_INI_FILE = REPO_ROOT / "lms" / "envs" / "help_tokens.ini" -HELP_TOKENS_LANGUAGE_CODE = lambda settings: settings.LANGUAGE_CODE -HELP_TOKENS_VERSION = lambda settings: doc_version() +HELP_TOKENS_LANGUAGE_CODE = Derived(lambda settings: settings.LANGUAGE_CODE) +HELP_TOKENS_VERSION = Derived(lambda settings: doc_version()) HELP_TOKENS_BOOKS = { 'learner': 'https://edx.readthedocs.io/projects/open-edx-learner-guide', 'course_author': 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course', } -derived('HELP_TOKENS_LANGUAGE_CODE', 'HELP_TOKENS_VERSION') ############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ###################### # The Open edX Enterprise service is currently hosted via the LMS container/process. @@ -4945,14 +4942,13 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # .. setting_description: Set the format a retired user username field gets transformed into, where {} # is replaced with the hash of the original username. This is a derived setting that depends on # RETIRED_USERNAME_PREFIX value. -RETIRED_USERNAME_FMT = lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}' +RETIRED_USERNAME_FMT = Derived(lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}'), # .. setting_name: RETIRED_EMAIL_FMT # .. setting_default: retired__user_{}@retired.invalid # .. setting_description: Set the format a retired user email field gets transformed into, where {} is # replaced with the hash of the original email. This is a derived setting that depends on # RETIRED_EMAIL_PREFIX and RETIRED_EMAIL_DOMAIN values. -RETIRED_EMAIL_FMT = lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN -derived('RETIRED_USERNAME_FMT', 'RETIRED_EMAIL_FMT') +RETIRED_EMAIL_FMT = Derived(lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN), # .. setting_name: RETIRED_USER_SALTS # .. setting_default: ['abc', '123'] # .. setting_description: Set a list of salts used for hashing usernames and emails on users retirement. @@ -5440,11 +5436,11 @@ def _should_send_learning_badge_events(settings): EVENT_BUS_PRODUCER_CONFIG = { 'org.openedx.learning.certificate.created.v1': { 'learning-certificate-lifecycle': - {'event_key_field': 'certificate.course.course_key', 'enabled': _should_send_certificate_events}, + {'event_key_field': 'certificate.course.course_key', 'enabled': Derived(_should_send_certificate_events)}, }, 'org.openedx.learning.certificate.revoked.v1': { 'learning-certificate-lifecycle': - {'event_key_field': 'certificate.course.course_key', 'enabled': _should_send_certificate_events}, + {'event_key_field': 'certificate.course.course_key', 'enabled': Derived(_should_send_certificate_events)}, }, 'org.openedx.learning.course.unenrollment.completed.v1': { 'course-unenrollment-lifecycle': @@ -5506,33 +5502,16 @@ def _should_send_learning_badge_events(settings): "org.openedx.learning.course.passing.status.updated.v1": { "learning-badges-lifecycle": { "event_key_field": "course_passing_status.course.course_key", - "enabled": _should_send_learning_badge_events, + "enabled": Derived(_should_send_learning_badge_events), }, }, "org.openedx.learning.ccx.course.passing.status.updated.v1": { "learning-badges-lifecycle": { "event_key_field": "course_passing_status.course.ccx_course_key", - "enabled": _should_send_learning_badge_events, + "enabled": Derived(_should_send_learning_badge_events), }, }, } -derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.created.v1', - 'learning-certificate-lifecycle', 'enabled') -derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.revoked.v1', - 'learning-certificate-lifecycle', 'enabled') - -derived_collection_entry( - "EVENT_BUS_PRODUCER_CONFIG", - "org.openedx.learning.course.passing.status.updated.v1", - "learning-badges-lifecycle", - "enabled", -) -derived_collection_entry( - "EVENT_BUS_PRODUCER_CONFIG", - "org.openedx.learning.ccx.course.passing.status.updated.v1", - "learning-badges-lifecycle", - "enabled", -) BEAMER_PRODUCT_ID = "" diff --git a/lms/envs/docs/README.rst b/lms/envs/docs/README.rst index 34211a57517d..e94b62f81b6c 100644 --- a/lms/envs/docs/README.rst +++ b/lms/envs/docs/README.rst @@ -37,6 +37,8 @@ platform, please see `Feature Flags and Settings`_. Derived Settings **************** +TODO UPDATE THIS + In cases where you need to define one or more settings relative to the value of another setting, you can explicitly designate them as derived calculations. This can let you override one setting (such as a path or a feature toggle) and diff --git a/lms/envs/production.py b/lms/envs/production.py index 32c2d54dc8a2..9e979dc15a18 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -28,7 +28,7 @@ from path import Path as path from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType -from openedx.core.lib.derived import derive_settings +from openedx.core.lib.derived import Derived from openedx.core.lib.logsettings import get_logger_config from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed # lint-amnesty, pylint: disable=wrong-import-order @@ -126,37 +126,37 @@ def get_env_setting(setting): "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" ) -## Derived Settings -CC_MERCHANT_NAME = lambda settings: settings.PLATFORM_NAME -EMAIL_FILE_PATH = lambda settings: settings.DATA_DIR / "emails" / "lms" -LMS_INTERNAL_ROOT_URL = lambda settings: settings.LMS_ROOT_URL +CC_MERCHANT_NAME = Derived(lambda settings: settings.PLATFORM_NAME) +EMAIL_FILE_PATH = Derived(lambda settings: settings.DATA_DIR / "emails" / "lms") +LMS_INTERNAL_ROOT_URL = Derived(lambda settings: settings.LMS_ROOT_URL) + # This is the domain that is used to set shared cookies between various sub-domains. # By default, it's set to the same thing as the SESSION_COOKIE_DOMAIN -SHARED_COOKIE_DOMAIN = lambda settings: settings.SESSION_COOKIE_DOMAIN +SHARED_COOKIE_DOMAIN = Derived(lambda settings: settings.SESSION_COOKIE_DOMAIN) # We want Bulk Email running on the high-priority queue, so we define the # routing key that points to it. At the moment, the name is the same. # We have to reset the value here, since we have changed the value of the queue name. -BULK_EMAIL_ROUTING_KEY = lambda settings: settings.HIGH_PRIORITY_QUEUE +BULK_EMAIL_ROUTING_KEY = Derived(lambda settings: settings.HIGH_PRIORITY_QUEUE) # We can run smaller jobs on the low priority queue. See note above for why # we have to reset the value here. -BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = lambda settings: settings.DEFAULT_PRIORITY_QUEUE +BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = Derived(lambda settings: settings.DEFAULT_PRIORITY_QUEUE) # Queue to use for expiring old entitlements -ENTITLEMENTS_EXPIRATION_ROUTING_KEY = lambda settings: settings.DEFAULT_PRIORITY_QUEUE +ENTITLEMENTS_EXPIRATION_ROUTING_KEY = Derived(lambda settings: settings.DEFAULT_PRIORITY_QUEUE) # Intentional defaults. -ID_VERIFICATION_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK -PASSWORD_RESET_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK -ACTIVATION_EMAIL_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK -LOGIN_ISSUE_SUPPORT_LINK = lambda settings: settings.SUPPORT_SITE_LINK +ID_VERIFICATION_SUPPORT_LINK = Derived(lambda settings: settings.SUPPORT_SITE_LINK) +PASSWORD_RESET_SUPPORT_LINK = Derived(lambda settings: settings.SUPPORT_SITE_LINK) +ACTIVATION_EMAIL_SUPPORT_LINK = Derived(lambda settings: settings.SUPPORT_SITE_LINK) +LOGIN_ISSUE_SUPPORT_LINK = Derived(lambda settings: settings.SUPPORT_SITE_LINK) # Default queues for various routes -GRADES_DOWNLOAD_ROUTING_KEY = lambda settings: settings.HIGH_MEM_QUEUE -CREDENTIALS_GENERATION_ROUTING_KEY = lambda settings: settings.DEFAULT_PRIORITY_QUEUE -PROGRAM_CERTIFICATES_ROUTING_KEY = lambda settings: settings.DEFAULT_PRIORITY_QUEUE -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = lambda settings: settings.HIGH_PRIORITY_QUEUE +GRADES_DOWNLOAD_ROUTING_KEY = Derived(lambda settings: settings.HIGH_MEM_QUEUE) +CREDENTIALS_GENERATION_ROUTING_KEY = Derived(lambda settings: settings.DEFAULT_PRIORITY_QUEUE) +PROGRAM_CERTIFICATES_ROUTING_KEY = Derived(lambda settings: settings.DEFAULT_PRIORITY_QUEUE) +SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = Derived(lambda settings: settings.HIGH_PRIORITY_QUEUE) ############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ###################### # The Open edX Enterprise service is currently hosted via the LMS container/process. @@ -165,10 +165,14 @@ def get_env_setting(setting): # not find references to them within the edx-platform project. # Publicly-accessible enrollment URL, for use on the client side. -ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = lambda settings: (settings.LMS_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH +ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = Derived( + lambda settings: (settings.LMS_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH +) # Enrollment URL used on the server-side. -ENTERPRISE_ENROLLMENT_API_URL = lambda settings: (settings.LMS_INTERNAL_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH +ENTERPRISE_ENROLLMENT_API_URL = Derived( + lambda settings: (settings.LMS_INTERNAL_ROOT_URL or '') + settings.LMS_ENROLLMENT_API_PATH +) ############## ENTERPRISE SERVICE API CLIENT CONFIGURATION ###################### # The LMS communicates with the Enterprise service via the requests.Session() client @@ -180,38 +184,13 @@ def _generate_default_enterprise_api_url(settings): if settings.LMS_INTERNAL_ROOT_URL is not None: default_enterprise_api_url = settings.LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' return default_enterprise_api_url - -ENTERPRISE_API_URL = _generate_default_enterprise_api_url +ENTERPRISE_API_URL = Derived(_generate_default_enterprise_api_url) def _generate_default_enterprise_consent_api_url(settings): default_enterprise_consent_api_url = None if settings.LMS_INTERNAL_ROOT_URL is not None: default_enterprise_consent_api_url = settings.LMS_INTERNAL_ROOT_URL + '/consent/api/v1/' -ENTERPRISE_CONSENT_API_URL = lambda settings: _generate_default_enterprise_consent_api_url - -# Note the order of this matters, don't sort this list. -derived( - 'CC_MERCHANT_NAME', - 'EMAIL_FILE_PATH', - 'LMS_INTERNAL_ROOT_URL', - 'SHARED_COOKIE_DOMAIN', - 'BULK_EMAIL_ROUTING_KEY', - 'BULK_EMAIL_ROUTING_KEY_SMALL_JOBS', - 'ENTITLEMENTS_EXPIRATION_ROUTING_KEY', - 'ID_VERIFICATION_SUPPORT_LINK', - 'PASSWORD_RESET_SUPPORT_LINK', - 'ACTIVATION_EMAIL_SUPPORT_LINK', - 'LOGIN_ISSUE_SUPPORT_LINK', - 'GRADES_DOWNLOAD_ROUTING_KEY', - 'CREDENTIALS_GENERATION_ROUTING_KEY', - 'PROGRAM_CERTIFICATES_ROUTING_KEY', - 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', - 'ENTERPRISE_PUBLIC_ENROLLMENT_API_URL', - 'ENTERPRISE_ENROLLMENT_API_URL', - 'ENTERPRISE_API_URL', - 'ENTERPRISE_CONSENT_API_URL', -) - +ENTERPRISE_CONSENT_API_URL = Derived(_generate_default_enterprise_consent_api_url) ####################################################################################################################### @@ -413,17 +392,6 @@ def _generate_default_enterprise_consent_api_url(settings): # use the one from common.py MODULESTORE = convert_module_store_setting_if_needed(_YAML_TOKENS.get('MODULESTORE', MODULESTORE)) -# After conversion above, the modulestore will have a "stores" list with all defined stores, for all stores, add the -# fs_root entry to derived collection so that if it's a callable it can be resolved. We need to do this because the -# `derived_collection_entry` takes an exact index value but the config file might have overridden the number of stores -# and so we can't be sure that the 2 we define in common.py will be there when we try to derive settings. This could -# lead to exceptions being thrown when the `derive_settings` call later in this file tries to update settings. We call -# the derived_collection_entry function here to ensure that we update the fs_root for any callables that remain after -# we've updated the MODULESTORE setting from our config file. -for idx, store in enumerate(MODULESTORE['default']['OPTIONS']['stores']): - if 'OPTIONS' in store and 'fs_root' in store["OPTIONS"]: - derived_collection_entry('MODULESTORE', 'default', 'OPTIONS', 'stores', idx, 'OPTIONS', 'fs_root') - BROKER_URL = "{}://{}:{}@{}/{}".format(CELERY_BROKER_TRANSPORT, CELERY_BROKER_USER, CELERY_BROKER_PASSWORD, diff --git a/openedx/core/lib/derived.py b/openedx/core/lib/derived.py index 47e191614fee..b153091b069a 100644 --- a/openedx/core/lib/derived.py +++ b/openedx/core/lib/derived.py @@ -28,71 +28,58 @@ derive_settings(__name__) ``` """ +from __future__ import annotations import sys +import typing as t +from collections.abc import Sequence -# Global list holding all settings which will be derived. -__DERIVED = [] - -def derived(*settings): +class Derived: """ - Registers settings which are derived from other settings. - Can be called multiple times to add more derived settings. - - Args: - settings (str): Setting names to register. + TODO doc + TODO typing """ - __DERIVED.extend(settings) + def __init__(self, calculate_value: t.Callable): + self.calculate_value = calculate_value -def derived_collection_entry(collection_name, *accessors): +def derive_settings(module_name: str): """ - Registers a setting which is a dictionary or list and needs a derived value for a particular entry. - Can be called multiple times to add more derived settings. + Derives all registered settings and sets them onto a particular module. Args: - collection_name (str): Name of setting which contains a dictionary or list. - accessors (int|str): Sequence of dictionary keys and list indices in the collection (and - collections within it) leading to the value which will be derived. - For example: 0, 'DIRS'. + module_name (str): Name of module to which the derived settings will be added. """ - __DERIVED.append((collection_name, accessors)) + module = sys.modules[module_name] + _derive_dict_items(module, vars(module)) -def derive_settings(module_name): +def _derive_dict_items(settings, the_dict: dict): """ - Derives all registered settings and sets them onto a particular module. - Skips deriving settings that are set to a value. - - Args: - module_name (str): Name of module to which the derived settings will be added. + TODO doc """ - module = sys.modules[module_name] - for derived in __DERIVED: # lint-amnesty, pylint: disable=redefined-outer-name - if isinstance(derived, str): - setting = getattr(module, derived) - if callable(setting): - setting_val = setting(module) - setattr(module, derived, setting_val) - elif isinstance(derived, tuple): - # If a tuple, two elements are expected - else ignore. - if len(derived) == 2: - # The first element is the name of the attribute which is expected to be a dictionary or list. - # The second element is a list of string keys in that dictionary leading to a derived setting. - collection = getattr(module, derived[0]) - accessors = derived[1] - for accessor in accessors[:-1]: - collection = collection[accessor] - setting = collection[accessors[-1]] - if callable(setting): - setting_val = setting(module) - collection[accessors[-1]] = setting_val - - -def clear_for_tests(): + for key, child in the_dict.items(): + if isinstance(child, Derived): + the_dict[key] = child.calculate_value(settings) + elif isinstance(child, Sequence) and not isinstance(child, str): + the_dict[key] = _derive_sequence_items(settings, child) + _derive_sequence_items(settings, child) + elif isinstance(child, dict): + _derive_dict_items(settings, child) + + +def _derive_sequence_items(settings, the_seq: Sequence): """ - Clears all settings to be derived. For tests only. + TODO doc """ - global __DERIVED - __DERIVED = [] + result = [] + for ix, child in enumerate(the_seq): + if isinstance(child, Derived): + result.append(child.calculate_value(settings)) + elif isinstance(child, Sequence) and not isinstance(child, str): + result.append(_derive_sequence_items(settings, child)) + elif isinstance(child, dict): + _derive_dict_items(settings, child) + result.append(child) + return type(the_seq)(result) diff --git a/openedx/core/lib/tests/test_derived.py b/openedx/core/lib/tests/test_derived.py index ef3f98042432..7d3f70fa6ab1 100644 --- a/openedx/core/lib/tests/test_derived.py +++ b/openedx/core/lib/tests/test_derived.py @@ -5,7 +5,7 @@ import sys from unittest import TestCase -from openedx.core.lib.derived import derived, derived_collection_entry, derive_settings, clear_for_tests +from openedx.core.lib.derived import Derived, derive_settings class TestDerivedSettings(TestCase): @@ -14,18 +14,14 @@ class TestDerivedSettings(TestCase): """ def setUp(self): super().setUp() - clear_for_tests() self.module = sys.modules[__name__] self.module.SIMPLE_VALUE = 'paneer' - self.module.DERIVED_VALUE = lambda settings: 'mutter ' + settings.SIMPLE_VALUE - self.module.ANOTHER_DERIVED_VALUE = lambda settings: settings.DERIVED_VALUE + ' with naan' + self.module.DERIVED_VALUE = Derived(lambda settings: 'mutter ' + settings.SIMPLE_VALUE) + self.module.ANOTHER_DERIVED_VALUE = Derived(lambda settings: settings.DERIVED_VALUE + ' with naan') self.module.UNREGISTERED_DERIVED_VALUE = lambda settings: settings.SIMPLE_VALUE + ' is cheese' - derived('DERIVED_VALUE', 'ANOTHER_DERIVED_VALUE') self.module.DICT_VALUE = {} - self.module.DICT_VALUE['test_key'] = lambda settings: settings.DERIVED_VALUE * 3 - derived_collection_entry('DICT_VALUE', 'test_key') - self.module.DICT_VALUE['list_key'] = ['not derived', lambda settings: settings.DERIVED_VALUE] - derived_collection_entry('DICT_VALUE', 'list_key', 1) + self.module.DICT_VALUE['test_key'] = Derived(lambda settings: settings.DERIVED_VALUE * 3) + self.module.DICT_VALUE['list_key'] = ['not derived', Derived(lambda settings: settings.DERIVED_VALUE)] def test_derived_settings_are_derived(self): derive_settings(__name__) diff --git a/xmodule/modulestore/modulestore_settings.py b/xmodule/modulestore/modulestore_settings.py index 05470cf8f1ef..6222f2055a88 100644 --- a/xmodule/modulestore/modulestore_settings.py +++ b/xmodule/modulestore/modulestore_settings.py @@ -6,6 +6,8 @@ import copy import warnings +from openedx.core.lib.derived import Derived + def convert_module_store_setting_if_needed(module_store_setting): """