282 lines
8.6 KiB
Python
282 lines
8.6 KiB
Python
"""Adapter for using Jinja2 with Django."""
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import functools
|
|
import logging
|
|
import re
|
|
|
|
from django.apps import apps
|
|
from django.conf import settings
|
|
from django.template.base import Origin, TemplateDoesNotExist
|
|
from django.template.loader import BaseLoader
|
|
|
|
try:
|
|
from importlib import import_module
|
|
except ImportError:
|
|
from django.utils.importlib import import_module
|
|
|
|
try:
|
|
import importlib.util
|
|
if hasattr(importlib.util, 'find_spec'): # Py3>=3.4
|
|
def has_helpers(config):
|
|
module = '%s.helpers' % (config.name,)
|
|
return importlib.util.find_spec(module) is not None
|
|
else: # Py3<3.4
|
|
def has_helpers(config):
|
|
# For Python 3.3, just try to import the module. Unfortunately,
|
|
# this changes the contract slightly for Python 3.3: if there is an
|
|
# module but this raises a legitimate ImportError, jingo will act
|
|
# as if the module doesn't exist. The intent is that we raise
|
|
# legitimate ImportErrors but ignore missing modules.
|
|
try:
|
|
import_module('%s.helpers' % config.name)
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
except ImportError:
|
|
import imp
|
|
|
|
def has_helpers(config):
|
|
try:
|
|
imp.find_module('helpers', [config.path])
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
import jinja2
|
|
|
|
try:
|
|
from django.template.engine import Engine
|
|
|
|
has_engine = True
|
|
|
|
def get_standard_processors():
|
|
return Engine.get_default().template_context_processors
|
|
|
|
except ImportError:
|
|
from django.template.context import get_standard_processors
|
|
has_engine = False
|
|
|
|
VERSION = (0, 9, 0)
|
|
__version__ = '.'.join(map(str, VERSION))
|
|
|
|
EXCLUDE_APPS = (
|
|
'admin',
|
|
'admindocs',
|
|
'registration',
|
|
'context_processors',
|
|
)
|
|
|
|
log = logging.getLogger('jingo')
|
|
|
|
_helpers_loaded = False
|
|
|
|
|
|
class Template(jinja2.Template):
|
|
|
|
def render(self, context={}):
|
|
"""Render's a template, context can be a Django Context or a
|
|
dictionary.
|
|
"""
|
|
# flatten the Django Context into a single dictionary.
|
|
context_dict = {}
|
|
if hasattr(context, 'dicts'):
|
|
for d in context.dicts:
|
|
context_dict.update(d)
|
|
else:
|
|
context_dict = context
|
|
|
|
# Django Debug Toolbar needs a RequestContext-like object in order
|
|
# to inspect context.
|
|
class FakeRequestContext:
|
|
dicts = [context]
|
|
context = FakeRequestContext()
|
|
|
|
# Used by debug_toolbar.
|
|
if settings.TEMPLATE_DEBUG:
|
|
from django.test import signals
|
|
self.origin = Origin(self.filename)
|
|
signals.template_rendered.send(sender=self, template=self,
|
|
context=context)
|
|
|
|
return super(Template, self).render(context_dict)
|
|
|
|
|
|
class Environment(jinja2.Environment):
|
|
template_class = Template
|
|
|
|
def get_template(self, name, parent=None, globals=None):
|
|
"""Make sure our helpers get loaded before any templates."""
|
|
load_helpers()
|
|
return super(Environment, self).get_template(name, parent, globals)
|
|
|
|
def from_string(self, source, globals=None, template_class=None):
|
|
load_helpers()
|
|
return super(Environment, self).from_string(source, globals,
|
|
template_class)
|
|
|
|
|
|
_env = None
|
|
|
|
|
|
def get_env():
|
|
"""Configure and return a jinja2 Environment."""
|
|
global _env
|
|
if _env:
|
|
return _env
|
|
# Mimic Django's setup by loading templates from directories in
|
|
# TEMPLATE_DIRS and packages in INSTALLED_APPS.
|
|
loaders = [jinja2.FileSystemLoader(d) for d in settings.TEMPLATE_DIRS]
|
|
loaders += [jinja2.PackageLoader(c.name) for c in apps.get_app_configs()]
|
|
|
|
opts = {
|
|
'trim_blocks': True,
|
|
'extensions': ['jinja2.ext.i18n', 'jingo.ext.JingoExtension'],
|
|
'autoescape': True,
|
|
'auto_reload': settings.DEBUG,
|
|
'loader': jinja2.ChoiceLoader(loaders),
|
|
}
|
|
|
|
if hasattr(settings, 'JINJA_CONFIG'):
|
|
if hasattr(settings.JINJA_CONFIG, '__call__'):
|
|
config = settings.JINJA_CONFIG()
|
|
else:
|
|
config = settings.JINJA_CONFIG
|
|
opts.update(config)
|
|
|
|
e = Environment(**opts)
|
|
# Install null translations since gettext isn't always loaded up during
|
|
# testing.
|
|
if ('jinja2.ext.i18n' in e.extensions or
|
|
'jinja2.ext.InternationalizationExtension' in e.extensions):
|
|
e.install_null_translations()
|
|
_env = e
|
|
return e
|
|
|
|
|
|
def render_to_string(request, template, context=None):
|
|
"""
|
|
Render a template into a string.
|
|
"""
|
|
def get_context():
|
|
c = {} if context is None else context.copy()
|
|
for processor in get_standard_processors():
|
|
c.update(processor(request))
|
|
return c
|
|
|
|
# If it's not a Template, it must be a path to be loaded.
|
|
if not isinstance(template, jinja2.environment.Template):
|
|
template = get_env().get_template(template)
|
|
|
|
return template.render(get_context())
|
|
|
|
|
|
def load_helpers():
|
|
"""Try to import ``helpers.py`` from each app in INSTALLED_APPS."""
|
|
# We want to wait as long as possible to load helpers so there aren't any
|
|
# weird circular imports with jingo.
|
|
global _helpers_loaded
|
|
if _helpers_loaded:
|
|
return
|
|
_helpers_loaded = True
|
|
|
|
for config in apps.get_app_configs():
|
|
if not has_helpers(config):
|
|
continue
|
|
|
|
import_module('%s.helpers' % config.name)
|
|
|
|
|
|
class Register(object):
|
|
"""Decorators to add filters and functions to the template Environment."""
|
|
def filter(self, f=None, override=True):
|
|
"""Adds the decorated function to Jinja's filter library."""
|
|
def decorator(f):
|
|
@functools.wraps(f)
|
|
def wrapper(*args, **kw):
|
|
return f(*args, **kw)
|
|
return self.filter(wrapper, override)
|
|
|
|
if not f:
|
|
return decorator
|
|
if override or f.__name__ not in get_env().filters:
|
|
get_env().filters[f.__name__] = f
|
|
return f
|
|
|
|
def function(self, f=None, override=True):
|
|
"""Adds the decorated function to Jinja's global namespace."""
|
|
def decorator(f):
|
|
@functools.wraps(f)
|
|
def wrapper(*args, **kw):
|
|
return f(*args, **kw)
|
|
return self.function(wrapper, override)
|
|
|
|
if not f:
|
|
return decorator
|
|
if override or f.__name__ not in get_env().globals:
|
|
get_env().globals[f.__name__] = f
|
|
return f
|
|
|
|
def inclusion_tag(self, template):
|
|
"""Adds a function to Jinja, but like Django's @inclusion_tag."""
|
|
def decorator(f):
|
|
@functools.wraps(f)
|
|
def wrapper(*args, **kw):
|
|
context = f(*args, **kw)
|
|
t = get_env().get_template(template).render(context)
|
|
return jinja2.Markup(t)
|
|
return self.function(wrapper)
|
|
return decorator
|
|
|
|
register = Register()
|
|
|
|
|
|
class Loader(BaseLoader):
|
|
is_usable = True
|
|
|
|
def __init__(self):
|
|
if has_engine:
|
|
super(Loader, self).__init__(Engine.get_default())
|
|
else:
|
|
super(Loader, self).__init__()
|
|
include_pattern = getattr(settings, 'JINGO_INCLUDE_PATTERN', None)
|
|
if include_pattern:
|
|
self.include_re = re.compile(include_pattern)
|
|
else:
|
|
self.include_re = None
|
|
|
|
def _valid_template(self, template_name):
|
|
if self.include_re:
|
|
if not self.include_re.search(template_name):
|
|
return False
|
|
|
|
if hasattr(template_name, 'split'):
|
|
app = template_name.split('/')[0]
|
|
if app in getattr(settings, 'JINGO_EXCLUDE_APPS', EXCLUDE_APPS):
|
|
return False
|
|
|
|
return True
|
|
|
|
def load_template(self, template_name, template_dirs=None):
|
|
if not self._valid_template(template_name):
|
|
raise TemplateDoesNotExist(template_name)
|
|
|
|
try:
|
|
template = get_env().get_template(template_name)
|
|
return template, template.filename
|
|
except jinja2.TemplateNotFound:
|
|
raise TemplateDoesNotExist(template_name)
|
|
|
|
def load_template_source(self, template_name, template_dirs=None):
|
|
if not self._valid_template(template_name):
|
|
raise TemplateDoesNotExist(template_name)
|
|
|
|
try:
|
|
template = get_env().get_template(template_name)
|
|
except jinja2.TemplateNotFound:
|
|
raise TemplateDoesNotExist(template_name)
|
|
|
|
with open(template.filename, 'rb') as fp:
|
|
return (fp.read().decode(settings.FILE_CHARSET), template.filename)
|