Commit fdc50c3a by Jeremy Bowman

PLAT-1419 Make edxmako a proper template backend

parent 989c3a30
......@@ -44,11 +44,11 @@ def event(request):
return HttpResponse(status=204)
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
def render_from_lms(template_name, dictionary, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
Render a template using the LMS Mako templates
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
return render_to_string(template_name, dictionary, namespace="lms." + namespace)
def get_parent_xblock(xblock):
......
......@@ -119,7 +119,7 @@ from lms.envs.common import (
VIDEO_TRANSCRIPTS_SETTINGS,
# Methods to derive settings
_make_main_mako_templates,
_make_mako_template_dirs,
_make_locale_paths,
)
from path import Path as path
......@@ -134,7 +134,7 @@ from openedx.core.djangoapps.theming.helpers_dirs import (
get_theme_base_dirs_from_settings
)
from openedx.core.lib.license import LicenseMixin
from openedx.core.lib.derived import derived, derived_dict_entry
from openedx.core.lib.derived import derived, derived_collection_entry
from openedx.core.release import doc_version
############################ FEATURE CONFIGURATION #############################
......@@ -310,11 +310,9 @@ GEOIPV6_PATH = REPO_ROOT / "common/static/data/geoip/GeoIPv6.dat"
############################# TEMPLATE CONFIGURATION #############################
# Mako templating
# TODO: Move the Mako templating into a different engine in TEMPLATES below.
import tempfile
MAKO_MODULE_DIR = os.path.join(tempfile.gettempdir(), 'mako_cms')
MAKO_TEMPLATES = {}
MAIN_MAKO_TEMPLATES_BASE = [
MAKO_TEMPLATE_DIRS_BASE = [
PROJECT_ROOT / 'templates',
COMMON_ROOT / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
......@@ -324,19 +322,27 @@ MAIN_MAKO_TEMPLATES_BASE = [
OPENEDX_ROOT / 'core' / 'lib' / 'license' / 'templates',
CMS_ROOT / 'djangoapps' / 'pipeline_js' / 'templates',
]
MAKO_TEMPLATES['lms.main'] = lms.envs.common.MAIN_MAKO_TEMPLATES_BASE
MAKO_TEMPLATES['main'] = _make_main_mako_templates
derived_dict_entry('MAKO_TEMPLATES', 'main')
CONTEXT_PROCESSORS = (
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.contrib.auth.context_processors.auth', # this is required for admin
'django.template.context_processors.csrf',
'dealer.contrib.django.staff.context_processor', # access git revision
'help_tokens.context_processor',
)
# Django templating
TEMPLATES = [
{
'NAME': 'django',
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# Don't look for template source files inside installed applications.
'APP_DIRS': False,
# Instead, look for template source files in these dirs.
'DIRS': MAIN_MAKO_TEMPLATES_BASE,
'DIRS': _make_mako_template_dirs,
# Options specific to this backend.
'OPTIONS': {
'loaders': (
......@@ -346,21 +352,36 @@ TEMPLATES = [
'edxmako.makoloader.MakoFilesystemLoader',
'edxmako.makoloader.MakoAppDirectoriesLoader',
),
'context_processors': (
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.contrib.auth.context_processors.auth', # this is required for admin
'django.template.context_processors.csrf',
'dealer.contrib.django.staff.context_processor', # access git revision
'help_tokens.context_processor',
),
'context_processors': CONTEXT_PROCESSORS,
# Change 'debug' in your environment settings files - not here.
'debug': False
}
}
},
{
'NAME': 'mako',
'BACKEND': 'edxmako.backend.Mako',
'APP_DIRS': False,
'DIRS': _make_mako_template_dirs,
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
'debug': False,
}
},
{
# This separate copy of the Mako backend is used to render previews using the LMS templates
'NAME': 'preview',
'BACKEND': 'edxmako.backend.Mako',
'APP_DIRS': False,
'DIRS': lms.envs.common.MAKO_TEMPLATE_DIRS_BASE,
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
'debug': False,
'namespace': 'lms.main',
}
},
]
derived_collection_entry('TEMPLATES', 0, 'DIRS')
derived_collection_entry('TEMPLATES', 1, 'DIRS')
DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]
##############################################################################
......
......@@ -14,3 +14,13 @@
LOOKUP = {}
from .paths import add_lookup, lookup_template, clear_lookups, save_lookups
class Engines(object):
"""
Aliases for the available template engines.
Note that the preview engine is only configured for cms.
"""
DJANGO = 'django'
MAKO = 'mako'
PREVIEW = 'preview'
......@@ -20,8 +20,11 @@ class EdxMakoConfig(AppConfig):
IMPORTANT: This method can be called multiple times during application startup. Any changes to this method
must be safe for multiple callers during startup phase.
"""
template_locations = settings.MAKO_TEMPLATES
for namespace, directories in template_locations.items():
for backend in settings.TEMPLATES:
if 'edxmako' not in backend['BACKEND']:
continue
namespace = backend['OPTIONS'].get('namespace', 'main')
directories = backend['DIRS']
clear_lookups(namespace)
for directory in directories:
add_lookup(namespace, directory)
"""
Django template system engine for Mako templates.
"""
from __future__ import absolute_import, unicode_literals
import logging
from django.template import TemplateDoesNotExist, TemplateSyntaxError
from django.template.backends.base import BaseEngine
from django.template.context import _builtin_context_processors
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from mako.exceptions import MakoException, TopLevelLookupException, text_error_template
from openedx.core.djangoapps.theming.helpers import get_template_path
from .paths import lookup_template
from .template import Template
LOGGER = logging.getLogger(__name__)
class Mako(BaseEngine):
"""
A Mako template engine to be added to the ``TEMPLATES`` Django setting.
"""
app_dirname = 'templates'
def __init__(self, params):
"""
Fetches template options, initializing BaseEngine properties,
and assigning our Mako default settings.
Note that OPTIONS contains backend-specific settings.
:param params: This is simply the template dict you
define in your settings file.
"""
params = params.copy()
options = params.pop('OPTIONS').copy()
super(Mako, self).__init__(params)
self.context_processors = options.pop('context_processors', [])
self.namespace = options.pop('namespace', 'main')
def from_string(self, template_code):
try:
return Template(template_code)
except MakoException:
message = text_error_template().render()
raise TemplateSyntaxError(message)
def get_template(self, template_name):
"""
Loads and returns a template for the given name.
"""
template_name = get_template_path(template_name)
try:
return Template(lookup_template(self.namespace, template_name), engine=self)
except TopLevelLookupException:
raise TemplateDoesNotExist(template_name)
@cached_property
def template_context_processors(self):
"""
Collect and cache the active context processors.
"""
context_processors = _builtin_context_processors
context_processors += tuple(self.context_processors)
return tuple(import_string(path) for path in context_processors)
......@@ -2,7 +2,7 @@ import logging
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.template import Engine
from django.template import Engine, engines
from django.template.base import TemplateDoesNotExist
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
from django.template.loaders.filesystem import Loader as FilesystemLoader
......@@ -16,8 +16,8 @@ log = logging.getLogger(__name__)
class MakoLoader(object):
"""
This is a Django loader object which will load the template as a
Mako template if the first line is "## mako". It is based off BaseLoader
in django.template.loader.
Mako template if the first line is "## mako". It is based off Loader
in django.template.loaders.base.
We need this in order to be able to include mako templates inside main_django.html.
"""
......@@ -53,7 +53,8 @@ class MakoLoader(object):
output_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
uri=template_name)
uri=template_name,
engine=engines['mako'])
return template, None
else:
# This is a regular template
......
......@@ -20,24 +20,12 @@ Methods for creating RequestContext for using with Mako templates.
from crum import get_current_request
from django.conf import settings
from django.template import RequestContext
from django.template.context import _builtin_context_processors
from django.utils.module_loading import import_string
import request_cache
from util.request import safe_get_host
def get_template_context_processors():
"""
Returns the context processors defined in settings.TEMPLATES.
"""
context_processors = _builtin_context_processors
context_processors += tuple(settings.DEFAULT_TEMPLATE_ENGINE['OPTIONS']['context_processors'])
return tuple(import_string(path) for path in context_processors)
def get_template_request_context(request=None):
"""
Returns the template processing context to use for the current request,
......@@ -60,13 +48,6 @@ def get_template_request_context(request=None):
context['is_secure'] = request.is_secure()
context['site'] = safe_get_host(request)
# This used to happen when a RequestContext object was initialized but was
# moved to a different part of the logic when template engines were introduced.
# Since we are not using template engines we do this here.
# https://github.com/django/django/commit/37505b6397058bcc3460f23d48a7de9641cd6ef0
for processor in get_template_context_processors():
context.update(processor(request))
request_cache_dict[cache_key] = context
return context
......@@ -18,12 +18,12 @@ from urlparse import urljoin
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.template import Context
from django.template import engines
from edxmako import lookup_template
from edxmako.request_context import get_template_request_context
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_template_path, is_request_in_themed_site
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site
from . import Engines
log = logging.getLogger(__name__)
......@@ -131,7 +131,7 @@ def footer_context_processor(request): # pylint: disable=unused-argument
)
def render_to_string(template_name, dictionary, context=None, namespace='main', request=None):
def render_to_string(template_name, dictionary, namespace='main', request=None):
"""
Render a Mako template to as a string.
......@@ -147,52 +147,23 @@ def render_to_string(template_name, dictionary, context=None, namespace='main',
from the template paths specified in configuration.
dictionary: A dictionary of variables to insert into the template during
rendering.
context: A :class:`~django.template.Context` with values to make
available to the template.
namespace: The Mako namespace to find the named template in.
request: The request to use to construct the RequestContext for rendering
this template. If not supplied, the current request will be used.
"""
if namespace == 'lms.main':
engine = engines[Engines.PREVIEW]
else:
engine = engines[Engines.MAKO]
template = engine.get_template(template_name)
return template.render(dictionary, request)
template_name = get_template_path(template_name)
context_instance = Context(dictionary)
# add dictionary to context_instance
context_instance.update(dictionary or {})
# collapse context_instance to a single dictionary for mako
context_dictionary = {}
context_instance['settings'] = settings
context_instance['EDX_ROOT_URL'] = settings.EDX_ROOT_URL
context_instance['marketing_link'] = marketing_link
context_instance['is_any_marketing_link_set'] = is_any_marketing_link_set
context_instance['is_marketing_link_set'] = is_marketing_link_set
# In various testing contexts, there might not be a current request context.
request_context = get_template_request_context(request)
if request_context:
for item in request_context:
context_dictionary.update(item)
for item in context_instance:
context_dictionary.update(item)
if context:
context_dictionary.update(context)
# "Fix" CSRF token by evaluating the lazy object
KEY_CSRF_TOKENS = ('csrf_token', 'csrf')
for key in KEY_CSRF_TOKENS:
if key in context_dictionary:
context_dictionary[key] = unicode(context_dictionary[key])
# fetch and render template
template = lookup_template(namespace, template_name)
return template.render_unicode(**context_dictionary)
def render_to_response(template_name, dictionary=None, context_instance=None, namespace='main', request=None, **kwargs):
def render_to_response(template_name, dictionary=None, namespace='main', request=None, **kwargs):
"""
Returns a HttpResponse whose content is filled with the result of calling
lookup.get_template(args[0]).render with the passed arguments.
"""
dictionary = dictionary or {}
return HttpResponse(render_to_string(template_name, dictionary, context_instance, namespace, request), **kwargs)
return HttpResponse(render_to_string(template_name, dictionary, namespace, request), **kwargs)
......@@ -13,47 +13,89 @@
# limitations under the License.
from django.conf import settings
from django.template import Context, engines
from mako.template import Template as MakoTemplate
from six import text_type
import edxmako
from edxmako.request_context import get_template_request_context
from edxmako.shortcuts import marketing_link
from . import Engines, LOOKUP
from .request_context import get_template_request_context
from .shortcuts import is_any_marketing_link_set, is_marketing_link_set, marketing_link
KEY_CSRF_TOKENS = ('csrf_token', 'csrf')
# TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate)
class Template(MakoTemplate):
class Template(object):
"""
This bridges the gap between a Mako template and a djano template. It can
be rendered like it is a django template because the arguments are transformed
This bridges the gap between a Mako template and a Django template. It can
be rendered like it is a Django template because the arguments are transformed
in a way that MakoTemplate can understand.
"""
def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False):
kwargs['lookup'] = edxmako.LOOKUP['main']
super(Template, self).__init__(*args, **kwargs)
self.engine = kwargs.pop('engine', engines[Engines.MAKO])
if len(args) and isinstance(args[0], MakoTemplate):
self.mako_template = args[0]
else:
kwargs['lookup'] = LOOKUP['main']
self.mako_template = MakoTemplate(*args, **kwargs)
def render(self, context_instance):
def render(self, context=None, request=None):
"""
This takes a render call with a context (from Django) and translates
it to a render call on the mako template.
"""
# collapse context_instance to a single dictionary for mako
context_dictionary = {}
context_object = self._get_context_object(request)
context_dictionary = self._get_context_processors_output_dict(context_object)
if isinstance(context, Context):
context_dictionary.update(context.flatten())
elif context is not None:
context_dictionary.update(context)
self._add_core_context(context_dictionary)
self._evaluate_lazy_csrf_tokens(context_dictionary)
return self.mako_template.render_unicode(**context_dictionary)
@staticmethod
def _get_context_object(request):
"""
Get a Django RequestContext or Context, as appropriate for the situation.
In some tests, there might not be a current request.
"""
request_context = get_template_request_context(request)
if request_context is not None:
return request_context
else:
return Context({})
# In various testing contexts, there might not be a current request context.
request_context = get_template_request_context()
if request_context:
for item in request_context:
context_dictionary.update(item)
for item in context_instance:
context_dictionary.update(item)
def _get_context_processors_output_dict(self, context_object):
"""
Run the context processors for the given context and get the output as a new dictionary.
"""
with context_object.bind_template(self):
return context_object.flatten()
@staticmethod
def _add_core_context(context_dictionary):
"""
Add to the given dictionary context variables which should always be
present, even when context processors aren't run during tests. Using
a context processor should almost always be preferred to adding more
variables here.
"""
context_dictionary['settings'] = settings
context_dictionary['EDX_ROOT_URL'] = settings.EDX_ROOT_URL
context_dictionary['django_context'] = context_instance
context_dictionary['marketing_link'] = marketing_link
context_dictionary['is_any_marketing_link_set'] = is_any_marketing_link_set
context_dictionary['is_marketing_link_set'] = is_marketing_link_set
return super(Template, self).render_unicode(**context_dictionary)
@staticmethod
def _evaluate_lazy_csrf_tokens(context_dictionary):
"""
Evaluate any lazily-evaluated CSRF tokens in the given context.
"""
for key in KEY_CSRF_TOKENS:
if key in context_dictionary:
context_dictionary[key] = text_type(context_dictionary[key])
from django.template import loader
from django.template.base import Context, Template
from django.template.loader import get_template, select_template
def django_template_include(file_name, mako_context):
"""
This can be used within a mako template to include a django template
in the way that a django-style {% include %} does. Pass it context
which can be the mako context ('context') or a dictionary.
"""
dictionary = dict(mako_context)
return loader.render_to_string(file_name, dictionary=dictionary)
def render_inclusion(func, file_name, takes_context, django_context, *args, **kwargs):
"""
This allows a mako template to call a template tag function (written
for django templates) that is an "inclusion tag". These functions are
decorated with @register.inclusion_tag.
-func: This is the function that is registered as an inclusion tag.
You must import it directly using a python import statement.
-file_name: This is the filename of the template, passed into the
@register.inclusion_tag statement.
-takes_context: This is a parameter of the @register.inclusion_tag.
-django_context: This is an instance of the django context. If this
is a mako template rendered through the regular django rendering calls,
a copy of the django context is available as 'django_context'.
-*args and **kwargs are the arguments to func.
"""
if takes_context:
args = [django_context] + list(args)
_dict = func(*args, **kwargs)
if isinstance(file_name, Template):
t = file_name
elif not isinstance(file_name, basestring) and is_iterable(file_name):
t = select_template(file_name)
else:
t = get_template(file_name)
nodelist = t.nodelist
new_context = Context(_dict)
csrf_token = django_context.get('csrf_token', None)
if csrf_token is not None:
new_context['csrf_token'] = csrf_token
return nodelist.render(new_context)
......@@ -24,14 +24,11 @@ class DemoSystem(object):
self.lookup = TemplateLookup(directories=[path(__file__).dirname() / 'templates'])
self.DEBUG = True
def render_template(self, template_filename, dictionary, context=None):
if context is None:
context = {}
context_dict = {}
context_dict.update(dictionary)
context_dict.update(context)
return self.lookup.get_template(template_filename).render(**context_dict)
def render_template(self, template_filename, dictionary):
"""
Render the specified template with the given dictionary of context data.
"""
return self.lookup.get_template(template_filename).render(**dictionary)
def main():
......
......@@ -8,8 +8,8 @@ import mimetypes
from django.conf import settings
from django.http import Http404, HttpResponseNotFound, HttpResponseServerError
from django.shortcuts import redirect
from django.template import TemplateDoesNotExist
from django.views.decorators.csrf import ensure_csrf_cookie
from mako.exceptions import TopLevelLookupException
from edxmako.shortcuts import render_to_response, render_to_string
from util.cache import cache_if_anonymous
......@@ -51,7 +51,7 @@ def render(request, template):
if template == 'honor.html':
context['allow_iframing'] = True
return render_to_response('static_templates/' + template, context, content_type=content_type)
except TopLevelLookupException:
except TemplateDoesNotExist:
raise Http404
......@@ -68,7 +68,7 @@ def render_press_release(request, slug):
template = slug.lower().replace('-', '_') + ".html"
try:
resp = render_to_response('static_templates/press_releases/' + template, {})
except TopLevelLookupException:
except TemplateDoesNotExist:
raise Http404
else:
return resp
......
......@@ -43,7 +43,7 @@ from openedx.core.djangoapps.theming.helpers_dirs import (
get_themes_unchecked,
get_theme_base_dirs_from_settings
)
from openedx.core.lib.derived import derived, derived_dict_entry
from openedx.core.lib.derived import derived, derived_collection_entry
from openedx.core.release import doc_version
from xmodule.modulestore.modulestore_settings import update_module_store_settings
from xmodule.modulestore.edit_info import EditInfoMixin
......@@ -535,11 +535,9 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'
################################## TEMPLATE CONFIGURATION #####################################
# Mako templating
# TODO: Move the Mako templating into a different engine in TEMPLATES below.
import tempfile
MAKO_MODULE_DIR = os.path.join(tempfile.gettempdir(), 'mako_lms')
MAKO_TEMPLATES = {}
MAIN_MAKO_TEMPLATES_BASE = [
MAKO_TEMPLATE_DIRS_BASE = [
PROJECT_ROOT / 'templates',
COMMON_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
......@@ -550,24 +548,55 @@ MAIN_MAKO_TEMPLATES_BASE = [
]
def _make_main_mako_templates(settings):
def _make_mako_template_dirs(settings):
"""
Derives the final MAKO_TEMPLATES['main'] setting from other settings.
Derives the final Mako template directories list from other settings.
"""
if settings.ENABLE_COMPREHENSIVE_THEMING:
themes_dirs = get_theme_base_dirs_from_settings(settings.COMPREHENSIVE_THEME_DIRS)
for theme in get_themes_unchecked(themes_dirs, PROJECT_ROOT):
if theme.themes_base_dir not in settings.MAIN_MAKO_TEMPLATES_BASE:
settings.MAIN_MAKO_TEMPLATES_BASE.insert(0, theme.themes_base_dir)
for theme in get_themes_unchecked(themes_dirs, settings.PROJECT_ROOT):
if theme.themes_base_dir not in settings.MAKO_TEMPLATE_DIRS_BASE:
settings.MAKO_TEMPLATE_DIRS_BASE.insert(0, theme.themes_base_dir)
if settings.FEATURES.get('USE_MICROSITES', False) and getattr(settings, "MICROSITE_CONFIGURATION", False):
settings.MAIN_MAKO_TEMPLATES_BASE.insert(0, settings.MICROSITE_ROOT_DIR)
return settings.MAIN_MAKO_TEMPLATES_BASE
MAKO_TEMPLATES['main'] = _make_main_mako_templates
derived_dict_entry('MAKO_TEMPLATES', 'main')
settings.MAKO_TEMPLATE_DIRS_BASE.insert(0, settings.MICROSITE_ROOT_DIR)
return settings.MAKO_TEMPLATE_DIRS_BASE
CONTEXT_PROCESSORS = [
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.contrib.auth.context_processors.auth', # this is required for admin
'django.template.context_processors.csrf',
# Added for django-wiki
'django.template.context_processors.media',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai',
# Hack to get required link URLs to password reset templates
'edxmako.shortcuts.marketing_link_context_processor',
# Shoppingcart processor (detects if request.user has a cart)
'shoppingcart.context_processor.user_has_cart_context_processor',
# Timezone processor (sends language and time_zone preference)
'courseware.context_processor.user_timezone_locale_prefs',
# Allows the open edX footer to be leveraged in Django Templates.
'edxmako.shortcuts.footer_context_processor',
# Online contextual help
'help_tokens.context_processor',
'openedx.core.djangoapps.site_configuration.context_processors.configuration_context'
]
# Django templating
TEMPLATES = [
{
'NAME': 'django',
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# Don't look for template source files inside installed applications.
'APP_DIRS': False,
......@@ -588,41 +617,27 @@ TEMPLATES = [
'edxmako.makoloader.MakoFilesystemLoader',
'edxmako.makoloader.MakoAppDirectoriesLoader',
],
'context_processors': [
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.contrib.auth.context_processors.auth', # this is required for admin
'django.template.context_processors.csrf',
# Added for django-wiki
'django.template.context_processors.media',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai',
# Hack to get required link URLs to password reset templates
'edxmako.shortcuts.marketing_link_context_processor',
# Shoppingcart processor (detects if request.user has a cart)
'shoppingcart.context_processor.user_has_cart_context_processor',
# Timezone processor (sends language and time_zone preference)
'courseware.context_processor.user_timezone_locale_prefs',
# Allows the open edX footer to be leveraged in Django Templates.
'edxmako.shortcuts.footer_context_processor',
# Online contextual help
'help_tokens.context_processor',
'openedx.core.djangoapps.site_configuration.context_processors.configuration_context'
],
'context_processors': CONTEXT_PROCESSORS,
# Change 'debug' in your environment settings files - not here.
'debug': False
}
}
},
{
'NAME': 'mako',
'BACKEND': 'edxmako.backend.Mako',
# 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,
# Options specific to this backend.
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
# Change 'debug' in your environment settings files - not here.
'debug': False,
}
},
]
derived_collection_entry('TEMPLATES', 1, 'DIRS')
DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]
DEFAULT_TEMPLATE_ENGINE_DIRS = DEFAULT_TEMPLATE_ENGINE['DIRS'][:]
......@@ -634,8 +649,10 @@ def _add_microsite_dirs_to_default_template_engine(settings):
if settings.FEATURES.get('USE_MICROSITES', False) and getattr(settings, "MICROSITE_CONFIGURATION", False):
DEFAULT_TEMPLATE_ENGINE_DIRS.append(settings.MICROSITE_ROOT_DIR)
return DEFAULT_TEMPLATE_ENGINE_DIRS
DEFAULT_TEMPLATE_ENGINE['DIRS'] = _add_microsite_dirs_to_default_template_engine
derived_dict_entry('DEFAULT_TEMPLATE_ENGINE', 'DIRS')
derived_collection_entry('DEFAULT_TEMPLATE_ENGINE', 'DIRS')
###############################################################################################
......
......@@ -491,7 +491,7 @@ MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
TEST_THEME = COMMON_ROOT / "test" / "test-theme"
# add extra template directory for test-only templates
MAIN_MAKO_TEMPLATES_BASE.extend([
MAKO_TEMPLATE_DIRS_BASE.extend([
COMMON_ROOT / 'test' / 'templates',
COMMON_ROOT / 'test' / 'test_sites',
REPO_ROOT / 'openedx' / 'core' / 'djangolib' / 'tests' / 'templates',
......
......@@ -113,7 +113,7 @@ def send_credit_notifications(username, course_key):
else:
email_body_content = ''
email_body = Template(email_body_content).render([context])
email_body = Template(email_body_content).render(context)
msg_alternative.attach(SafeMIMEText(email_body, _subtype='html', _charset='utf-8'))
# attach logo image
......
......@@ -4,9 +4,9 @@ These views will NOT be shown on production: trying to access them will result
in a 404 error.
"""
from django.http import HttpResponseNotFound
from django.template import TemplateDoesNotExist
from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_response
from mako.exceptions import TopLevelLookupException
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
......@@ -51,5 +51,5 @@ def show_reference_template(request, template):
PageLevelMessages.register_error_message(request, _('This is a test error'))
return render_to_response(template, context)
except TopLevelLookupException:
except TemplateDoesNotExist:
return HttpResponseNotFound('Missing template {template}'.format(template=template))
......@@ -17,21 +17,23 @@ def derived(*settings):
Can be called multiple times to add more derived settings.
Args:
settings (list): List of setting names to register.
settings (str): Setting names to register.
"""
__DERIVED.extend(settings)
def derived_dict_entry(setting_dict, key):
def derived_collection_entry(collection_name, *accessors):
"""
Registers a setting which is a dictionary and needs a derived value for a particular key.
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.
Args:
setting_dict (str): Name of setting which contains a dictionary.
key (str): Name of key in the setting dictionary which will be derived.
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'.
"""
__DERIVED.append((setting_dict, key))
__DERIVED.append((collection_name, accessors))
def derive_settings(module_name):
......@@ -52,13 +54,16 @@ def derive_settings(module_name):
elif isinstance(derived, tuple):
# If a tuple, two elements are expected - else ignore.
if len(derived) == 2:
# Both elements are expected to be strings.
# The first string is the attribute which is expected to be a dictionary.
# The second string is a key in that dictionary containing a derived setting.
setting = getattr(module, derived[0])[derived[1]]
# 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)
getattr(module, derived[0]).update({derived[1]: setting_val})
collection[accessors[-1]] = setting_val
def clear_for_tests():
......
......@@ -4,7 +4,7 @@ Tests for derived.py
import sys
from unittest import TestCase
from openedx.core.lib.derived import derived, derive_settings, clear_for_tests
from openedx.core.lib.derived import derived, derived_collection_entry, derive_settings, clear_for_tests
class TestDerivedSettings(TestCase):
......@@ -22,7 +22,9 @@ class TestDerivedSettings(TestCase):
derived('DERIVED_VALUE', 'ANOTHER_DERIVED_VALUE')
self.module.DICT_VALUE = {}
self.module.DICT_VALUE['test_key'] = lambda settings: settings.DERIVED_VALUE * 3
derived(('DICT_VALUE', 'test_key'))
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)
def test_derived_settings_are_derived(self):
derive_settings(__name__)
......@@ -42,3 +44,7 @@ class TestDerivedSettings(TestCase):
def test_derived_dict_settings(self):
derive_settings(__name__)
self.assertEqual(self.module.DICT_VALUE['test_key'], 'mutter paneermutter paneermutter paneer')
def test_derived_nested_settings(self):
derive_settings(__name__)
self.assertEqual(self.module.DICT_VALUE['list_key'][1], 'mutter paneer')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment