Showing posts with label templatetags. Show all posts
Showing posts with label templatetags. Show all posts

Tuesday, 28 April 2009

Custom Template Tags - Part 2: Once around the block

In last weeks tutorial we saw how to create simple tags for inclusion in django templates. This week we shall look at how to create block style tags to further to further simplify some common tasks.


Say for example your site design called for a series of articles to be displayed one after another, each one in a box that shared a common style. Such as the (particularly simple) example below:


<div class="content_box">
<h1 class="box_heading">Content Title</h1>
Content
</div>

From what we covered last week it would be possible to create two simple tags, one to represent the opening tags and top of the box, the other to represent the closing tags and the bottom of the box. A cleaner way however would be to create a block style tag, allowing something like the following to be used:


{% content_box "Content Title" %}
Content
{% endcontent_box %}

In order to do this add the following to the file content_tags.py which should be created in the templatetags folder from last week.


from django import template
register = template.Library()

@register.tag(name="content_box")
def do_content_box(parser, token):
try:
tag_name, content_heading = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError, "%r tag requires\
exactly one argument" % token.contents.split()[0]
content_heading = template.Variable(content_heading)
nodelist = parser.parse(('endcontent_box',))
parser.delete_first_token()
return FormatContentBoxNode(content_heading, nodelist)

class FormatContentBoxNode(template.Node):
def __init__(self, content_heading, nodelist):
self.content_heading = content_heading
self.nodelist = nodelist

def render(self, context):
content = self.nodelist.render(context)
return """<div class="content_box">
<h1 class="box_heading">%(title)s</h1>
%(content)s
</div>""" % {'title':self.content_heading,
'content':content,}

This code consists of two parts, the function do_content_box is the main tag code. The parser variable is there to provide access to the main body of the template and the token variable is there to provide access to any variables that may have been passed in to the tag. Variables are extracted from token using split_contents() function. This is a special type of split which ensures that quotes are properly preserved rather than just splitting on any white space going. It should also be noted that the first value returned by split_contents() will always be the name of the tag. The next task is to turn the split parameters into Variable objects. This means that django will save anything encased in quotes as a string, whilst attempting to resolve anything else as a context variable.


The main content is extracted from parser by using the parse(List of tags to stop parsing at) function. This returns a list of nodes that we pass into the second part - the FormatContentBoxNode (more on that shortly however). Once the data you require has been parsed then the parser needs to be told to delete the token it just hit with a call to delete_first_token() otherwise it will attempt to parse it a second time which generally isn't a good idea. Finally we pass our extracted data to a FormatContentBoxNode and return it.


The FormatContentBoxNode is a class that inherits from template.Node and is responsible for saying how the tag should be rendered. The __init__ function is fairly basic just storing whatever it's given for later use. The render function is the important one though as this does all of the hard work. In the example given I've included the snippet of html to be rendered as a string but it could quite easily use a Template that's been rendered to a string (in the same way you might render something in a view).


What is important to note about this function is to make sure that you call render(context) on any node lists that you pass into it. This ensures that django processes all the content within your tags too (so that any variables etc. are expanded correctly and not just output as code). The rest of the function is just simple formatting to prepare the content for output.


Now you can see it in action if you create a new view using the following code and point a url at it:


from django.template import Template,Context
from django.http import HttpResponse
def tag_test(request):
test_list=['Item 1','Item 2','Third time lucky']
t = Template("""{% load content_tags %}
<html>
<head><title>Tag Test 2</title></head>
<body>
{% content_box "Some uniformly formatted content" %}
This text should appear within the div.<br />
Variables can also be used in these tags too such as {{ test_list }}.
{% endcontent_box %}
</body>
</html>""")
return HttpResponse(t.render(Context({'test_list':test_list})))

Now that we've created a basic version of the block style template tag it's time to expand on it to include a second optional tag that delimits the text within. To expand on the first example we will look at how you can add a 'Read More' point in the text entered so that everything below that point will be concealed until the reader clicks to say they wish to read more about it. The code below demonstrates how to do this (all changes from the original example have been highlighted).


@register.tag(name="content_box")
def do_content_box(parser, token):
try:
tag_name, content_heading = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError, "%r tag requires exactly one argument" % token.contents.split()[0]
content_heading = template.Variable(content_heading)
sample_nodelist = parser.parse(('readmore','endcontent_box',))
readmore_nodelist = None
ptoken = parser.next_token()
if ptoken.contents == 'readmore':
readmore_nodelist = parser.parse(('endcontent_box',))
parser.delete_first_token()
return FormatContentBoxNode(content_heading, sample_nodelist, readmore_nodelist)

class FormatContentBoxNode(template.Node):
def __init__(self, content_heading, sample_nodelist, readmore_nodelist):
self.content_heading = content_heading
self.sample_nodelist = sample_nodelist
self.readmore_nodelist = readmore_nodelist

