Login

Template Tag Caveat

Author:
ericmoritz
Posted:
June 17, 2008
Language:
Python
Version:
.96
Score:
2 (after 2 ratings)

This describes an issue with the template system that people may come across.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""
In the Django template system.  There is a small caveat that you need to 
recognize when developing your own template tags.

When Django parses the Node tree it creates a template.Node instance for 
each template tag in the template.  The node tree is just like our beloved 
HTML DOM.  

So for instance take the following::

    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>    
    </ul>

That turns into something like this::

    <ul:Element>
      <li:HTMLElement>
      <li:HTMLElement>
      <li:HTMLElement>
      <li:HTMLElement>
      <li:HTMLElement>

The indention shows that the li's are child nodes of the ul tag. Each li in a 
different instance of an HTMLElement, each with their own state.

So for a Django Template take the following::

    {% block content %}
      {% firstof var1 var2 var3 %}
      {% firstof var1 var2 var3 %}
      {% firstof var1 var2 var3 %}
      {% firstof var1 var2 var3 %}  
    {% endblock %}

The node tree would look like this::

    <BlockNode>
      <FirstOfNode>
      <FirstOfNode>
      <FirstOfNode>
      <FirstOfNode>  

So we have a block node with four FirstOfNode instances

In python this would translate roughly to::

    f1 = FirstOfNode(...)
    f2 = FirstOfNode(...)
    f3 = FirstOfNode(...)
    f4 = FirstOfNode(...)
    
    f1.render()
    f2.render()
    f3.render()
    f4.render()

Everything looks fine here.  Render is called once per node instance.

Now, check this out::

    {% block content %} 
      {% for i in value_list %} 
        {% repr obj %} 
      {% endfor %} 
    {% endblock %}

The node tree that django builds would look something like this::

    <BlockNode> 
      <ForNode> 
        <ReprNode>

Now each node in that tree is it's own object instance with it's own
state.  This can be an issue if you don't realize one thing, each node
is persistent while the template is rendering.

Let me write out how the template rendering of the a loop would look
in python with the ReprNode::

    repr_node = ReprNode("obj")
    
    for i in range(10): 
        repr_node.render(context)

Can you tell what the problem?  We're calling the render method
on the same instance of the Node.

Let's peek under the covers and look at ReprNode's definition::
  
    class ReprNode(template.Node): 
        def __init__(self, obj) 
            self.obj = obj
    
        def render(self, context): 
            self.obj = template.resolve_variable(self.obj, context)
            return str(self.obj)
    
        def do_repr(parser, token): 
            tag_name, obj = token.split_contents()
            return ReprNode(obj)


Now look at what's going on.  self.obj is assumed to be the name of the 
variable in the context.  That's fine when render is called once.  When render
called a second time, self.obj is now the actual value of obj from the context.
What happens when you try to resolve that?  You get a VariableDoesNotExist 
error, uh oh!

I made the assumption that render() is only called once.  You don't realize 
that inside a for tag, the render method is called repeatedly.  This can cause
tons of issues.  If it wasn't a huge design change, Template Nodes should 
probably be Static classes.

I don't know if this is technically a bug in the Django templating system, 
bad design, bad documentation, or just simply poor assumptions on my part, but
I came across this issue today and it took stepping through PDB to find the 
problem with my custom template tag (the ReprNode was completely made up for 
the example)

Example code <http://www.djangosnippets.org/snippets/811/>
"""
from django import template
register = template.Library()

class ReprNode(template.Node):
    def __init__(self, obj):
        self.obj = obj

    def render(self, context):
        self.obj = template.resolve_variable(self.obj, context)
        return str(self.obj)

@register.tag
def repr(parser, token):
    # Get the tag_name and the variable name of the start variable
    tag_name, start_exp = token.split_contents()

    return ReprNode(start_exp)


# This allows me to do everything in one file
template.add_to_builtins(__name__)


def test_reprtag():
    t = template.Template("""
  {% for i in counter %}
  mystr: {% repr mystr %}
  {% endfor %}
""")

    print t.render(template.Context({"mystr": "foo",
                                     "counter": range(10)}))

if __name__ == '__main__':
    test_reprtag()

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Gulopine (on June 17, 2008):

My vote would be for incomplete documentation. Using nodes like that seems optimal to me, compiling the tag once, then rendering it multiple times. But you're right, it's a bit counter-intuitive if you're used to common object-oriented behavior, and I can definitely see how it could trip you up.

#

Please login first before commenting.