A Smarter Django URL Tag

Posted on 2013-07-01 00:25:38+00:00

Django's built-in url tag is very useful. Repeating yourself goes against Django's philosophy, and the url tag helps you avoid that pitfall. But there's plenty of room for improvement. This post will show you how to make Django's url tag smarter and more useful. Note: this post applies to Django 1.5 and above. Earlier versions of Django use a deprecated syntax.

So what can we do to improve the url tag? It'd be nice if it could tell us when a path is active. That way, we can mark certain menu items and style them differently. But let's not stop there. Sometimes an exact path match is not good enough. Say you're under the "Shop" section of your site. You still want the navigation item for "Shop" marked as active, even if you're in a deeper part of the site - other than the "Shop" landing page. So let's define these behaviors:

  • Active means the paths are an exact match. If you're on the /shop/ page, and your URL tag result matches that, then that link is active. However, if you're visiting the /shop/on-sale/ section of the site, then the URL is not active.
  • Active root means that the paths match or the beginning of the path matches. So if you're visiting /shop/on-sale/, url tags for /shop/ would match. This is useful for main navigation items and the like.

One last improvement: we want to be able to use these helpers for hard-coded URLs too. For example, when linking to flatpages, you do want to be able to pass '/my-flatpage/' to the tag and see if it's active (or in the active root).

Learning Django? Subscribe to my Django articles, tips and tutorials.

With all that out of the way, let's start by defining our URL tag. In any of your existing apps, create the templatetags directory (don't forget the __init__.py file inside), and then create a file called utilities.py inside. Feel free to name this differently if you'd like.

In this file, add the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django import template
from django.template.defaulttags import URLNode, url as url_tag

register = template.Library()


@register.tag
def url(parser, token):
    validator = url_tag(parser, token)
    return SmartURLNode(validator.view_name, validator.args, validator.kwargs, validator.asvar)

You'll notice we're registering our tag with the same name as the existing core Django tag. Don't worry, since the syntax and arguments match, this isn't a problem. In our code, we need to import the old url tag so we can reuse its validation logic. No need to repeat ourselves!

For our improvements to work, we need to attach arbitrary attributes to our result string (for the URL), so we define a dummy class as follows:

1
2
3
4
5
6
class SmartURL(unicode):
    """
    This is a wrapper class that allows us to attach attributes to regular
    unicode strings.
    """
    pass

Now let's define our smarter URL node, as follows:

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Notice we inherit from the existing URLNode.
class SmartURLNode(URLNode):
    def render(self, context):
        # Step 1
        resolved_view_name = self.view_name.resolve(context)

        if resolved_view_name:
            view_name_string = "'%s'" % resolved_view_name
        else:
            view_name_string = unicode(self.view_name)

        # Step 2
        if len(view_name_string) >= 3 and view_name_string[0] == view_name_string[-1] and view_name_string[0] in ('"', "'") and view_name_string[1] == '/':
            rendered = view_name_string[1:-1]

            if self.asvar:
                context[self.asvar] = rendered
        # Step 3
        else:
            rendered = super(SmartURLNode, self).render(context)

        # Step 4
        if not self.asvar:
            return rendered
        # Step 5
        else:
            resolved_url = SmartURL(context[self.asvar])
            request = context.get('request', None)

            # Step 6
            if request:
                # Step 7
                if resolved_url == request.path:
                    resolved_url.active = 'active'
                else:
                    resolved_url.active = ''

                # Step 8
                if request.path.startswith(resolved_url):
                    resolved_url.active_root = 'active'
                else:
                    resolved_url.active_root = ''

            # Step 9
            context[self.asvar] = resolved_url
            return ''

Let's go step by step here. Use the comments above to follow each step:

  1. First we try to resolve the view name. If we passed in a variable or function, including, say, some_model_instance.get_absolute_url, then this will resolve nicely into a path. If it resolves, we turn it into a faked template string. If it doesn't resolve, then we assume they passed a hard-coded path.
  2. But we only accept hard-coded paths starting with a slash. If that's the case, we assume that's the final path, no need to further resolve the path.
  3. If it's not a hard-coded path, we pass the rendering on to the regular URLNode that we inherited from. Remember: avoid repeating yourself. Just reuse existing logic.
  4. Now, if we aren't assigning to a variable, simple return the resolved path. And yes, it's a little silly to use this tag for hard-coded paths without assignment. But it'll work!
  5. On the other hand, if we're assigning, things get interesting. First, we get our fully resolved string.
  6. Then, if there's a request object in the context, we compare paths and assign attributes as necessary.
  7. If the request path matches the url exactly, set the active attribute on our SmartURL instance.
  8. If the request path starts with the url, also known as the root, then set the active_root attribute.
  9. And that's it! Assign the SmartURL instance back into the context and we're done. As this is an assignment use, return an empty string.

Here are some usage examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{# First we load the library. #}
{% load utilities %}

{# Our standard tag usage. #}
<a href="{% url 'home_page' %}">Home Page</a>

{# Our standard tag usage with assignment. #}
{% url 'home_page' as url %}<a href="{{ url }}">Home Page</a>

{# Here we use assignment, and add the active class when the pages match. #}
{% url 'another_page' as url %}<a class="{{ url.active }}" href="{{ url }}">Another Page</a>

{# We use assignment again, but we match the root rather than the whole path. #}
{% url 'last_page' as url %}<a class="{{ url.active_root }}" href="{{ url }}">Last Page</a>

{# Flatpages work too! #}
{% url '/about/' as url %}<a class="{{ url.active }}" href="{{ url }}">About Page</a>

And in our CSS files, we can now do:

1
2
a { color: black; }
a.active { color: red; }

That completes this tutorial. Got some other improvements you'd like to make? Leave a comment below. As always, questions and feedback are also welcome.

Tags:Django

Comments

Sander2013-11-04 13:21:34+00:00

Nice stuff!

But could you please change your code font-style to something more readable. This is hurting my eyes. I like Consolas for example.

Reply
silviogutierrez2013-11-04 16:50:45+00:00

I've been meaning to - classic form vs function dilemma, I suppose.

Reply

Post New Comment