views:

526

answers:

3

I want to be able to set variables in a template to string values. I wrote a tag, but it doesn't seem to change the context. The intended use is:

{% define "a string" as my_var %}

Update (solved):

class DefineNode(Node):
    def __init__(self, var, name):
        self.var = var
        self.name = name

    def __repr__(self):
        return "<DefineNode>"

    def render(self, context):
        context[self.name] = self.var
        return ''

@register.tag
def define(parser, token):
    """
    Adds a name to the context for referencing an arbitrarily defined string.

    For example:

        {% define "my_string" as my_string %}

    Now anywhere in the template:

        {{ my_string }}
    """
    bits = list(token.split_contents())
    if (len(bits) != 4 or bits[2] != "as") or \
        not (bits[1][0] in ('"', "'") and bits[1][-1] == bits[1][0]):
        raise TemplateSyntaxError("%r expected format is '\"string\" as name'" % bits[0])
    else:
        value = bits[1][1:-1]
    name = bits[3]
    return DefineNode(value, name)
+2  A: 

You don't need to write your own tag. The built-in {% with %} tag does this.

Daniel Roseman
Yes you are right. One thing that this tag does differently is that it does not use and end tag. The name is put into the context for the rest of the template.
hekevintran
I'm surprised this has a tick since it doesn't address the problem: I don't see 'with' creating a new context variable; it only renames an existing one for a moment.
John Mee
A: 

First of all, you generally want to set context variables within your view. Putting logic in the template is really a formula for added mess. That said, there does come a time when you want to use this, and the {% with %} tag makes a mess of things since you HAVE to end it with a {% endwith %}, losing the variable. The problem I ran into is that I can't include a template while passing it a value. I'd like to do:

{% if criteria %}
  {% define 'foo' as some_option %}
{% else %}
  {% define 'bar' as some_option %}
{% endif %}

{% include "some_template_partial.html" %}

This is impossible to do using {% with %} tags without having repeated code:

{% if criteria %}
  {% with 'foo' as some_option %}
    {% include "some_template_partial.html" %}
  {% endwith %}
{% else %}
  {% with 'bar' as some_option %}
    {% include "some_template_partial.html" %}
  {% endwith %}
{% endif %}

Fine as it is now, but this will degrade into a horrible mess as cases proliferate. Thus this code was written:

from django import template
from django.conf import settings
import logging
import re
register = template.Library()

NAMESPACE_PROTECTION = settings.DEBUG

class define_node(template.Node):
  def __init__(self, value, key, parse):
    self.value = value
    self.key = key
    self.parse = parse
  def render(self, context):
    if NAMESPACE_PROTECTION:
      if self.key in context:
        raise Exception("EPIC NAMESPACE FAIL, CONTEXT HAZ A %s" % self.key)
    if self.parse:
      context[self.key] = context[self.value]
    else:
      context[self.key] = self.value
    return ''

@register.tag
def define(parser, token):
  """Definition template tag. Use to define variables in your context within the template.
  Sorta like the {% with "blah" as blah %} tag, but without the {% endwith %} mess.

  Supports two modes:
  Literal mode: argument is encapsulated with quotes (e.g. "blah" or 'blah')
                variable, is set to the string literal, ex:
                {% define "fish" as foo %}
  Variable mode: argument is prefixed with a $ (e.g. $blah or $monkey)
                 variable is copied from another context variable, ex:
                 {% define $fish as foo %}

  Namespace protection is also provided if django.conf.settings.DEBUG is True.
  You will get an epic namespace fail if that occurs (please fix it before you deploy)

  TODO:
    * define override nomenclature if you REALLY want to overwrite a variable
      - should decide what nomeclature to use first
    * expand on variables so that {% define $array.blah as foo %} will work
      (this currently WILL NOT)
  """
  try:
    tag_name, arg = token.contents.split(None, 1)
  except ValueError:
    raise template.TemplateSyntaxError, "%r tag requires arguments" % token.contents.split()[0]
  m = re.search(r'(.*?) as (\w+)', arg)
  if not m:
    raise template.TemplateSyntaxError, "%r tag had invalid arguments" % tag_name
  value, key = m.groups()
  if (value[0] == value[-1] and value[0] in ('"', "'")):
    ret = value[1:-1]
    parse = False
  elif (value[0] == '$'):
    ret = value[1:]
    parse = True
  else:
    raise template.TemplateSyntaxError, "%r tag's first argument indeciperable" % tag_name
  return define_node(ret, key, parse)
shu.chen
A: 

The answer is buried inside the more complex current_time example in the documentation.

Problem

You want to add a variable to the context. But you don't want to go back and add that variable to all the views which call all the templates which invoke the tag. You just want a tag which can add some data to the context wherever its wanted. I'm looking for this kind of thing when rendering those random distractions which get dropped into sidebars and aren't specifically related to the work of the main view, for example.

Method

To inject a variable to the context you need access to the context. To do that your custom tag will inject a node which added the data to the template context.

Example

This example adds a "coming_events" queryset to the context then loops over each result. It does that by declaring a custom tag which renders a node which adds a queryset to the context.

from django import template
from apps.events.models import Event
register = template.Library()

@register.tag
def coming_events(parser, token):
    return EventNode()

class EventNode(template.Node):
    def render(self, context):
        context['coming_events'] = Event.objects.all()
        return ''

You'd use it like this:

{% load events %}
{% coming_events %}
{% for event in coming_events %}
<div class="eventItem">
   <p>{{event.title}} {{event.data}}</p>
</div>
{% endfor %}

Extra Credit

If you're really keen to be able to name the variable arbitrarily eg {% coming_events as events %} then look closely at the example in the documentation and note how they split the token into what's before the ' as ' and what's after and use the latter part to name the context variable. You'd have to implement that.

Note that if I wound up putting the HTML for each event into its own dedicated template then I'd be better off just following the standard inclusion tag example in the documentation. This solution is suggested for when you want the data without any baggage.

John Mee