12.1. Adding a tag cloud¶
Django does not only ship some built-in tags for the template engine but also allows you to extend it with custom tags. In this exercise we will write our own template tag to display a tag cloud on every page we want. The tag will be able to either display a tag cloud for all tags or just for one user depending on whether we provide an extra argument or not. This short example shows the different ways the new tag can be used:
1 2 3 4 5 6 7 | <!-- Tag cloud for all tags -->
{% tagcloud %}
<!-- Tag cloud for a single user (several variants) -->
{% tagcloud owner %}
{% tagcloud owner=owner %}
{% tagcloud owner=some_other_user %}
|
12.1.1. Writing custom template tags¶
Before we can write our template tag we have to prepare a special file structure
so Django can find and load our template tag. So create the templatetags
directory inside the marcador
directory and inside of the new directory
create the two Python files:
mysite
`-- marcador
`-- templatetags
|-- __init__.py
`-- marcador_tags.py
From now on we are able to load our tags in the template via {% load marcador_tags %}
.
Let’s add our first tag:
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 | from django import template
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.html import format_html_join
from ..models import Tag
register = template.Library()
@register.simple_tag(takes_context=True)
def tagcloud(context, owner=None):
url = reverse('marcador_bookmark_list')
filters = {'bookmark__is_public': True}
if owner is not None:
url = reverse('marcador_bookmark_user',
kwargs={'username': owner.username})
filters['bookmark__owner'] = owner
if context['user'] == owner:
del filters['bookmark__is_public']
tags = Tag.objects.filter(**filters)
tags = tags.annotate(count=models.Count('bookmark'))
tags = tags.order_by('name').values_list('name', 'count')
fmt = '<a href="%s?tag={0}">{0} ({1})</a>' % url
return format_html_join(', ', fmt, tags)
|
We start by initializing a Library
class, where we can register our tags.
Django provides a few shortcuts for writing simple tags without having to dig
to deep into the template engine. simple_tag
is such a shortcut which
allows us to write a simple function which the template system will call and
then put the output into the template.
When we register our function via the simple_tag
-decorator, Django will
inspect the function definition and mark tag arguments as required or optional
depending on whether they have a default value or not. In our case this means
that the tag will support a single argument (ignore context
for now) named
owner
which is optional.
Since we want to be able to show tags of private bookmarks to the owner, we
would have to add another argument to the tag. Luckily Django provides an easier
way to access all variables in the template; this is the special context
argument in our function, which only exists if we register the tag with
takes_context=True
. By default, Django has a few special variables in every
template, like user
for the currently authenticated user. Those variables
never change and make a perfect candidate to be accessed via the global context
instead of a custom argument to our tag.
So what does the simple tag tagcloud
actually do? At first the URL
to the main page is generated. We will use this URL to show just a
single tag instead of the full list. A dictionary filters
is defined
which will later be used to query the database and select only public
bookmarks. If the argument owner
has been set, the content of
url
is replaced with an URL pointing to the bookmarks of the user
represented by owner
. Also a new key is added to filters
to
select only the bookmarks of the user owner
. If owner
is the
currently logged in user we remove the filter to show only private
bookmarks because the user is looking at her own bookmarks.
Now the database query has to be build: The first step is to use the
dictionary filters
to filter all bookmarks so that the resulting
QuerySet
only contains the ones we need. Then we use the
annotate method to add
the number of bookmarks to each tag. Finally we order the tags by name
and convert the QuerySet
into a list of tuples using the
values_list method.
The last step is to render the actual HTML that the template tag will insert
into the page. Therefore we define a string fmt
which uses the url
we
set above. This string uses Python`s Format String Syntax to be able to add query string, tag name and bookmarks count
dynamically. Now we can use the format_html_join helper function to render the
HTML for all tags, separated by a comma using the fmt
string.
Currently the views used to render the list of bookmarks and the bookmarks of a user are not capable of using the query string to filter the results. But it’s easy to change that! Add the following two lines to each view:
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | def bookmark_list(request):
bookmarks = Bookmark.public.all()
if request.GET.get('tag'):
bookmarks = bookmarks.filter(tags__name=request.GET['tag'])
context = {'bookmarks': bookmarks}
return render(request, 'marcador/bookmark_list.html', context)
def bookmark_user(request, username):
user = get_object_or_404(User, username=username)
if request.user == user:
bookmarks = user.bookmarks.all()
else:
bookmarks = Bookmark.public.filter(owner__username=username)
if request.GET.get('tag'):
bookmarks = bookmarks.filter(tags__name=request.GET['tag'])
context = {'bookmarks': bookmarks, 'owner': user}
return render(request, 'marcador/bookmark_user.html', context)
|
The two new lines add a new filter to the bookmarks
QuerySet
if
tag
is present in the query string. Using the get
method on the
dictionary-like request.GET
object prevents a KeyError
to be
raised if tag
is not present in the query string.
To show the tag cloud in our template we first load the tags in
templates/base.html
:
1 2 | {% load staticfiles marcador_tags %}
<!doctype html>
|
and change the layout to include a sidebar with the tags:
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 | <div class="container">
<div class="row">
<div class="col-md-12">
{% block heading %}{% endblock %}
</div>
</div>
<div class="row">
<div class="col-md-8">
{% block content %}{% endblock %}
</div>
<div class="col-md-4">
<div class="panel panel-primary">
<div class="panel-heading">Tag cloud</div>
<div class="panel-body">
{% block sidebar %}
{% tagcloud %}
{% endblock %}
</div>
</div>
</div>
</div>
<hr>
<footer>
<p>© Company 2015</p>
</footer>
</div> <!-- /container --> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
|
Finally we also modify the user bookmark template
(marcador/templates/marcador/bookmark_user.html
) to only show the tags
for the requested user:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {% extends "marcador/bookmark_list.html" %}
{% load marcador_tags %}
{% block title %}{{ owner.username }}'s bookmarks{% endblock %}
{% block heading %}
<h2>{{ owner.username }}'s bookmarks<br>
<small>{{ bookmarks.count }} bookmarks in total</small>
</h2>
{% endblock %}
{% block sidebar %}
{% tagcloud owner %}
{% endblock %}
|
That’s it! Try out the new tag cloud in your frontend - it should looks like this. If you click on the tags the list should be filtered to display only bookmarks with the selected tag: