Mixing Django with Jinja2 without losing template debugging

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 %}
Bookmark and Share

Tags:

About Rick van Hattem

Rick van Hattem is a Dutch Internet entrepreneur and co-founder of Fashiolista.com

5 Responses to “Mixing Django with Jinja2 without losing template debugging”

  1. raacer | 2013-10-23 at 02:15:51 #

    Thank you for usefull snippet 🙂

  2. Anup | 2014-01-26 at 00:15:35 #

    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,

Trackbacks/Pingbacks

  1. Mixing Django with Jinja2 without losing template debugging | Django Daily - 2013-12-02

    […] Since I have finally set up my own blog this post has been moved to: http://w.wol.ph/2013/07/28/mixing-django-with-jinja2-without-losing-template-debugging/ […]

  2. django, jinja, jinja2, coffin, template, templatetag, tags - 2014-01-29

    […] Wolph on Mixing Django with Jinja2 without losing template debugging […]

Leave a Reply