diff --git a/CHANGELOG.md b/CHANGELOG.md index b11eb41..3083d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v10.1.1](https://github.com/eduNEXT/eox-theming/compare/v10.1.0...v10.1.1) - (2026-09-03) + +### Fixed + +- **Verawood context processor registration**: Register the ``theming`` context processor on + ``CONTEXT_PROCESSORS`` instead of mutating ``TEMPLATES[*]['OPTIONS']['context_processors']`` + directly. From Verawood onwards those entries are ``Derived`` values (not lists), so the + previous ``.append()`` raised and was silently swallowed, leaving the ``theming`` template + variable undefined and causing every legacy Mako page to fail with + ``AttributeError: 'Undefined' object has no attribute 'options'``. The new approach remains + compatible with earlier releases, where both template engines derive their context processors + from the same ``CONTEXT_PROCESSORS`` list. + ## [v10.1.0](https://github.com/eduNEXT/eox-theming/compare/v10.0.0...v10.1.0) - (2026-06-24) ### Changed diff --git a/README.rst b/README.rst index f1ee0b2..689a563 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ Compatibility Notes +------------------+-----------------+ | Ulmo | >= 10.0.0 | +------------------+-----------------+ -| Verawood | >= 10.1.0 | +| Verawood | >= 10.1.1 | +------------------+-----------------+ The plugin is configured for the latest release (Teak). If you need compatibility for previous releases, go to the README of the relevant version tag and if it is necessary you can change the configuration in ``eox_theming/settings/common.py``. @@ -142,6 +142,21 @@ You need to update the configuration block like this: TEMPLATES[1]["DIRS"] = _make_mako_template_dirs(settings) derive_settings("lms.envs.production") +**Note for Verawood and later versions (>= 10.1.1):** + +Starting from Verawood, the helper was renamed to ``make_mako_template_dirs`` (without the +leading underscore) and moved to ``openedx.envs.common``. Update the configuration block like +this: + + .. code-block:: python + + from django.conf import settings + from openedx.envs.common import make_mako_template_dirs # pylint: disable=import-error + + ENABLE_COMPREHENSIVE_THEMING = True + TEMPLATES[1]["DIRS"] = make_mako_template_dirs(settings) + derive_settings("lms.envs.production") + Usage ===== diff --git a/eox_theming/__init__.py b/eox_theming/__init__.py index 93d140a..a531ad8 100644 --- a/eox_theming/__init__.py +++ b/eox_theming/__init__.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals -__version__ = '10.1.0' +__version__ = '10.1.1' diff --git a/eox_theming/settings/common.py b/eox_theming/settings/common.py index 7a580dc..a809192 100644 --- a/eox_theming/settings/common.py +++ b/eox_theming/settings/common.py @@ -59,14 +59,30 @@ def plugin_settings(settings): try: eox_configuration_path = 'eox_theming.theming.context_processor.eox_configuration' - if eox_configuration_path not in settings.TEMPLATES[0]['OPTIONS']['context_processors']: - settings.TEMPLATES[0]['OPTIONS']['context_processors'].append(eox_configuration_path) - if eox_configuration_path not in settings.TEMPLATES[1]['OPTIONS']['context_processors']: - settings.TEMPLATES[1]['OPTIONS']['context_processors'].append(eox_configuration_path) + # Register the eox-theming context processor on ``CONTEXT_PROCESSORS``. Both the Django + # and Mako template engines build their ``context_processors`` from this setting, so this + # works across releases: earlier releases point each engine's ``context_processors`` at + # this very list, while from Verawood onwards those entries are ``Derived`` values that + # resolve to ``settings.CONTEXT_PROCESSORS`` and can no longer be mutated in place. + # Appending to the ``Derived`` object raised an ``AttributeError`` that was silently + # swallowed, so the ``theming`` variable was never injected and every legacy Mako page + # failed with ``'Undefined' object has no attribute 'options'``. + context_processors = getattr(settings, 'CONTEXT_PROCESSORS', None) + if isinstance(context_processors, list): + if eox_configuration_path not in context_processors: + context_processors.append(eox_configuration_path) + else: + # Fallback for releases that don't expose a ``CONTEXT_PROCESSORS`` setting: register + # on every template engine whose ``context_processors`` is still a plain list. + for engine in settings.TEMPLATES: + engine_context_processors = engine.get('OPTIONS', {}).get('context_processors') + if isinstance(engine_context_processors, list) \ + and eox_configuration_path not in engine_context_processors: + engine_context_processors.append(eox_configuration_path) settings.DEFAULT_TEMPLATE_ENGINE = settings.TEMPLATES[0] except (AttributeError, TypeError): - logger.error("Couldn't set default template engine. Check your settings.") + logger.error("Couldn't register the eox-theming context processor. Check your settings.") try: settings.MIDDLEWARE = [ diff --git a/eox_theming/tests/test_settings.py b/eox_theming/tests/test_settings.py new file mode 100644 index 0000000..3e8fbad --- /dev/null +++ b/eox_theming/tests/test_settings.py @@ -0,0 +1,77 @@ +""" +Tests for :func:`eox_theming.settings.common.plugin_settings`. + +These focus on the registration of the ``theming`` context processor, which must work both on +releases where each template engine's ``context_processors`` is a plain list and on Verawood and +later releases, where those entries are ``Derived`` values that resolve to +``settings.CONTEXT_PROCESSORS``. +""" +from types import SimpleNamespace + +from django.test import TestCase + +from eox_theming.settings.common import plugin_settings + +CONTEXT_PROCESSOR_PATH = 'eox_theming.theming.context_processor.eox_configuration' + + +def build_settings(**overrides): + """Return a minimal settings-like object with everything ``plugin_settings`` touches.""" + settings = SimpleNamespace( + STATICFILES_FINDERS=[], + MIDDLEWARE=[], + TEMPLATES=[ + {'OPTIONS': {'loaders': ['placeholder'], 'context_processors': []}}, + {'OPTIONS': {'context_processors': []}}, + ], + ) + for key, value in overrides.items(): + setattr(settings, key, value) + return settings + + +class DerivedContextProcessors: + """Stand-in for the edx-platform ``Derived`` object: not a list, not iterable.""" + + +class PluginSettingsContextProcessorTests(TestCase): + """Tests for the ``theming`` context processor registration.""" + + def test_registers_on_context_processors_setting(self): + """Verawood+: engines use ``Derived`` values, so registration lands on CONTEXT_PROCESSORS.""" + settings = build_settings(CONTEXT_PROCESSORS=[]) + settings.TEMPLATES[0]['OPTIONS']['context_processors'] = DerivedContextProcessors() + settings.TEMPLATES[1]['OPTIONS']['context_processors'] = DerivedContextProcessors() + + plugin_settings(settings) + + self.assertIn(CONTEXT_PROCESSOR_PATH, settings.CONTEXT_PROCESSORS) + + def test_registration_is_idempotent(self): + """Registering twice must not duplicate the context processor.""" + settings = build_settings(CONTEXT_PROCESSORS=[CONTEXT_PROCESSOR_PATH]) + + plugin_settings(settings) + + self.assertEqual(settings.CONTEXT_PROCESSORS.count(CONTEXT_PROCESSOR_PATH), 1) + + def test_legacy_shared_list_reaches_template_engines(self): + """Legacy releases share one list between CONTEXT_PROCESSORS and every engine.""" + shared = [] + settings = build_settings(CONTEXT_PROCESSORS=shared) + settings.TEMPLATES[0]['OPTIONS']['context_processors'] = shared + settings.TEMPLATES[1]['OPTIONS']['context_processors'] = shared + + plugin_settings(settings) + + self.assertIn(CONTEXT_PROCESSOR_PATH, settings.TEMPLATES[0]['OPTIONS']['context_processors']) + self.assertIn(CONTEXT_PROCESSOR_PATH, settings.TEMPLATES[1]['OPTIONS']['context_processors']) + + def test_fallback_when_no_context_processors_setting(self): + """Without a CONTEXT_PROCESSORS setting, register on each engine's list.""" + settings = build_settings() # no CONTEXT_PROCESSORS attribute + + plugin_settings(settings) + + self.assertIn(CONTEXT_PROCESSOR_PATH, settings.TEMPLATES[0]['OPTIONS']['context_processors']) + self.assertIn(CONTEXT_PROCESSOR_PATH, settings.TEMPLATES[1]['OPTIONS']['context_processors']) diff --git a/setup.cfg b/setup.cfg index 2421578..a5a4f85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 10.1.0 +current_version = 10.1.1 commit = False tag = False