Skip to content

Commit

Permalink
Render specific blocks from templates (useful for AJAX)
Browse files Browse the repository at this point in the history
  • Loading branch information
sciyoshi authored and clokep committed May 18, 2016
1 parent 7b38342 commit c731a5b
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 0 deletions.
30 changes: 30 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
Django Render Block
###################

Allows getting the rendered content of a specific block tag. Useful if you want
to send just a part of a template back for an AJAX request. Works for arbitrary
template inheritance, even if a block is defined in the child template but not
in the parent.

Example:

In `test1.html`:

```jinja
{% block block1 %}block1 from test1{% endblock %}
{% block block2 %}block2 from test1{% endblock %}
```

In `test2.html`:

```jinja
{% extends 'test1.html' %}
{% block block1 %}block1 from test1{% endblock %}
```

And from the Python shell:

```python
>>> from django.template import loader, Context
>>> from template import render_block_to_string
>>> print render_block_to_string('test2.html', 'block1', Context({}))
u'block1 from test2'
>>> print render_block_to_string('test2.html', 'block2', Context({}))
u'block2 from test1'
```

Attribution
===========
Expand Down
8 changes: 8 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env python
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'

from django.core import management

if __name__ == "__main__":
management.execute_from_command_line()
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Install the current directory as an editable package. Denpendencies required
# for installing the package and running unit tests are defined in setup.py.
-e .

# Not tested with older versions of Django.
Django==1.1.4 # Actually 0.96, but this is the oldest on pypi.
91 changes: 91 additions & 0 deletions template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# file template.py

import new
from django.template.loader_tags import BlockNode, ExtendsNode
from django.template import loader, Context, RequestContext, TextNode

class BlockNotFound(Exception):
pass

class ExtendsNodeMixin(object):
def compile(self, context):
"""
Compiles this node and returns the compiled parent.
"""
compiled_parent = self.get_parent(context)
pos = 0
while isinstance(compiled_parent.nodelist[pos], TextNode):
pos += 1
parent_is_child = isinstance(compiled_parent.nodelist[pos], ExtendsNode)
parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
for block_node in self.nodelist.get_nodes_by_type(BlockNode):
# Check for a BlockNode with this node's name, and replace it if found.
try:
parent_block = parent_blocks[block_node.name]
except KeyError:
# This BlockNode wasn't found in the parent template, but the
# parent block might be defined in the parent's *parent*, so we
# add this BlockNode to the parent's ExtendsNode nodelist, so
# it'll be checked when the parent node's render() is called.
if parent_is_child:
compiled_parent.nodelist[pos].nodelist.append(block_node)
else:
# Keep any existing parents and add a new one. Used by BlockNode.
parent_block.parent = block_node.parent
parent_block.add_parent(parent_block.nodelist)
parent_block.nodelist = block_node.nodelist
return compiled_parent

ExtendsNode.__bases__ += (ExtendsNodeMixin,)

def render(self, context):
self.compiled_parent = self.compile(context)
return self.compiled_parent.render(context)

ExtendsNode.render = new.instancemethod(render, None, ExtendsNode)

def render_template_block(template, block, context):
"""
Renders a single block from a template. This template should have previously been rendered.
"""
if len(template.nodelist) and not isinstance(template.nodelist[0], ExtendsNode):
for blk in template.nodelist:
if isinstance(blk, BlockNode) and blk.name == block:
return blk.render(context)
raise BlockNotFound
for blk in template.nodelist[0].nodelist:
if isinstance(blk, BlockNode) and blk.name == block:
return blk.render(context)
return render_template_block(template.nodelist[0].compiled_parent, block, context)

def render_block_to_string(template_name, block, dictionary=None, context_instance=None):
"""
Loads the given template_name and renders the given block with the given dictionary as
context. Returns a string.
"""
dictionary = dictionary or {}
t = loader.get_template(template_name)
if context_instance:
context_instance.update(dictionary)
else:
context_instance = Context(dictionary)
t.render(context_instance)
return render_template_block(t, block, context_instance)

def direct_block_to_template(request, template, block, extra_context=None, mimetype=None, **kwargs):
"""
Render a given block in a given template with any extra URL parameters in the context as
``{{ params }}``.
"""
if extra_context is None:
extra_context = {}
dictionary = {'params': kwargs}
for key, value in extra_context.items():
if callable(value):
dictionary[key] = value()
else:
dictionary[key] = value
c = RequestContext(request, dictionary)
t = loader.get_template(template)
t.render(c)
return HttpResponse(render_template_block(t, block, c), mimetype=mimetype)
File renamed without changes.
Empty file added tests/models.py
Empty file.
15 changes: 15 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
SECRET_KEY = 'not_empty'
SITE_ID = 1

DATABASE_ENGINE = 'sqlite3',

MIDDLEWARE_CLASSES = tuple()

EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'

INSTALLED_APPS = (
'tests',
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
2 changes: 2 additions & 0 deletions tests/templates/test1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% block block1 %}block1 from test1{% endblock %}
{% block block2 %}block2 from test1{% endblock %}
2 changes: 2 additions & 0 deletions tests/templates/test2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% extends 'test1.html' %}
{% block block1 %}block1 from test1{% endblock %}
15 changes: 15 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from unittest import TestCase

from django.template import loader, Context

from template import render_block_to_string


class TestCases(TestCase):
def test_1(self):
result = render_block_to_string('test2.html', 'block1', Context({}))
self.assertEqual(result, u'block1 from test2')

def test_2(self):
result = render_block_to_string('test2.html', 'block2', Context({}))
self.assertEqual(result, u'block2 from test1')

0 comments on commit c731a5b

Please sign in to comment.