At Fashiolista we’ve build nearly the entire site with Jinja instead of the Django template engine.
There are a lot of reasons for choosing Jinja2 over Django for us. Better performance (atleast… it was a lot better with previous Django versions), way more options (named arguments, multiple arguments for filters, etc), macros and simply easier to extend. Writing custom tags is simply not needed anymore since you can just make any function callable from the templates.
But… during the conversion there are always moments when you need a Django function in a Jinja template or vice versa. So… I created a few template tags to allow for Jinja code in Django templates (I’ve also created code to run Django code from Jinja, but I haven’t seen the need for it so I omitted it here).
A Jinja Include tag to include a template and let it be parsed by Jinja from a Django template:
[python]
from django import template
from coffin import shortcuts as jinja_shortcuts
register = template.Library()
class JinjaInclude(template.Node):
def __init__(self, filename):
self.filename = filename
def render(self, context):
return jinja_shortcuts.render_to_string(self.filename, context)
@register.tag
def jinja_include(parser, token):
bits = token.contents.split()
”’Check if a filename was given”’
if len(bits) != 2:
raise template.TemplateSyntaxError(‘%r tag requires the name of the ‘
‘template to be included included ‘ % bits[0])
filename = bits[1]
”’Remove quotes if used”’
if filename[0] in (‘”‘, “‘”) and filename[-1] == filename[0]:
filename = bits[1:-1]
return JinjaInclude(filename)
[/python]
Usage:
[htmldjango]
{% jinja_include “some_template.html” %}
[/htmldjango]
A couple of noop nodes to make sure that when you convert your Jinja templates to be executed from Django, they won’t break because of the missing Django tag.
[python]
from django import template
class Empty(template.Node):
def render(self, context):
return ”
@register.tag
def django(parser, token):
return Empty()
@register.tag
def end_django(parser, token):
return Empty()
[/python]
And the Jinja tag to allow Jinja blocks in Django templates.
[python]
from django import template
from coffin.template import Template
register = template.Library()
class Jinja(template.Node):
def __init__(self, template):
self.template = template
def render(self, context):
return self.template.render(context)
@register.tag
def jinja(parser, token):
”’Create a Jinja template block
Usage:
{% jinja %}
Although you’re in a Django template, code here will be executed by Jinja
{% end_jinja %}
”’
”’Generate the end tag from the currently used tag name”’
end_tag = ‘end_%s’ % token.contents.split()[0]
tokens = []
”’Convert all tokens to the string representation of them
That way we can keep Django template debugging with Jinja and feed the
entire string to Jinja”’
while parser.tokens:
token = parser.next_token()
if token.token_type == template.TOKEN_TEXT:
tokens.append(token.contents)
elif token.token_type == template.TOKEN_VAR:
tokens.append(‘ ‘.join((
template.VARIABLE_TAG_START,
token.contents,
template.VARIABLE_TAG_END,
)))
elif token.token_type == template.TOKEN_BLOCK:
if token.contents == end_tag:
break
tokens.append(‘ ‘.join((
template.BLOCK_TAG_START,
token.contents,
template.BLOCK_TAG_END,
)))
elif token.token_type == template.TOKEN_COMMENT:
pass
else:
raise template.TemplateSyntaxError(‘Unknown token type: “%s”‘ % token.token_type)
”’If our token has a `source` attribute than template_debugging is
enabled. If it’s enabled create a valid source attribute for the Django
template debugger”’
if hasattr(token, ‘source’):
source = token.source[0], (token.source[1][0], token.source[1][1])
else:
source = None
return Jinja(Template(”.join(tokens), source=source))
[/python]
Do note that I have modified the “coffin.template.Template” to enable debugging completely. Just replace the “Template” class in “coffin/template/__init__.py” to make it work.
[python]
def _generate_django_exception(e, source=None):
”’Generate a Django exception from a Jinja source”’
from django.views.debug import linebreak_iter
if source:
exception = DjangoTemplateSyntaxError(e.message)
exception_dict = e.__dict__
del exception_dict[‘source’]
”’Fetch the entire template in a string”’
template_string = source[0].reload()
”’Get the line number from the error message, if available”’
match = re.match(‘.* at (\d+)$’, e.message)
start_index = 0
stop_index = 0
if match:
”’Convert the position found in the stacktrace to a position
the Django template debug system can use”’
position = int(match.group(1)) + source[1][0] + 1
for index in linebreak_iter(template_string):
if index >= position:
stop_index = min(index, position + 3)
start_index = min(index, position – 2)
break
start_index = index
else:
”’So there wasn’t a matching error message, in that case we
simply have to highlight the entire line instead of the specific
words”’
ignore_lines = 0
for i, index in enumerate(linebreak_iter(template_string)):
if source[1][0] > index:
ignore_lines += 1
if i – ignore_lines == e.lineno:
stop_index = index
break
start_index = index
”’Convert the positions to a source that is compatible with the
Django template debugger”’
source = source[0], (
start_index,
stop_index,
)
else:
”’No source available so we let Django fetch it for us”’
lineno = e.lineno – 1
template_string, source = django_loader.find_template_source(e.name)
exception = DjangoTemplateSyntaxError(e.message)
”’Find the positions by the line number given in the exception”’
start_index = 0
for i in range(lineno):
start_index = template_string.index(‘\n’, start_index + 1)
source = source, (
start_index + 1,
template_string.index(‘\n’, start_index + 1) + 1,
)
exception.source = source
return exception
class Template(_Jinja2Template):
“””Fixes the incompabilites between Jinja2’s template class and
Django’s.
The end result should be a class that renders Jinja2 templates but
is compatible with the interface specfied by Django.
This includes flattening a “Context“ instance passed to render
and making sure that this class will automatically use the global
coffin environment.
“””
def __new__(cls, template_string, origin=None, name=None, source=None):
# We accept the “origin” and “name” arguments, but discard them
# right away – Jinja’s Template class (apparently) stores no
# equivalent information.
from coffin.common import env
try:
return env.from_string(template_string, template_class=cls)
except JinjaTemplateSyntaxError, e:
raise _generate_django_exception(e, source)
def __iter__(self):
# TODO: Django allows iterating over the templates nodes. Should
# be parse ourself and iterate over the AST?
raise NotImplementedError()
def render(self, context=None):
“””Differs from Django’s own render() slightly in that makes the
“context“ parameter optional. We try to strike a middle ground
here between implementing Django’s interface while still supporting
Jinja’s own call syntax as well.
“””
if not context:
context = {}
else:
context = dict_from_django_context(context)
try:
return super(Template, self).render(context)
except JinjaTemplateSyntaxError, e:
raise _generate_django_exception(e)
def dict_from_django_context(context):
“””Flattens a Django :class:`django.template.context.Context` object.
“””
if isinstance(context, DjangoContext):
dict_ = {}
# Newest dicts are up front, so update from oldest to newest.
for subcontext in reversed(list(context)):
dict_.update(dict_from_django_context(subcontext))
return dict_
else:
return context
[/python]
And you’re done, now you can just mix your Django and Jinja templates like this:
[htmldjango]
{% ifequal foo bar %}
Django style if…
{% endif %}
{% jinja %}
{% if foo == bar %}
Jinja style if…
{% endif %}
{% end_jinja %}
[/htmldjango]
Thank you for usefull snippet 🙂
Thanks for this post. You had mention “(I’ve also created code to run Django code from Jinja, but I haven’t seen the need for it so I omitted it here).” Is there a way you could publish that code I am in a situation where 3rd party django project that i am using uses jinja2 and I have written django custom template which i want to get it working with jinja2. Thanks,
Hi Anup, I’ve since added an example for you: http://w.wol.ph/2014/01/29/django-template-tags-jinja2/