diff --git a/cms/djangoapps/contentstore/views/helpers.py b/cms/djangoapps/contentstore/views/helpers.py index 29e0f0bc6b..290a84deb0 100644 --- a/cms/djangoapps/contentstore/views/helpers.py +++ b/cms/djangoapps/contentstore/views/helpers.py @@ -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): diff --git a/cms/envs/common.py b/cms/envs/common.py index 5257b81e06..24fa7cd7b2 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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] ############################################################################## diff --git a/common/djangoapps/edxmako/__init__.py b/common/djangoapps/edxmako/__init__.py index e7b68f44c3..7a8614bca5 100644 --- a/common/djangoapps/edxmako/__init__.py +++ b/common/djangoapps/edxmako/__init__.py @@ -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' diff --git a/common/djangoapps/edxmako/apps.py b/common/djangoapps/edxmako/apps.py index 20fe2a5a1c..fa1aa515f8 100644 --- a/common/djangoapps/edxmako/apps.py +++ b/common/djangoapps/edxmako/apps.py @@ -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) diff --git a/common/djangoapps/edxmako/backend.py b/common/djangoapps/edxmako/backend.py new file mode 100644 index 0000000000..43690901a5 --- /dev/null +++ b/common/djangoapps/edxmako/backend.py @@ -0,0 +1,67 @@ +""" +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) diff --git a/common/djangoapps/edxmako/makoloader.py b/common/djangoapps/edxmako/makoloader.py index 8ec7466217..cbff64e23e 100644 --- a/common/djangoapps/edxmako/makoloader.py +++ b/common/djangoapps/edxmako/makoloader.py @@ -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 diff --git a/common/djangoapps/edxmako/management/__init__.py b/common/djangoapps/edxmako/management/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/djangoapps/edxmako/management/commands/__init__.py b/common/djangoapps/edxmako/management/commands/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/djangoapps/edxmako/request_context.py b/common/djangoapps/edxmako/request_context.py index 41b0f466fa..71a7a42852 100644 --- a/common/djangoapps/edxmako/request_context.py +++ b/common/djangoapps/edxmako/request_context.py @@ -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 diff --git a/common/djangoapps/edxmako/shortcuts.py b/common/djangoapps/edxmako/shortcuts.py index d3187e08fe..f8045347e3 100644 --- a/common/djangoapps/edxmako/shortcuts.py +++ b/common/djangoapps/edxmako/shortcuts.py @@ -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. """ - - 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) + if namespace == 'lms.main': + engine = engines[Engines.PREVIEW] + else: + engine = engines[Engines.MAKO] + template = engine.get_template(template_name) + return template.render(dictionary, request) -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) diff --git a/common/djangoapps/edxmako/template.py b/common/djangoapps/edxmako/template.py index 2be367f966..3a4f8cb9dd 100644 --- a/common/djangoapps/edxmako/template.py +++ b/common/djangoapps/edxmako/template.py @@ -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) - # 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) + 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({}) + + 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]) diff --git a/common/djangoapps/edxmako/templatetag_helpers.py b/common/djangoapps/edxmako/templatetag_helpers.py deleted file mode 100644 index 153944bb77..0000000000 --- a/common/djangoapps/edxmako/templatetag_helpers.py +++ /dev/null @@ -1,52 +0,0 @@ -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) diff --git a/common/lib/capa/capa/checker.py b/common/lib/capa/capa/checker.py index a481be0c44..e0cd7c6950 100755 --- a/common/lib/capa/capa/checker.py +++ b/common/lib/capa/capa/checker.py @@ -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(): diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index 67bc790280..0082c21186 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -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 diff --git a/lms/envs/common.py b/lms/envs/common.py index 3a6aa5246e..c4e50c175d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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') ############################################################################################### diff --git a/lms/envs/test.py b/lms/envs/test.py index d2db7bf33b..7d028eb7c3 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -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', diff --git a/openedx/core/djangoapps/credit/email_utils.py b/openedx/core/djangoapps/credit/email_utils.py index cc7ebadf29..99670b0634 100644 --- a/openedx/core/djangoapps/credit/email_utils.py +++ b/openedx/core/djangoapps/credit/email_utils.py @@ -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 diff --git a/openedx/core/djangoapps/debug/views.py b/openedx/core/djangoapps/debug/views.py index aa6ec5f6ad..3573f3c8f7 100644 --- a/openedx/core/djangoapps/debug/views.py +++ b/openedx/core/djangoapps/debug/views.py @@ -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)) diff --git a/openedx/core/lib/derived.py b/openedx/core/lib/derived.py index 63497e6756..de21fe8a64 100644 --- a/openedx/core/lib/derived.py +++ b/openedx/core/lib/derived.py @@ -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(): diff --git a/openedx/core/lib/tests/test_derived.py b/openedx/core/lib/tests/test_derived.py index c42a20bdee..ec6e6532dc 100644 --- a/openedx/core/lib/tests/test_derived.py +++ b/openedx/core/lib/tests/test_derived.py @@ -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')