Unverified Commit 067785db by Jeremy Bowman Committed by GitHub

Merge pull request #16710 from edx/jmbowman/PLAT-1419

PLAT-1419 Make edxmako a proper template backend
parents fefdbef2 fdc50c3a
...@@ -44,11 +44,11 @@ def event(request): ...@@ -44,11 +44,11 @@ def event(request):
return HttpResponse(status=204) 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): def get_parent_xblock(xblock):
......
...@@ -119,7 +119,7 @@ from lms.envs.common import ( ...@@ -119,7 +119,7 @@ from lms.envs.common import (
VIDEO_TRANSCRIPTS_SETTINGS, VIDEO_TRANSCRIPTS_SETTINGS,
# Methods to derive settings # Methods to derive settings
_make_main_mako_templates, _make_mako_template_dirs,
_make_locale_paths, _make_locale_paths,
) )
from path import Path as path from path import Path as path
...@@ -134,7 +134,7 @@ from openedx.core.djangoapps.theming.helpers_dirs import ( ...@@ -134,7 +134,7 @@ from openedx.core.djangoapps.theming.helpers_dirs import (
get_theme_base_dirs_from_settings get_theme_base_dirs_from_settings
) )
from openedx.core.lib.license import LicenseMixin 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 from openedx.core.release import doc_version
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -310,11 +310,9 @@ GEOIPV6_PATH = REPO_ROOT / "common/static/data/geoip/GeoIPv6.dat" ...@@ -310,11 +310,9 @@ GEOIPV6_PATH = REPO_ROOT / "common/static/data/geoip/GeoIPv6.dat"
############################# TEMPLATE CONFIGURATION ############################# ############################# TEMPLATE CONFIGURATION #############################
# Mako templating # Mako templating
# TODO: Move the Mako templating into a different engine in TEMPLATES below.
import tempfile import tempfile
MAKO_MODULE_DIR = os.path.join(tempfile.gettempdir(), 'mako_cms') MAKO_MODULE_DIR = os.path.join(tempfile.gettempdir(), 'mako_cms')
MAKO_TEMPLATES = {} MAKO_TEMPLATE_DIRS_BASE = [
MAIN_MAKO_TEMPLATES_BASE = [
PROJECT_ROOT / 'templates', PROJECT_ROOT / 'templates',
COMMON_ROOT / 'templates', COMMON_ROOT / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates', COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
...@@ -324,19 +322,27 @@ MAIN_MAKO_TEMPLATES_BASE = [ ...@@ -324,19 +322,27 @@ MAIN_MAKO_TEMPLATES_BASE = [
OPENEDX_ROOT / 'core' / 'lib' / 'license' / 'templates', OPENEDX_ROOT / 'core' / 'lib' / 'license' / 'templates',
CMS_ROOT / 'djangoapps' / 'pipeline_js' / '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 CONTEXT_PROCESSORS = (
derived_dict_entry('MAKO_TEMPLATES', 'main') '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 # Django templating
TEMPLATES = [ TEMPLATES = [
{ {
'NAME': 'django',
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
# Don't look for template source files inside installed applications. # Don't look for template source files inside installed applications.
'APP_DIRS': False, 'APP_DIRS': False,
# Instead, look for template source files in these dirs. # 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 specific to this backend.
'OPTIONS': { 'OPTIONS': {
'loaders': ( 'loaders': (
...@@ -346,21 +352,36 @@ TEMPLATES = [ ...@@ -346,21 +352,36 @@ TEMPLATES = [
'edxmako.makoloader.MakoFilesystemLoader', 'edxmako.makoloader.MakoFilesystemLoader',
'edxmako.makoloader.MakoAppDirectoriesLoader', 'edxmako.makoloader.MakoAppDirectoriesLoader',
), ),
'context_processors': ( 'context_processors': 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',
),
# Change 'debug' in your environment settings files - not here. # Change 'debug' in your environment settings files - not here.
'debug': False '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] DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]
############################################################################## ##############################################################################
......
...@@ -14,3 +14,13 @@ ...@@ -14,3 +14,13 @@
LOOKUP = {} LOOKUP = {}
from .paths import add_lookup, lookup_template, clear_lookups, save_lookups 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): ...@@ -20,8 +20,11 @@ class EdxMakoConfig(AppConfig):
IMPORTANT: This method can be called multiple times during application startup. Any changes to this method 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. must be safe for multiple callers during startup phase.
""" """
template_locations = settings.MAKO_TEMPLATES for backend in settings.TEMPLATES:
for namespace, directories in template_locations.items(): if 'edxmako' not in backend['BACKEND']:
continue
namespace = backend['OPTIONS'].get('namespace', 'main')
directories = backend['DIRS']
clear_lookups(namespace) clear_lookups(namespace)
for directory in directories: for directory in directories:
add_lookup(namespace, directory) 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 ...@@ -2,7 +2,7 @@ import logging
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured 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.base import TemplateDoesNotExist
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
from django.template.loaders.filesystem import Loader as FilesystemLoader from django.template.loaders.filesystem import Loader as FilesystemLoader
...@@ -16,8 +16,8 @@ log = logging.getLogger(__name__) ...@@ -16,8 +16,8 @@ log = logging.getLogger(__name__)
class MakoLoader(object): class MakoLoader(object):
""" """
This is a Django loader object which will load the template as a 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 Mako template if the first line is "## mako". It is based off Loader
in django.template.loader. in django.template.loaders.base.
We need this in order to be able to include mako templates inside main_django.html. We need this in order to be able to include mako templates inside main_django.html.
""" """
...@@ -53,7 +53,8 @@ class MakoLoader(object): ...@@ -53,7 +53,8 @@ class MakoLoader(object):
output_encoding='utf-8', output_encoding='utf-8',
default_filters=['decode.utf8'], default_filters=['decode.utf8'],
encoding_errors='replace', encoding_errors='replace',
uri=template_name) uri=template_name,
engine=engines['mako'])
return template, None return template, None
else: else:
# This is a regular template # This is a regular template
......
...@@ -20,24 +20,12 @@ Methods for creating RequestContext for using with Mako templates. ...@@ -20,24 +20,12 @@ Methods for creating RequestContext for using with Mako templates.
from crum import get_current_request from crum import get_current_request
from django.conf import settings
from django.template import RequestContext from django.template import RequestContext
from django.template.context import _builtin_context_processors
from django.utils.module_loading import import_string
import request_cache import request_cache
from util.request import safe_get_host 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): def get_template_request_context(request=None):
""" """
Returns the template processing context to use for the current request, Returns the template processing context to use for the current request,
...@@ -60,13 +48,6 @@ def get_template_request_context(request=None): ...@@ -60,13 +48,6 @@ def get_template_request_context(request=None):
context['is_secure'] = request.is_secure() context['is_secure'] = request.is_secure()
context['site'] = safe_get_host(request) 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 request_cache_dict[cache_key] = context
return context return context
...@@ -18,12 +18,12 @@ from urlparse import urljoin ...@@ -18,12 +18,12 @@ from urlparse import urljoin
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponse 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.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__) log = logging.getLogger(__name__)
...@@ -131,7 +131,7 @@ def footer_context_processor(request): # pylint: disable=unused-argument ...@@ -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. Render a Mako template to as a string.
...@@ -147,52 +147,23 @@ def render_to_string(template_name, dictionary, context=None, namespace='main', ...@@ -147,52 +147,23 @@ def render_to_string(template_name, dictionary, context=None, namespace='main',
from the template paths specified in configuration. from the template paths specified in configuration.
dictionary: A dictionary of variables to insert into the template during dictionary: A dictionary of variables to insert into the template during
rendering. 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. namespace: The Mako namespace to find the named template in.
request: The request to use to construct the RequestContext for rendering request: The request to use to construct the RequestContext for rendering
this template. If not supplied, the current request will be used. 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) def render_to_response(template_name, dictionary=None, namespace='main', request=None, **kwargs):
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):
""" """
Returns a HttpResponse whose content is filled with the result of calling Returns a HttpResponse whose content is filled with the result of calling
lookup.get_template(args[0]).render with the passed arguments. lookup.get_template(args[0]).render with the passed arguments.
""" """
dictionary = dictionary or {} 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 @@ ...@@ -13,47 +13,89 @@
# limitations under the License. # limitations under the License.
from django.conf import settings from django.conf import settings
from django.template import Context, engines
from mako.template import Template as MakoTemplate from mako.template import Template as MakoTemplate
from six import text_type
import edxmako from . import Engines, LOOKUP
from edxmako.request_context import get_template_request_context from .request_context import get_template_request_context
from edxmako.shortcuts import marketing_link 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(object):
class Template(MakoTemplate):
""" """
This bridges the gap between a Mako template and a djano template. It can 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 be rendered like it is a Django template because the arguments are transformed
in a way that MakoTemplate can understand. in a way that MakoTemplate can understand.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides""" """Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False): self.engine = kwargs.pop('engine', engines[Engines.MAKO])
kwargs['lookup'] = edxmako.LOOKUP['main'] if len(args) and isinstance(args[0], MakoTemplate):
super(Template, self).__init__(*args, **kwargs) 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 This takes a render call with a context (from Django) and translates
it to a render call on the mako template. it to a render call on the mako template.
""" """
# collapse context_instance to a single dictionary for mako context_object = self._get_context_object(request)
context_dictionary = {} 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. def _get_context_processors_output_dict(self, context_object):
request_context = get_template_request_context() """
if request_context: Run the context processors for the given context and get the output as a new dictionary.
for item in request_context: """
context_dictionary.update(item) with context_object.bind_template(self):
for item in context_instance: return context_object.flatten()
context_dictionary.update(item)
@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['settings'] = settings
context_dictionary['EDX_ROOT_URL'] = settings.EDX_ROOT_URL context_dictionary['EDX_ROOT_URL'] = settings.EDX_ROOT_URL
context_dictionary['django_context'] = context_instance
context_dictionary['marketing_link'] = marketing_link 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): ...@@ -24,14 +24,11 @@ class DemoSystem(object):
self.lookup = TemplateLookup(directories=[path(__file__).dirname() / 'templates']) self.lookup = TemplateLookup(directories=[path(__file__).dirname() / 'templates'])
self.DEBUG = True self.DEBUG = True
def render_template(self, template_filename, dictionary, context=None): def render_template(self, template_filename, dictionary):
if context is None: """
context = {} Render the specified template with the given dictionary of context data.
"""
context_dict = {} return self.lookup.get_template(template_filename).render(**dictionary)
context_dict.update(dictionary)
context_dict.update(context)
return self.lookup.get_template(template_filename).render(**context_dict)
def main(): def main():
......
...@@ -8,8 +8,8 @@ import mimetypes ...@@ -8,8 +8,8 @@ import mimetypes
from django.conf import settings from django.conf import settings
from django.http import Http404, HttpResponseNotFound, HttpResponseServerError from django.http import Http404, HttpResponseNotFound, HttpResponseServerError
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import TemplateDoesNotExist
from django.views.decorators.csrf import ensure_csrf_cookie 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 edxmako.shortcuts import render_to_response, render_to_string
from util.cache import cache_if_anonymous from util.cache import cache_if_anonymous
...@@ -51,7 +51,7 @@ def render(request, template): ...@@ -51,7 +51,7 @@ def render(request, template):
if template == 'honor.html': if template == 'honor.html':
context['allow_iframing'] = True context['allow_iframing'] = True
return render_to_response('static_templates/' + template, context, content_type=content_type) return render_to_response('static_templates/' + template, context, content_type=content_type)
except TopLevelLookupException: except TemplateDoesNotExist:
raise Http404 raise Http404
...@@ -68,7 +68,7 @@ def render_press_release(request, slug): ...@@ -68,7 +68,7 @@ def render_press_release(request, slug):
template = slug.lower().replace('-', '_') + ".html" template = slug.lower().replace('-', '_') + ".html"
try: try:
resp = render_to_response('static_templates/press_releases/' + template, {}) resp = render_to_response('static_templates/press_releases/' + template, {})
except TopLevelLookupException: except TemplateDoesNotExist:
raise Http404 raise Http404
else: else:
return resp return resp
......
...@@ -43,7 +43,7 @@ from openedx.core.djangoapps.theming.helpers_dirs import ( ...@@ -43,7 +43,7 @@ from openedx.core.djangoapps.theming.helpers_dirs import (
get_themes_unchecked, get_themes_unchecked,
get_theme_base_dirs_from_settings 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 openedx.core.release import doc_version
from xmodule.modulestore.modulestore_settings import update_module_store_settings from xmodule.modulestore.modulestore_settings import update_module_store_settings
from xmodule.modulestore.edit_info import EditInfoMixin from xmodule.modulestore.edit_info import EditInfoMixin
...@@ -535,11 +535,9 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application' ...@@ -535,11 +535,9 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'
################################## TEMPLATE CONFIGURATION ##################################### ################################## TEMPLATE CONFIGURATION #####################################
# Mako templating # Mako templating
# TODO: Move the Mako templating into a different engine in TEMPLATES below.
import tempfile import tempfile
MAKO_MODULE_DIR = os.path.join(tempfile.gettempdir(), 'mako_lms') MAKO_MODULE_DIR = os.path.join(tempfile.gettempdir(), 'mako_lms')
MAKO_TEMPLATES = {} MAKO_TEMPLATE_DIRS_BASE = [
MAIN_MAKO_TEMPLATES_BASE = [
PROJECT_ROOT / 'templates', PROJECT_ROOT / 'templates',
COMMON_ROOT / 'templates', COMMON_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates', COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
...@@ -550,24 +548,55 @@ MAIN_MAKO_TEMPLATES_BASE = [ ...@@ -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: if settings.ENABLE_COMPREHENSIVE_THEMING:
themes_dirs = get_theme_base_dirs_from_settings(settings.COMPREHENSIVE_THEME_DIRS) themes_dirs = get_theme_base_dirs_from_settings(settings.COMPREHENSIVE_THEME_DIRS)
for theme in get_themes_unchecked(themes_dirs, PROJECT_ROOT): for theme in get_themes_unchecked(themes_dirs, settings.PROJECT_ROOT):
if theme.themes_base_dir not in settings.MAIN_MAKO_TEMPLATES_BASE: if theme.themes_base_dir not in settings.MAKO_TEMPLATE_DIRS_BASE:
settings.MAIN_MAKO_TEMPLATES_BASE.insert(0, theme.themes_base_dir) settings.MAKO_TEMPLATE_DIRS_BASE.insert(0, theme.themes_base_dir)
if settings.FEATURES.get('USE_MICROSITES', False) and getattr(settings, "MICROSITE_CONFIGURATION", False): if settings.FEATURES.get('USE_MICROSITES', False) and getattr(settings, "MICROSITE_CONFIGURATION", False):
settings.MAIN_MAKO_TEMPLATES_BASE.insert(0, settings.MICROSITE_ROOT_DIR) settings.MAKO_TEMPLATE_DIRS_BASE.insert(0, settings.MICROSITE_ROOT_DIR)
return settings.MAIN_MAKO_TEMPLATES_BASE return settings.MAKO_TEMPLATE_DIRS_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',
# 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 # Django templating
TEMPLATES = [ TEMPLATES = [
{ {
'NAME': 'django',
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
# Don't look for template source files inside installed applications. # Don't look for template source files inside installed applications.
'APP_DIRS': False, 'APP_DIRS': False,
...@@ -588,41 +617,27 @@ TEMPLATES = [ ...@@ -588,41 +617,27 @@ TEMPLATES = [
'edxmako.makoloader.MakoFilesystemLoader', 'edxmako.makoloader.MakoFilesystemLoader',
'edxmako.makoloader.MakoAppDirectoriesLoader', 'edxmako.makoloader.MakoAppDirectoriesLoader',
], ],
'context_processors': [ 'context_processors': 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'
],
# Change 'debug' in your environment settings files - not here. # Change 'debug' in your environment settings files - not here.
'debug': False '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 = TEMPLATES[0]
DEFAULT_TEMPLATE_ENGINE_DIRS = DEFAULT_TEMPLATE_ENGINE['DIRS'][:] DEFAULT_TEMPLATE_ENGINE_DIRS = DEFAULT_TEMPLATE_ENGINE['DIRS'][:]
...@@ -634,8 +649,10 @@ def _add_microsite_dirs_to_default_template_engine(settings): ...@@ -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): if settings.FEATURES.get('USE_MICROSITES', False) and getattr(settings, "MICROSITE_CONFIGURATION", False):
DEFAULT_TEMPLATE_ENGINE_DIRS.append(settings.MICROSITE_ROOT_DIR) DEFAULT_TEMPLATE_ENGINE_DIRS.append(settings.MICROSITE_ROOT_DIR)
return DEFAULT_TEMPLATE_ENGINE_DIRS return DEFAULT_TEMPLATE_ENGINE_DIRS
DEFAULT_TEMPLATE_ENGINE['DIRS'] = _add_microsite_dirs_to_default_template_engine 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' ...@@ -491,7 +491,7 @@ MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
TEST_THEME = COMMON_ROOT / "test" / "test-theme" TEST_THEME = COMMON_ROOT / "test" / "test-theme"
# add extra template directory for test-only templates # 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' / 'templates',
COMMON_ROOT / 'test' / 'test_sites', COMMON_ROOT / 'test' / 'test_sites',
REPO_ROOT / 'openedx' / 'core' / 'djangolib' / 'tests' / 'templates', REPO_ROOT / 'openedx' / 'core' / 'djangolib' / 'tests' / 'templates',
......
...@@ -113,7 +113,7 @@ def send_credit_notifications(username, course_key): ...@@ -113,7 +113,7 @@ def send_credit_notifications(username, course_key):
else: else:
email_body_content = '' 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')) msg_alternative.attach(SafeMIMEText(email_body, _subtype='html', _charset='utf-8'))
# attach logo image # attach logo image
......
...@@ -4,9 +4,9 @@ These views will NOT be shown on production: trying to access them will result ...@@ -4,9 +4,9 @@ These views will NOT be shown on production: trying to access them will result
in a 404 error. in a 404 error.
""" """
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from django.template import TemplateDoesNotExist
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from mako.exceptions import TopLevelLookupException
from openedx.core.djangoapps.util.user_messages import PageLevelMessages from openedx.core.djangoapps.util.user_messages import PageLevelMessages
...@@ -51,5 +51,5 @@ def show_reference_template(request, template): ...@@ -51,5 +51,5 @@ def show_reference_template(request, template):
PageLevelMessages.register_error_message(request, _('This is a test error')) PageLevelMessages.register_error_message(request, _('This is a test error'))
return render_to_response(template, context) return render_to_response(template, context)
except TopLevelLookupException: except TemplateDoesNotExist:
return HttpResponseNotFound('Missing template {template}'.format(template=template)) return HttpResponseNotFound('Missing template {template}'.format(template=template))
...@@ -17,21 +17,23 @@ def derived(*settings): ...@@ -17,21 +17,23 @@ def derived(*settings):
Can be called multiple times to add more derived settings. Can be called multiple times to add more derived settings.
Args: Args:
settings (list): List of setting names to register. settings (str): Setting names to register.
""" """
__DERIVED.extend(settings) __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. Can be called multiple times to add more derived settings.
Args: Args:
setting_dict (str): Name of setting which contains a dictionary. collection_name (str): Name of setting which contains a dictionary or list.
key (str): Name of key in the setting dictionary which will be derived. 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): def derive_settings(module_name):
...@@ -52,13 +54,16 @@ def derive_settings(module_name): ...@@ -52,13 +54,16 @@ def derive_settings(module_name):
elif isinstance(derived, tuple): elif isinstance(derived, tuple):
# If a tuple, two elements are expected - else ignore. # If a tuple, two elements are expected - else ignore.
if len(derived) == 2: if len(derived) == 2:
# Both elements are expected to be strings. # The first element is the name of the attribute which is expected to be a dictionary or list.
# The first string is the attribute which is expected to be a dictionary. # The second element is a list of string keys in that dictionary leading to a derived setting.
# The second string is a key in that dictionary containing a derived setting. collection = getattr(module, derived[0])
setting = getattr(module, derived[0])[derived[1]] accessors = derived[1]
for accessor in accessors[:-1]:
collection = collection[accessor]
setting = collection[accessors[-1]]
if callable(setting): if callable(setting):
setting_val = setting(module) setting_val = setting(module)
getattr(module, derived[0]).update({derived[1]: setting_val}) collection[accessors[-1]] = setting_val
def clear_for_tests(): def clear_for_tests():
......
...@@ -4,7 +4,7 @@ Tests for derived.py ...@@ -4,7 +4,7 @@ Tests for derived.py
import sys import sys
from unittest import TestCase 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): class TestDerivedSettings(TestCase):
...@@ -22,7 +22,9 @@ class TestDerivedSettings(TestCase): ...@@ -22,7 +22,9 @@ class TestDerivedSettings(TestCase):
derived('DERIVED_VALUE', 'ANOTHER_DERIVED_VALUE') derived('DERIVED_VALUE', 'ANOTHER_DERIVED_VALUE')
self.module.DICT_VALUE = {} self.module.DICT_VALUE = {}
self.module.DICT_VALUE['test_key'] = lambda settings: settings.DERIVED_VALUE * 3 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): def test_derived_settings_are_derived(self):
derive_settings(__name__) derive_settings(__name__)
...@@ -42,3 +44,7 @@ class TestDerivedSettings(TestCase): ...@@ -42,3 +44,7 @@ class TestDerivedSettings(TestCase):
def test_derived_dict_settings(self): def test_derived_dict_settings(self):
derive_settings(__name__) derive_settings(__name__)
self.assertEqual(self.module.DICT_VALUE['test_key'], 'mutter paneermutter paneermutter paneer') 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