diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fef3a2b3de1da94c1aa11cb16cc00277cd7e5ebd..92f01952d3494811d8292cef9c579330ac1e7f76 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +Changed +~~~~~~~ + +* Official apps can now override any setting + `2.8`_ - 2022-03-11 ------------------- diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 966adca96ca4d34a98510185c925433e54021cb7..e329e9bc69ae795aae7c125cd31f69bd5e1ced98 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -7,7 +7,12 @@ from django.utils.translation import gettext_lazy as _ from dynaconf import LazySettings -from .util.core_helpers import get_app_packages, merge_app_settings, monkey_patch +from .util.core_helpers import ( + get_app_packages, + get_app_settings_overrides, + merge_app_settings, + monkey_patch, +) monkey_patch() @@ -1007,3 +1012,5 @@ merge_app_settings("SHELL_PLUS_DONT_LOAD", SHELL_PLUS_DONT_LOAD) # Add django-cleanup after all apps to ensure that it gets all signals as last app INSTALLED_APPS.append("django_cleanup.apps.CleanupConfig") + +locals().update(get_app_settings_overrides()) diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 6988dd53137f2d5ca364bcfd7091d9b1795e7f10..6c9e0f10f0009f4a435c07e7f332605fda7a3e85 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from importlib import import_module, metadata from itertools import groupby from operator import itemgetter +from types import ModuleType from typing import Any, Callable, Dict, Optional, Sequence, Union from warnings import warn @@ -59,9 +60,31 @@ def dt_show_toolbar(request: HttpRequest) -> bool: return False -def get_app_packages() -> Sequence[str]: +def get_app_packages(only_official: bool = False) -> Sequence[str]: """Find all registered apps from the setuptools entrypoint.""" - return [f"{ep.module}.{ep.attr}" for ep in metadata.entry_points().get("aleksis.app", [])] + apps = [] + + for ep in metadata.entry_points().get("aleksis.app", []): + path = f"{ep.module}.{ep.attr}" + if path.startswith("aleksis.apps.") or not only_official: + apps.append(path) + + return apps + + +def get_app_settings_module(app: str) -> Optional[ModuleType]: + """Get the settings module of an app.""" + pkg = ".".join(app.split(".")[:-2]) + mod_settings = None + while "." in pkg: + try: + return import_module(pkg + ".settings") + except ImportError: + # Import errors are non-fatal. + pkg = ".".join(pkg.split(".")[:-1]) + + # The app does not have settings + return None def merge_app_settings( @@ -77,18 +100,8 @@ def merge_app_settings( potentially malicious apps! """ for app in get_app_packages(): - pkg = ".".join(app.split(".")[:-2]) - mod_settings = None - while "." in pkg: - try: - mod_settings = import_module(pkg + ".settings") - except ImportError: - # Import errors are non-fatal. - pkg = ".".join(pkg.split(".")[:-1]) - continue - break + mod_settings = get_app_settings_module(app) if not mod_settings: - # The app does not have settings continue app_setting = getattr(mod_settings, setting, None) @@ -109,6 +122,26 @@ def merge_app_settings( raise TypeError("Only dict and list settings can be merged.") +def get_app_settings_overrides() -> dict[str, Any]: + """Get app settings overrides. + + Official apps (those under the ``aleksis.apps` namespace) can override + or add settings by listing them in their ``settings.overrides``. + """ + overrides = {} + + for app in get_app_packages(True): + mod_settings = get_app_settings_module(app) + if not mod_settings: + continue + + if hasattr(mod_settings, "overrides"): + for name in mod_settings.overrides: + overrides[name] = getattr(mod_settings, name) + + return overrides + + def get_site_preferences(): """Get the preferences manager of the current site.""" from django.contrib.sites.models import Site # noqa diff --git a/docs/dev/06_merging_app_settings.rst b/docs/dev/06_merging_app_settings.rst index ab24e91138cb7ecb646cd5c299a1f8b9833ed8f7..3c0c1affd210c706ac2959d02324f6ad9e009a2b 100644 --- a/docs/dev/06_merging_app_settings.rst +++ b/docs/dev/06_merging_app_settings.rst @@ -3,9 +3,15 @@ Merging of app settings AlekSIS provides features to merge app settings into main ``settings.py``. +Third-party apps can only add values to some select existing settings. +Official apps (those under the ``aleksis.apps.`` namespace) can mark any +setting for overriding. + Currently mergable settings --------------------------- +The following settings can be amended by any app: + * INSTALLED_APPS * DATABASES * YARN_INSTALLED_APPS @@ -24,3 +30,13 @@ the following into your ``settings.py``:: "PORT": 5432, } } + +Overriding any setting +---------------------- + +Official apps only (currently) can override any setting, but need to explicitly +mark it by listing it in a list called ``overrides`` in their ``settings.py``:: + + PAYMENT_MODEL = "tezor.Invoice" + + overrides = ["PAYMENT_MODEL"]