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:
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)
Usage:
{% jinja_include "some_template.html" %}
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.
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()
And the Jinja tag to allow Jinja blocks in Django templates.
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))
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.
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
And you’re done, now you can just mix your Django and Jinja templates like this:
{% ifequal foo bar %}
Django style if...
{% endif %}
{% jinja %}
{% if foo == bar %}
Jinja style if...
{% endif %}
{% end_jinja %}
David Cramer responded on 24 Aug 2010 at 2:28 am #
That’s a pretty cool hack :)
Henrique responded on 24 Aug 2010 at 3:20 am #
Wow…
Should we call this metatemplates? :)
Rick van Hattem responded on 24 Aug 2010 at 3:29 am #
Thanks David. Just wondering, would you be interested in having it added to coffin? I can create a patch for the latest version if you’re interested.
Ross responded on 24 Aug 2010 at 7:59 am #
Cool, I also use Jinja with Django and didn’t even consider that approach! Is there much overhead of mixing both Django and Jinja?
I’m lucky in that I use Django 1.2 and creating your own Jinja2 Template loader is trivial: http://www.rosslawley.co.uk/2010/07/django-12-and-jinja2-integration.html. The downside is you have to write your own filters and / or wrap django filters, but I even got the template debugging working with the debug toolbar!
'soup responded on 24 Aug 2010 at 11:16 am #
Mixing Django with Jinja2 without losing template debugging…
…
Rick van Hattem responded on 24 Aug 2010 at 11:32 am #
@Ross: it depends on which approach you take. If you use the Jinja block tag than Jinja won’t be able to cache your parsed template. However, if you use the Jinja include than it would be just as if you normally parse a Jinja template, it still uses byte caching like normal.
It would be trivial to add template caching to the tag though, but I don’t think it matters that much. Especially not for my use case where I need small specific Jinja functions in Django admin templates and otherwise I’ll simply use a full Jinja template.
Cody Soyland responded on 25 Aug 2010 at 6:01 pm #
Very nice — you beat me to writing this tag, as it’s something I’ve thought would be useful before. Is this code available under an open-source license? It would be good to build it into a tag library and put it on Github or elsewhere so people can install and use it more easily.
Rick van Hattem responded on 25 Aug 2010 at 6:16 pm #
I’m considering forking David’s Coffin code and putting it up on Github.
All the code is available under the BSD license :)
Most of this code is actually more than 6 months old, I just didn’t get to writing an article before.
Alexandre Côté responded on 08 Jan 2011 at 9:30 pm #
I think you have a typo in your jinja_include code:
line: filename = bits[1:-1]
should be
filename = filename[1:-1]
the the former case, filename will be “file.html” instead of file.html
Martin Burchell responded on 18 Jul 2011 at 3:59 pm #
We’d be interested in your code to run Django code from Jinja. We’re looking at integrating Askbot, which uses Jinja 2 into our existing site, which uses Django templates and we don’t want to have to rewrite all of our templates in Jinja 2.
flo responded on 15 Sep 2011 at 11:39 am #
I would also be interested in the django-from-jinja code. I would like to use the endless_pagination template tags without converting them to jinja context functions.