In Django template rendering, context processors only run once.
But when we do template rendering through edxmako (which we do for
each and every web fragment/XBlock), we can end up having hundreds
of invocations of templates and run context processors for each
separately. This removes that work.
This was originally added in ad5cc3d5 and reverted when we saw
errors in one of our environments that seemed to be related to
context processor code. Those errors persisted after the revert,
and so I'm adding this back in (with some modifications based on
review comments).
122 lines
5.0 KiB
Python
122 lines
5.0 KiB
Python
# Copyright (c) 2008 Mikeal Rogers
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
|
|
from django.conf import settings
|
|
from django.template import Context, engines
|
|
from edx_django_utils.cache import RequestCache
|
|
from mako.template import Template as MakoTemplate
|
|
from six import text_type
|
|
|
|
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')
|
|
|
|
|
|
class Template(object):
|
|
"""
|
|
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"""
|
|
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=None, request=None):
|
|
"""
|
|
This takes a render call with a context (from Django) and translates
|
|
it to a render call on the mako template.
|
|
|
|
When rendering a large sequence of XBlocks, we may end up rendering
|
|
hundreds of small templates. Even if context processors aren't very
|
|
expensive individually, they will quickly add up in that situation. To
|
|
help guard against this, we do context processing once for a given
|
|
request and then cache it.
|
|
"""
|
|
context_object = self._get_context_object(request)
|
|
|
|
request_cache = RequestCache('context_processors')
|
|
cache_response = request_cache.get_cached_response('cp_output')
|
|
if cache_response.is_found:
|
|
context_dictionary = dict(cache_response.value)
|
|
else:
|
|
context_dictionary = self._get_context_processors_output_dict(context_object)
|
|
# The context_dictionary is later updated with template specific
|
|
# variables. There are potentially hundreds of calls to templates
|
|
# rendering and we don't want them to interfere with each other, so
|
|
# we make a copy from the output of the context processors and then
|
|
# recreate a new dict every time we pull from the cache.
|
|
request_cache.set('cp_output', dict(context_dictionary))
|
|
|
|
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['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
|
|
|
|
@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])
|