Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -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
=====

Expand Down
2 changes: 1 addition & 1 deletion eox_theming/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

from __future__ import unicode_literals

__version__ = '10.1.0'
__version__ = '10.1.1'
26 changes: 21 additions & 5 deletions eox_theming/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
77 changes: 77 additions & 0 deletions eox_theming/tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -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'])
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 10.1.0
current_version = 10.1.1
commit = False
tag = False

Expand Down
Loading