Code

Ticket #13910: django-1.2.1.diff

File django-1.2.1.diff, 20.3 KB (added by rooney, 4 years ago)

diff with django-1.2.1 source code

Line 
1Index: django/shortcuts/__init__.py
2===================================================================
3--- django/shortcuts/__init__.py        (revision 1)
4+++ django/shortcuts/__init__.py        (working copy)
5@@ -11,14 +11,28 @@
6 from django.db.models.query import QuerySet
7 from django.core import urlresolvers
8 
9+def http_response_using(func, *args, **kwargs):
10+    """
11+    Returns a HttpResponse whose content is filled with the result of calling
12+    func() with the passed arguments.
13+    """
14+    httpresponse_kwargs = {'mimetype': kwargs.pop('mimetype', None)}
15+    return HttpResponse(func(*args, **kwargs), **httpresponse_kwargs)
16+
17 def render_to_response(*args, **kwargs):
18     """
19     Returns a HttpResponse whose content is filled with the result of calling
20     django.template.loader.render_to_string() with the passed arguments.
21     """
22-    httpresponse_kwargs = {'mimetype': kwargs.pop('mimetype', None)}
23-    return HttpResponse(loader.render_to_string(*args, **kwargs), **httpresponse_kwargs)
24+    return http_response_using(loader.render_to_string, *args, **kwargs)
25 
26+def stream_to_response(*args, **kwargs):
27+    """
28+    Returns a HttpResponse whose content is filled with the result of calling
29+    django.template.loader.get_render_stream() with the passed arguments.
30+    """
31+    return http_response_using(loader.stream, *args, **kwargs)
32+
33 def redirect(to, *args, **kwargs):
34     """
35     Returns an HttpResponseRedirect to the apropriate URL for the arguments
36Index: django/template/__init__.py
37===================================================================
38--- django/template/__init__.py (revision 1)
39+++ django/template/__init__.py (working copy)
40@@ -166,11 +166,17 @@
41     def _render(self, context):
42         return self.nodelist.render(context)
43 
44+    def _stream(self, context):
45+        return self.nodelist.stream(context)
46+
47     def render(self, context):
48         "Display stage -- can be called many times"
49+        return ''.join(self.stream(context))
50+
51+    def stream(self, context):
52         context.render_context.push()
53         try:
54-            return self._render(context)
55+            return self._stream(context)
56         finally:
57             context.render_context.pop()
58 
59@@ -770,6 +776,10 @@
60         "Return the node rendered as a string"
61         pass
62 
63+    def stream(self, context):
64+        "Returns a generator that progressively renders the node"
65+        yield self.render(context)
66+
67     def __iter__(self):
68         yield self
69 
70@@ -790,13 +800,15 @@
71     contains_nontext = False
72 
73     def render(self, context):
74-        bits = []
75+        return ''.join(self.stream(context))
76+       
77+    def stream(self, context):
78         for node in self:
79             if isinstance(node, Node):
80-                bits.append(self.render_node(node, context))
81+                for bit in self.stream_node(node, context):
82+                    yield mark_safe(force_unicode(bit))
83             else:
84-                bits.append(node)
85-        return mark_safe(''.join([force_unicode(b) for b in bits]))
86+                yield mark_safe(force_unicode(node))
87 
88     def get_nodes_by_type(self, nodetype):
89         "Return a list of all nodes of the given type"
90@@ -808,6 +820,9 @@
91     def render_node(self, node, context):
92         return node.render(context)
93 
94+    def stream_node(self, node, context):
95+        return node.stream(context)
96+
97 class TextNode(Node):
98     def __init__(self, s):
99         self.s = s
100Index: django/template/defaulttags.py
101===================================================================
102--- django/template/defaulttags.py      (revision 1)
103+++ django/template/defaulttags.py      (working copy)
104@@ -20,17 +20,21 @@
105     """Implements the actions of the autoescape tag."""
106     def __init__(self, setting, nodelist):
107         self.setting, self.nodelist = setting, nodelist
108-
109-    def render(self, context):
110+       
111+    def stream(self, context):
112         old_setting = context.autoescape
113         context.autoescape = self.setting
114-        output = self.nodelist.render(context)
115-        context.autoescape = old_setting
116         if self.setting:
117-            return mark_safe(output)
118+            for chunk in self.nodelist.stream(context):
119+                yield mark_safe(chunk)
120         else:
121-            return output
122+            for chunk in self.nodelist.stream(context):
123+                yield chunk
124+        context.autoescape = old_setting
125 
126+    def render(self, context):
127+        ''.join(self.stream(context))
128+
129 class CommentNode(Node):
130     def render(self, context):
131         return ''
132@@ -79,12 +83,15 @@
133         self.filter_expr, self.nodelist = filter_expr, nodelist
134 
135     def render(self, context):
136-        output = self.nodelist.render(context)
137-        # Apply filters.
138-        context.update({'var': output})
139-        filtered = self.filter_expr.resolve(context)
140+        return ''.join(self.stream(context))
141+
142+    def stream(self, context):
143+        for output in self.nodelist.stream(context):
144+            # Apply filters.
145+            context.update({'var': output})
146+            filtered = self.filter_expr.resolve(context)
147+            yield filtered
148         context.pop()
149-        return filtered
150 
151 class FirstOfNode(Node):
152     def __init__(self, vars):
153@@ -122,6 +129,9 @@
154             yield node
155 
156     def render(self, context):
157+        return ''.join(self.stream(context))
158+
159+    def stream(self, context):
160         if 'forloop' in context:
161             parentloop = context['forloop']
162         else:
163@@ -133,19 +143,35 @@
164             values = []
165         if values is None:
166             values = []
167-        if not hasattr(values, '__len__'):
168-            values = list(values)
169-        len_values = len(values)
170-        if len_values < 1:
171-            context.pop()
172-            return self.nodelist_empty.render(context)
173-        nodelist = NodeList()
174-        if self.is_reversed:
175-            values = reversed(values)
176+        if 'generator' == values.__class__.__name__:
177+            if self.is_reversed:
178+                values = list(values)
179+                len_values = len(values)
180+                values = reversed(values)
181+            else:
182+                len_values = 0 # generators have unknown len, but if the len is needed, convert the
183+                               # generator to list
184+                for node in self.nodelist_loop:
185+                    try:
186+                        token = node.filter_expression.token
187+                    except AttributeError:
188+                        continue
189+                    else:
190+                        if token.startswith('forloop.revcounter') or token.startswith('forloop.last'):
191+                            values = list(values)
192+                            len_values = len(values)
193+                            break
194+        else: # not generator
195+            if not hasattr(values, '__len__'):
196+                values = list(values)
197+            len_values = len(values)
198+            if self.is_reversed:
199+                values = reversed(values)
200         unpack = len(self.loopvars) > 1
201         # Create a forloop value in the context.  We'll update counters on each
202         # iteration just below.
203         loop_dict = context['forloop'] = {'parentloop': parentloop}
204+        i = None
205         for i, item in enumerate(values):
206             # Shortcuts for current loop iteration number.
207             loop_dict['counter0'] = i
208@@ -163,8 +189,8 @@
209                 context.update(dict(zip(self.loopvars, item)))
210             else:
211                 context[self.loopvars[0]] = item
212-            for node in self.nodelist_loop:
213-                nodelist.append(node.render(context))
214+            for chunk in self.nodelist_loop.stream(context):
215+                yield chunk
216             if unpack:
217                 # The loop variables were pushed on to the context so pop them
218                 # off again. This is necessary because the tag lets the length
219@@ -173,7 +199,9 @@
220                 # context.
221                 context.pop()
222         context.pop()
223-        return nodelist.render(context)
224+        if i is None: # values was empty
225+            for chunk in self.nodelist_empty.stream(context):
226+                yield chunk
227 
228 class IfChangedNode(Node):
229     child_nodelists = ('nodelist_true', 'nodelist_false')
230@@ -219,11 +247,14 @@
231         return "<IfEqualNode>"
232 
233     def render(self, context):
234+        return ''.join(self.stream(context))
235+       
236+    def stream(self, context):
237         val1 = self.var1.resolve(context, True)
238         val2 = self.var2.resolve(context, True)
239         if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
240-            return self.nodelist_true.render(context)
241-        return self.nodelist_false.render(context)
242+            return self.nodelist_true.stream(context)
243+        return self.nodelist_false.stream(context)
244 
245 class IfNode(Node):
246     child_nodelists = ('nodelist_true', 'nodelist_false')
247@@ -242,15 +273,18 @@
248             yield node
249 
250     def render(self, context):
251+        return ''.join(self.stream(context))
252+       
253+    def stream(self, context):
254         try:
255             var = self.var.eval(context)
256         except VariableDoesNotExist:
257             var = None
258 
259         if var:
260-            return self.nodelist_true.render(context)
261+            return self.nodelist_true.stream(context)
262         else:
263-            return self.nodelist_false.render(context)
264+            return self.nodelist_false.stream(context)
265 
266 class RegroupNode(Node):
267     def __init__(self, target, expression, var_name):
268@@ -281,13 +315,18 @@
269 class SsiNode(Node):
270     def __init__(self, filepath, parsed):
271         self.filepath, self.parsed = filepath, parsed
272+       
273+     
274+    def render(self, context):
275+        return ''.join(self.stream(context))
276 
277-    def render(self, context):
278+    def stream(self, context):
279         if not include_is_allowed(self.filepath):
280             if settings.DEBUG:
281-                return "[Didn't have permission to include file]"
282+                yield "[Didn't have permission to include file]"
283             else:
284-                return '' # Fail silently for invalid includes.
285+                yield '' # Fail silently for invalid includes.
286+            return
287         try:
288             fp = open(self.filepath, 'r')
289             output = fp.read()
290@@ -297,13 +336,15 @@
291         if self.parsed:
292             try:
293                 t = Template(output, name=self.filepath)
294-                return t.render(context)
295+                for chunk in t.stream(context):
296+                    yield chunk
297             except TemplateSyntaxError, e:
298                 if settings.DEBUG:
299-                    return "[Included template had syntax error: %s]" % e
300+                    yield "[Included template had syntax error: %s]" % e
301                 else:
302-                    return '' # Fail silently for invalid included templates.
303-        return output
304+                    yield '' # Fail silently for invalid included templates.
305+        else:
306+            yield output
307 
308 class LoadNode(Node):
309     def render(self, context):
310@@ -419,12 +460,15 @@
311         return "<WithNode>"
312 
313     def render(self, context):
314+        return ''.join(self.stream(context))
315+   
316+    def stream(self, context):
317         val = self.var.resolve(context)
318         context.push()
319         context[self.name] = val
320-        output = self.nodelist.render(context)
321+        for output in self.nodelist.stream(context):
322+            yield output
323         context.pop()
324-        return output
325 
326 #@register.tag
327 def autoescape(parser, token):
328Index: django/template/loader_tags.py
329===================================================================
330--- django/template/loader_tags.py      (revision 1)
331+++ django/template/loader_tags.py      (working copy)
332@@ -46,11 +46,15 @@
333         return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
334 
335     def render(self, context):
336+        return ''.join(self.stream(context))
337+
338+    def stream(self, context):
339         block_context = context.render_context.get(BLOCK_CONTEXT_KEY)
340         context.push()
341         if block_context is None:
342             context['block'] = self
343-            result = self.nodelist.render(context)
344+            for chunk in self.nodelist.stream(context):
345+                yield chunk
346         else:
347             push = block = block_context.pop(self.name)
348             if block is None:
349@@ -59,11 +63,11 @@
350             block = BlockNode(block.name, block.nodelist)
351             block.context = context
352             context['block'] = block
353-            result = block.nodelist.render(context)
354+            for chunk in block.nodelist.stream(context):
355+                yield chunk
356             if push is not None:
357                 block_context.push(self.name, push)
358         context.pop()
359-        return result
360 
361     def super(self):
362         render_context = self.context.render_context
363@@ -100,6 +104,9 @@
364         return get_template(parent)
365 
366     def render(self, context):
367+        return ''.join(self.stream(context))
368+
369+    def stream(self, context):
370         compiled_parent = self.get_parent(context)
371 
372         if BLOCK_CONTEXT_KEY not in context.render_context:
373@@ -120,9 +127,9 @@
374                     block_context.add_blocks(blocks)
375                 break
376 
377-        # Call Template._render explicitly so the parser context stays
378+        # Call Template._stream explicitly so the parser context stays
379         # the same.
380-        return compiled_parent._render(context)
381+        return compiled_parent._stream(context)
382 
383 class ConstantIncludeNode(Node):
384     def __init__(self, template_path):
385@@ -133,28 +140,34 @@
386             if settings.TEMPLATE_DEBUG:
387                 raise
388             self.template = None
389+           
390+    def render(self, context):
391+        ''.join(self.stream(context))
392 
393-    def render(self, context):
394+    def stream(self, context):
395         if self.template:
396-            return self.template.render(context)
397+            return self.template.stream(context)
398         else:
399-            return ''
400+            return empty_string_generator()
401 
402 class IncludeNode(Node):
403     def __init__(self, template_name):
404         self.template_name = Variable(template_name)
405 
406     def render(self, context):
407+        ''.join(self.stream(context))
408+
409+    def stream(self, context):
410         try:
411             template_name = self.template_name.resolve(context)
412             t = get_template(template_name)
413-            return t.render(context)
414+            return t.stream(context)
415         except TemplateSyntaxError, e:
416             if settings.TEMPLATE_DEBUG:
417                 raise
418-            return ''
419+            return empty_string_generator()
420         except:
421-            return '' # Fail silently for invalid included templates.
422+            return empty_string_generator() # Fail silently for invalid included templates.
423 
424 def do_block(parser, token):
425     """
426@@ -214,6 +227,9 @@
427     if path[0] in ('"', "'") and path[-1] == path[0]:
428         return ConstantIncludeNode(path[1:-1])
429     return IncludeNode(bits[1])
430+   
431+def empty_string_generator():
432+    yield ''
433 
434 register.tag('block', do_block)
435 register.tag('extends', do_extends)
436Index: django/template/loader.py
437===================================================================
438--- django/template/loader.py   (revision 1)
439+++ django/template/loader.py   (working copy)
440@@ -174,6 +174,15 @@
441     get_template, or it may be a tuple to use select_template to find one of
442     the templates in the list. Returns a string.
443     """
444+    return ''.join(stream(template_name, dictionary, context_instance))
445+
446+def stream(template_name, dictionary=None, context_instance=None):
447+    """
448+    Loads the given template_name and streams it with the given dictionary as
449+    context. The template_name may be a string to load a single template using
450+    get_template, or it may be a tuple to use select_template to find one of
451+    the templates in the list. Returns a string generator.
452+    """
453     dictionary = dictionary or {}
454     if isinstance(template_name, (list, tuple)):
455         t = select_template(template_name)
456@@ -183,7 +192,7 @@
457         context_instance.update(dictionary)
458     else:
459         context_instance = Context(dictionary)
460-    return t.render(context_instance)
461+    return t.stream(context_instance)
462 
463 def select_template(template_name_list):
464     "Given a list of template names, returns the first that can be loaded."
465Index: docs/howto/custom-template-tags.txt
466===================================================================
467--- docs/howto/custom-template-tags.txt (revision 1)
468+++ docs/howto/custom-template-tags.txt (working copy)
469@@ -932,3 +932,25 @@
470 For more examples of complex rendering, see the source code for ``{% if %}``,
471 ``{% for %}``, ``{% ifequal %}`` and ``{% ifchanged %}``. They live in
472 ``django/template/defaulttags.py``.
473+
474+Add streaming support
475+~~~~~~~~~~~~~~~~~~~~~
476+
477+Simple tag like the earlier example of ``CurrentTimeNode`` does not need to
478+worry about streaming support. But if your custom tag is a more complex one
479+that have a "begin" tag and "end" tag like the above ``{% upper %}`` ..
480+``{% endupper %}``, and you want the tag to be able to progressively render
481+its contents, you can define your own ``stream()`` method for the custom tag.
482+The ``stream()`` method takes the same arguments as ``render()`` but instead
483+of returning string, it needs to return string generator.
484+
485+As an example, the ``stream()`` method for the ``{% upper %}`` tag above will
486+look like this::
487+
488+    def stream(self, context):
489+        for chunk in self.nodelist.stream(context):
490+            yield chunk.upper()
491+           
492+If you define ``stream()`` in your custom template tag, you can define its
493+``render()`` method as ``''.join(self.stream(context))`` to avoid duplicate
494+codes.
495Index: docs/topics/http/shortcuts.txt
496===================================================================
497--- docs/topics/http/shortcuts.txt      (revision 1)
498+++ docs/topics/http/shortcuts.txt      (working copy)
499@@ -80,6 +80,30 @@
500         return HttpResponse(t.render(c),
501             mimetype="application/xhtml+xml")
502 
503+``stream_to_response``
504+======================
505+
506+.. function:: stream_to_response(template[, dictionary][, context_instance][, mimetype])
507+
508+   The ``stream_to_response`` shortcut is the same as
509+   :func:`render_to_response`, except that ``stream_to_response`` returns an
510+   :class:`~django.http.HttpResponse` object that gradually renders the
511+   template, while ``render_to_response`` waits for the whole template to be
512+   rendered before putting it into the response.
513+   
514+   This is useful if something inside the template takes a long time to
515+   render. For example, if the template contains the following code::
516+   
517+    {% for item in warehouse %}
518+        Current value: {{ item.calculate_depreciation }}
519+    {% endfor %}
520+   
521+   If there are 100 items in warehouse and ``calculate_depreciation()`` takes
522+   0.5 seconds to complete, ``render_to_response`` will need 50 seconds to
523+   render all items before returning a response, while ``stream_to_response``
524+   will immediately return a response that renders one item every half
525+   seconds.
526+
527 ``redirect``
528 ============
529 
530Index: docs/ref/templates/api.txt
531===================================================================
532--- docs/ref/templates/api.txt  (revision 1)
533+++ docs/ref/templates/api.txt  (working copy)
534@@ -207,6 +207,15 @@
535             self.database_record.delete()
536         sensitive_function.alters_data = True
537 
538+Getting the rendering generator
539+-------------------------------
540+
541+As an alternative to rendering a context all-at-once, you can get the string
542+generator that render the context progressively by calling the ``Template``
543+object's ``stream()`` method, passing the context as its argument. You can
544+then pass the generator to :class:`~django.http.HttpResponse` to get a
545+"streaming" effect on your view.
546+
547 .. _invalid-template-variables:
548 
549 How invalid variables are handled
550@@ -690,6 +699,14 @@
551 calls ``render_to_string`` and feeds the result into an ``HttpResponse``
552 suitable for returning directly from a view.
553 
554+The ``stream()`` shortcut
555+=========================
556+
557+This shortcut is like :func:`render_to_string` but instead of returning
558+the whole rendered text of the template, this shortcut returns a generator
559+that successively renders parts of the template. See
560+:func:`~django.shortcuts.stream_to_response()` for an example of its usage.
561+
562 Configuring the template system in standalone mode
563 ==================================================
564