Code

Ticket #6262: loader_tags.diff

File loader_tags.diff, 9.1 KB (added by cainmatt@…, 5 years ago)

Re-written BlockNode and ExtendsNode to not update nodelist in render()

Line 
1diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py
2index 38b2fff..d688983 100644
3--- a/django/template/loader_tags.py
4+++ b/django/template/loader_tags.py
5@@ -1,6 +1,6 @@
6 from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable
7 from django.template import Library, Node, TextNode
8-from django.template.loader import get_template, get_template_from_string, find_template_source
9+from django.template.loader import get_template
10 from django.conf import settings
11 from django.utils.safestring import mark_safe
12 
13@@ -10,91 +10,144 @@ class ExtendsError(Exception):
14     pass
15 
16 class BlockNode(Node):
17-    def __init__(self, name, nodelist, parent=None):
18-        self.name, self.nodelist, self.parent = name, nodelist, parent
19+    def __init__(self, name, nodelist):
20+        self.name, self.nodelist = name, nodelist
21 
22     def __repr__(self):
23         return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
24 
25-    def render(self, context):
26-        context.push()
27-        # Save context in case of block.super().
28-        self.context = context
29-        context['block'] = self
30-        result = self.nodelist.render(context)
31-        context.pop()
32+    def render(self, context, force_self=False):
33+        """
34+        Find the correct BlockNode and render its nodelist.
35+        case 1: no extends tags encountered => render self
36+        case 2: in block.super => render self
37+        case 3: extends tags but not in block.super => render "youngest" version of this block
38+        """
39+        block_context = context.get(BlockContext.KEY, None)
40+        if block_context is None:
41+            # no extends tag encountered; block tags have no effect
42+            return self.nodelist.render(context)
43+        elif force_self:
44+            block = self
45+        else:
46+            block = block_context[self.name]
47+
48+        block_context.enter(block)
49+        result = block.nodelist.render(context)
50+        block_context.exit(block)
51         return result
52 
53-    def super(self):
54-        if self.parent:
55-            return mark_safe(self.parent.render(self.context))
56-        return ''
57+class BlockContext(object):
58+    """
59+    BlockContext contains the block definitions for a chain of ExtendsNodes.
60+    The BlockContext is dynamically generated during rendering because the
61+    extends tag supports setting the parent template dynamically.
62+    Unlike template.Context, BlockContext.get(name) returns the "first"
63+    version added since that is defined in the youngest ancestor.
64+    """
65+    KEY = 'block' # so that {{ block.super }} works
66+
67+    def __init__(self, context):
68+        self.context = context
69+        self.dicts = []
70+        self.block_stack = []
71+
72+    def __repr__(self):
73+        return 'BlockContext(%r, %r)' % (self.block_stack, self.dicts)
74+
75+    def __getitem__(self, key):
76+        "Get a block by name"
77+        for d in self.dicts:
78+            if key in d:
79+                return d[key]
80+        raise KeyError(key)
81+
82+    def add_blocks(self, blocks_dict):
83+        "Like Context.update() but puts blocks_dict last."
84+        if not hasattr(blocks_dict, '__getitem__'):
85+            raise TypeError('blocks_dict must be a mapping (dictionary-like) object.')
86+        self.dicts.append(blocks_dict)
87+
88+    def enter(self, block):
89+        # TODO: check for loops
90+        self.block_stack.append(block)
91+
92+    def exit(self, block):
93+        last = self.block_stack.pop()
94+        if last is not block:
95+            raise ValueError('Invalid block stack: %r' % (self.block_stack + [last]))
96+
97+    def get_parent(self, block):
98+        "Find the parent version of a given block, if any"
99+        name = block.name
100+        found = False
101+        for d in self.dicts:
102+            b = d.get(name, None)
103+            if b:
104+                if found:
105+                    return b
106+                elif b is block:
107+                    found = True
108+        return None
109 
110-    def add_parent(self, nodelist):
111-        if self.parent:
112-            self.parent.add_parent(nodelist)
113+    def super(self):
114+        "Implement block.super"
115+        parent = self.get_parent(self.block_stack[-1])
116+        if parent:
117+            return mark_safe(parent.render(self.context, force_self=True))
118         else:
119-            self.parent = BlockNode(self.name, nodelist)
120+            return u''
121 
122 class ExtendsNode(Node):
123     must_be_first = True
124 
125     def __init__(self, nodelist, parent_name, parent_name_expr, template_dirs=None):
126         self.nodelist = nodelist
127-        self.parent_name, self.parent_name_expr = parent_name, parent_name_expr
128-        self.template_dirs = template_dirs
129+        self.block_nodes = dict([(n.name, n) for n in nodelist.get_nodes_by_type(BlockNode)])
130+        if parent_name:
131+            try:
132+                self.parent = get_template(parent_name)
133+            except TemplateDoesNotExist:
134+                error_msg = "Invalid template name in 'extends' tag: %r." % parent_name
135+                raise TemplateSyntaxError, error_msg
136+        self.parent_name_expr = parent_name_expr
137 
138     def __repr__(self):
139         if self.parent_name_expr:
140             return "<ExtendsNode: extends %s>" % self.parent_name_expr.token
141-        return '<ExtendsNode: extends "%s">' % self.parent_name
142+        return '<ExtendsNode: extends "%s">' % self.parent.name
143 
144     def get_parent(self, context):
145-        if self.parent_name_expr:
146-            self.parent_name = self.parent_name_expr.resolve(context)
147-        parent = self.parent_name
148-        if not parent:
149-            error_msg = "Invalid template name in 'extends' tag: %r." % parent
150-            if self.parent_name_expr:
151-                error_msg += " Got this from the '%s' variable." % self.parent_name_expr.token
152-            raise TemplateSyntaxError, error_msg
153+        if hasattr(self, 'parent'):
154+            return self.parent
155+        parent = self.parent_name_expr.resolve(context)
156         if hasattr(parent, 'render'):
157             return parent # parent is a Template object
158         try:
159-            source, origin = find_template_source(parent, self.template_dirs)
160+            return get_template(parent)
161         except TemplateDoesNotExist:
162-            raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
163-        else:
164-            return get_template_from_string(source, origin, parent)
165+            error_msg = "Invalid template name in 'extends' tag: %r." % parent
166+            error_msg += " Got this from the '%s' variable." % self.parent_name_expr.token
167+            raise TemplateSyntaxError, error_msg
168 
169     def render(self, context):
170-        compiled_parent = self.get_parent(context)
171-        parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
172-        for block_node in self.nodelist.get_nodes_by_type(BlockNode):
173-            # Check for a BlockNode with this node's name, and replace it if found.
174-            try:
175-                parent_block = parent_blocks[block_node.name]
176-            except KeyError:
177-                # This BlockNode wasn't found in the parent template, but the
178-                # parent block might be defined in the parent's *parent*, so we
179-                # add this BlockNode to the parent's ExtendsNode nodelist, so
180-                # it'll be checked when the parent node's render() is called.
181-
182-                # Find out if the parent template has a parent itself
183-                for node in compiled_parent.nodelist:
184-                    if not isinstance(node, TextNode):
185-                        # If the first non-text node is an extends, handle it.
186-                        if isinstance(node, ExtendsNode):
187-                            node.nodelist.append(block_node)
188-                        # Extends must be the first non-text node, so once you find
189-                        # the first non-text node you can stop looking.
190-                        break
191-            else:
192-                # Keep any existing parents and add a new one. Used by BlockNode.
193-                parent_block.parent = block_node.parent
194-                parent_block.add_parent(parent_block.nodelist)
195-                parent_block.nodelist = block_node.nodelist
196-        return compiled_parent.render(context)
197+        block_context = context.get(BlockContext.KEY, None)
198+        if block_context is None:
199+            block_context = BlockContext(context)
200+            context[BlockContext.KEY] = block_context
201+        block_context.add_blocks(self.block_nodes)
202+
203+        parent = self.get_parent(context)
204+
205+        # if parent is the base template, add all its blocks here
206+        # we only need to check the first two nodes of the parent to find ExtendsNode
207+        #if not parent.nodelist.get_nodes_by_type(ExtendsNode):
208+        if not filter(lambda n : isinstance(n, ExtendsNode), parent.nodelist[:2]):
209+            if not hasattr(parent, '_block_nodes'):
210+                parent._block_nodes = dict([(n.name, n) for n in parent.nodelist.get_nodes_by_type(BlockNode)])
211+            block_context.add_blocks(parent._block_nodes)
212+
213+        return parent.render(context)
214 
215 class ConstantIncludeNode(Node):
216     def __init__(self, template_path):