def render(self, context):
sample_content = self.sample_nodelist.render(context)
if self.readmore_nodelist is not None:
further_content = self.readmore_nodelist.render(context)
return """<div class="content_box">
<script type="text/javascript">
function show_readmore() {
document.getElementById('morelink').style.display='none';
document.getElementById('moretext').style.display='block';
return false;
</script>
<h1 class="box_heading">%(title)s</h1>
%(sample_content)s<br />
<a id="morelink" href="#" onclick="return show_readmore();">Read More...</a>
<div id="moretext" style="display:none">%(further_content)s</div>
</div>""" % {'title':self.content_heading,
'sample_content':sample_content,
'further_content':further_content,}
else:

return """<div class="content_box">
<h1 class="box_heading">%(title)s</h1>
%(sample_content)s
</div>""" % {'title':self.content_heading,
'sample_content':sample_content,}

The first change is to the parsing. parse has been given an extra option of 'readmore' the first time through telling it that it should stop if it hits either a 'readmore' or an 'endcontent_box' tag. The second change is that we use next_token() in order to process the token instead of deleting it. This allows us to check what the name of the tag we hit was and if it was the 'readmore' one we can parse the rest of the data until 'endcontent_box' is hit as was done in the first example.


The extra data is then passed through to the FormatContentBoxNode which checks if it was given any readmore text and if so it includes it in a hidden div and includes a read more link to allow it to be made visible (apologies for the quality of the javascript but I wanted something quick and simple to get the point across). In the case where no read more text is included we just display things as before.


Finally this can be tried by putting the code below in a view and pointing a url at it.


from django.template import Template,Context
from django.http import HttpResponse
def tag_test(request):
test_list=['Item 1','Item 2','Third time lucky']
t = Template("""{% load content_tags %}
<html>
<head><title>Tag Test 2</title></head>
<body>
{% content_box "Some uniformly formatted content" %}
This text should appear within the div.<br />
Variables can also be used in these tags too such as {{ test_list }}.
{% readmore %}
Only people who are interested in seeing more will be able to see this
text!
{% endcontent_box %}
</body>
</html>""")
return HttpResponse(t.render(Context({'test_list':test_list})))

That concludes the tutorial on block style template tags. If there's anything here that you would like more advice on, or any topics you would like for a future tutorial then feel free to drop me a comment below. Next time we shall be finishing talking about custom template items by looking at how to create custom filters.

Sunday, 19 April 2009

Custom Template Tags - Part 1: Keeping It Simple

If you've ever created a django template before then you'll have used a template tag in one form or another. This tutorial will show you how you can create a simple template tag to help automate a common task.


The simplest of the Simple template tags are used for those cases where you find that you have a chunk of HTML code which is used in multiple places within your code, but because the location isn't static it can't be included in a base template. These tags would take no parameters at all and would output the same chunk of HTML every time. An example of this in the default tags would be {% debug %} which will output a load of debug information at the point the tag is placed.


The next level of complexity for template tags comes by allowing parameters into the simple tags. This could be used for things where you require a consistent format or where you want to run a piece of python code over a variable before displaying it. An example of one if these in the default tags would be {% url url_name %} which we saw last week in the tutorial on naming urls.


Since both of these types of tags are created on a similar way this tutorial will demonstrate how to create one that takes parameters. The example to be used this week will be for a tag that takes a list as a parameter and formats it into a series of bullet points.


The first thing to do is to create a directory named templatetags within your application directory. This is a special directory that django will look in whenever you try to import a set of tags (it will look for this directory in every installed application). In the newly-created directory create a blank file named __init__.py (which is required by python) and a file name list_formatters.py which should contain the following code:


from django import template

register = template.Library()

@register.simple_tag
def format_list(input_list):
return "<ul>\n <li>%s</li>\n<ul>" % "</li>\n <li>".join(input_list)


This code basically registers the function with the template library and says that it's a simple tag. The function then takes its only input and joins it together with some HTML to produce a bullet point style list.


Now we have generated the code for the tag, we can import it into a template and try it out. Create a new view using the following code and point a url at it:


from django.template import Template,Context
from django.http import HttpResponse
def tag_test(request):
test_list=['Item 1','Item 2','Third time lucky']
t = Template("""{% load list_formatters %}
<html>
<head><title>Tag Test</title></head>
<body>
{% format_list test_list %}
</body>
</html>""")
return HttpResponse(t.render(Context({'test_list':test_list})))

View this newly-created page and you should see that the list passed into the context has been formatted as a series of bullet points.


The next tutorial will look at block template tags i.e. ones that take a block of text/code in the middle of them like {% block %}{% endblock %} or {% if condition %}{% else %}{% endif %}