Django templatetag, RequestContext, and inclusion_tag

27Jan09

We have a templatetag, called scurl, that needs to look at the HttpRequest object. Django’s templating system provides a straight-forward, yet wordy, mechanism to pass the request object in to the template:


  def my_view(request):
    return render_to_response("myview.html", context_instance=RequestContext(request))

The render method of our scurl templatetag gets access to this context and thus access to the HttpRequest.

So far, so good.

In our project, we also use inclusion_tags to include common chunks of HTML into pages. This sort of tag looks something like this:


  @register.inclusion_tag("html-to-include.html")
  def my_include_tag(myparam):
    return {"inclusion_param":myparam}

inclusion_tag is a nice time saving decorator that automatically pulls up the appropriate template and renders it. However, when we included our scurl tag (remember, this tag depends on the HttpRequest) inside the html-to-include.html template, everything blew up. Looking at the Django source, I was surprised to see that when processing the inclusion_tag, a new context is created and the parent context is not used. At first, this seemed crazy, but in thinking about it more, it’s the right thing to do: we don’t necessarily want our parent context colliding with expectations of the inclusion_tag. That is, because of this separate context, there is a way to formally adapt our parent context to the context of the inclusion_tag.

So now, the challenge is to adapt the context. The inclusion_tag decorator provides a handy flag takes_context that tells Django to provide the context to the template tag. To do this, we need to alter our signature slightly; the context must be the first parameter:


  @register.inclusion_tag("html-to-include.html", takes_context=True)
  def my_include_tag(context, myparam):
    return {"inclusion_param":myparam}

The parameter context is now passed the parent RequestContext.

Now, misunderstanding #2 came along: we thought that this context would automatically be propagated when rendering the template. It was not; a new Context instance was still being created. Diving into the source, inclusion_tag has an undocumented keyword parameter context_class that allows you to specify what context that Django instantiates. So this led to this trial:


  @register.inclusion_tag("html-to-include.html", takes_context=True, context_class=RequestContext)
  def my_include_tag(context, myparam):
    return {"inclusion_param":myparam}

This failed because the __init__ signature for RequestContext looks nothing like the Context __init__ signature. And in hindsight, it’s a naive trial because, unless there’s some serious magic under the hood, how would the actual request get passed through to the RequestContext that was being instantiated?

But wait! The intent of the inclusion_tag is to provide an adapter between contexts. AND python is dynamically typed. The latter is relevant because my initial tag scurl doesn’t actually care that it has a RequestContext, only that context['request'] returns what it needs.

So, all we had to do was implement the adapter (dumping the erroneous context_class bit):


  @register.inclusion_tag("html-to-include.html", takes_context=True)
  def my_include_tag(context, myparam):
    return {"inclusion_param":myparam, "request":context['request']}

Pretty cool, but rife with a pretty deep understanding of Django internals that is vital for any templatetag author. Would have never sorted this out without access to the Django source…

About these ads


10 Responses to “Django templatetag, RequestContext, and inclusion_tag”

  1. 1 dave

    Nice writeup, exactly what i needed :) thanks!!

  2. 2 Oleg

    Hi!

    I want to say hello from every page of my site to the current user.
    So I registered the tag:

    def console(context):
    return {“request”: context['request']}

    register.inclusion_tag(‘console.html’, takes_context=True)(console)

    But, when I tried to include it I am getting the error
    TemplateSyntaxError at /account/profile/test

    Caught an exception while rendering: ‘request’

    Original Traceback (most recent call last):
    File “/opt/local/lib/python2.5/site-packages/django/template/debug.py”, line 71, in render_node
    result = node.render(context)
    File “/opt/local/lib/python2.5/site-packages/django/template/__init__.py”, line 915, in render
    dict = func(*args)
    File “/Users/oleg/jin/profile_menu/trunk/jin/../jin/articleManager/templatetags/article_tags.py”, line 21, in console
    return {“request”: context['request']}
    File “/opt/local/lib/python2.5/site-packages/django/template/context.py”, line 43, in __getitem__
    raise KeyError(key)
    KeyError: ‘request’

    Please help!

  3. 3 Jason Collins

    Oleg,

    You have to make sure that the view itself passes the RequestContext. See the very first part of the article:

    def my_view(request):
    return render_to_response(“myview.html”, context_instance=RequestContext(request))

  4. Worth to mention, django.core.context_processors.request should be in your TEMPLATE_CONTEXT_PROCESSORS in settings.py.

  5. @Andreas Karlsson

    Thanks for the tip! It *is* absolutely worth to mention that
    django.core.context_processors.request
    is in your TEMPLATE_CONTEXT_PROCESSORS in settings.py!

    +1 on that.

  6. 6 John

    … or you can pass the template variable to your templatetag via a parameter?

    {% yourtag request.user %}

    … or, and maybe this is new since the original post, you can access the context in the “render” routine of the template.Node:

    class yourNode(template.Node):
    def render(self, context):
    user = context.request.user

    http://docs.djangoproject.com/en/1.2/howto/custom-template-tags/#passing-template-variables-to-the-tag

  7. 7 davce

    This didn’t actually work me, as you weren’t passing a RequestContext object, just the request instance. I need the RequestContext to pull in vars such as MEDIA_URL, but this wasn’t being set.

    So, my take for anyone to shoot down is this:–

    @register.inclusion_tag(“html-to-include.html”, takes_context=True)
    def my_include_tag(context, myparam):
    return template.RequestContext(context['request'], {
    “inclusion_param”:myparam,
    })

    I now have my MEDIAL_URL back in the included template

    • 8 Dal

      @davce – Thanks! Sorted out my issue too, wasn’t seeing MEDIA_URL in the included template, until I made the change you suggested.

  8. If you want to pass all context variables to the inclusion tag, you can do this:
    context.update({
    “inclusion_param”: myparam,
    })
    return context

    One drawback of this approach is that the context is polluted with the new variable “inclusion_param” after inclusion tag in the global template.

  9. Just to complete the discussion, you could just shallow-copy the context:

    from copy import copy
    inclusionContext = copy(context)
    inclusionContext.update({
    “inclusion_param”: myparam,
    })
    return context

    That way, you don’t pollute the parent template’s context (as long as you don’t change any mutable objects).


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: