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>&copy; 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:

Frontend Bookmark List View with Tag Cloud