Highlighting current active page in Django

Every once in a while, you would like to highlight a current, active page in the navigation. There are number of solutions available to this problem, but none of them was flexible enough for me to use in my projects.

The CurrentPageMiddleware is very useful and saves a lot of time, but its main problem is that it only adds CSS classes to anchor elements, but what if you wanted to add CSS class to its parent element?

On the other hand the solutions that use template tags are very verbose and often require passing the request object along with the URL we want to test.

However, the biggest drawback I came across is that they are also too generic and don’t work as expected with URLs using custom parameters. That’s why I came up with the following solution for my Django 1.4 applications.

Code

# utils/templatetags/navigation.py
 
from django import template
from django.core import urlresolvers
 
 
register = template.Library()
 
 
@register.simple_tag(takes_context=True)
def current(context, url_name, return_value=' current', **kwargs):
    matches = current_url_equals(context, url_name, **kwargs)
    return return_value if matches else ''
 
 
def current_url_equals(context, url_name, **kwargs):
    resolved = False
    try:
        resolved = urlresolvers.resolve(context.get('request').path)
    except:
        pass
    matches = resolved and resolved.url_name == url_name
    if matches and kwargs:
        for key in kwargs:
            kwarg = kwargs.get(key)
            resolved_kwarg = resolved.kwargs.get(key)
            if not resolved_kwarg or kwarg != resolved_kwarg:
                return False
    return matches

It’s broken down into two methods for easier testing and mocking.

Usage

Given your URL config file has the following entries:

# myapp/urls.py
 
url(r'^/pages/$', 'pages', name='pages-pages'),
url(r'^/page/(?P<page_slug>.+)/$', 'page', name='pages-page'),

Whenever you want to add CSS class based on the current page, all you have to do in your templates is:

# templates/myapp/menu.html
 
{% load url from future %}
{% load current from navigation %}
<span class="{% current 'pages-pages' %}">
    <a href="{% url 'pages-pages' %}">Page list</a>
</span>

If you need to test for kwargs in the URL, you can do it this way:

# templates/myapp/menu.html
 
{% load url from future %}
{% load current from navigation %}
<span class="{% current 'pages-page' page_slug='contact-us' %}">
    <a href="{% url 'pages-page' 'contact-us' %}">Contact Us</a>
</span>

Then the spans above will have current CSS class applied so you can style them as you like.

As zibi mentioned, be sure to put django.core.context_processors.request in TEMPLATE_CONTEXT_PROCESSORS

Unit Tests

The code above is covered by unit tests, you can see them below.

# utils/tests/templatetags.py
 
import mock
from django.core.urlresolvers import reverse
from django.template import loader
from django.template.context import Context
from django.test import TestCase
from utils.templatetags.navigation import current, current_url_equals
 
 
class CurrentTagTest(TestCase):
    def setUp(self):
        self.request = mock.Mock
        self.url_name = 'pages-page'
        self.request.path = reverse(self.url_name)
 
    def test_returns_value_when_resolved_path_equals_current_path(self):
        return_value = ' current'
        returned_value = current({'request': self.request}, self.url_name)
        self.assertEquals(return_value, returned_value)
 
        return_value = 'test'
        returned_value = current({'request': self.request},
                                 self.url_name, return_value)
        self.assertEquals(return_value, returned_value)
 
    def test_returns_empty_string_when_resolved_path_not_equals_current_path(self):
        return_value = ''
        returned_value = current({'request': self.request}, 'not_pages-page')
        self.assertEquals(return_value, returned_value)
 
    def test_returns_empty_string_when_current_path_is_not_resolved(self):
        return_value = ''
        request = mock.Mock
        request.path = '/invalid-!@#-path'
        returned_value = current({'request': request}, 'test')
        self.assertEquals(return_value, returned_value)
 
 
class CurrentUrlEqualsHelperTest(TestCase):
    def setUp(self):
        self.request = mock.Mock
        self.url_name = 'pages-page'
        self.request.path = reverse(self.url_name)
        self.context = {'request': self.request}
 
    def test_returns_true_when_resolved_path_equals_current_path(self):
        matches = current_url_equals(self.context, self.url_name)
        self.assertTrue(matches)
 
    @mock.patch('django.core.urlresolvers.resolve')
    def test_returns_true_when_kwargs_matched(self, mocked_resolve):
        url_name = 'test_url'
        page_slug = 'test_slug'
        mocked_resolve.url_name = url_name
        mocked_resolve.kwargs = {
            'page_slug': page_slug,
        }
        mocked_resolve.return_value = mocked_resolve
        matches = current_url_equals(self.context, url_name,
                                     page_slug=page_slug)
        path = self.context.get('request').path
        mocked_resolve.assert_called_once_with(path)
        self.assertTrue(matches)
 
    def test_returns_false_when_resolved_path_not_current_path(self):
        url_name = 'not_pages-page'
        matches = current_url_equals(self.context, url_name)
        self.assertFalse(matches)
 
    def test_returns_false_when_current_path_not_resolved(self):
        self.request.path = '/invalid-!@#-path'
        url_name = 'test'
        matches = current_url_equals(self.context, url_name)
        self.assertFalse(matches)
 
    def test_returns_false_when_context_invalid(self):
        context = mock.Mock
        url_name = self.url_name
        matches = current_url_equals(context, url_name)
        self.assertFalse(matches)
 
    @mock.patch('django.core.urlresolvers.resolve')
    def test_returns_false_when_kwargs_unmatched(self, mocked_resolve):
        url_name = 'test_url'
        page_slug = 'test_slug'
        mocked_resolve.url_name = url_name
        mocked_resolve.kwargs = {
            'page_slug': 'slug_test',
        }
        mocked_resolve.return_value = mocked_resolve
        matches = current_url_equals(self.context, url_name,
                                     page_slug=page_slug)
        path = self.context.get('request').path
        mocked_resolve.assert_called_once_with(path)
        self.assertFalse(matches)
 
    @mock.patch('django.core.urlresolvers.resolve')
    def test_returns_false_when_resolve_kwargs_unmatched(self,
                                                         mocked_resolve):
        url_name = 'test_url'
        page_slug = 'test_slug'
        mocked_resolve.url_name = url_name
        mocked_resolve.kwargs = {
            'page_slug': 'slug_test',
        }
        mocked_resolve.return_value = mocked_resolve
        matches = current_url_equals(self.context, url_name,
                                     page_slug=page_slug,
                                     other_kwarg='test')
        path = self.context.get('request').path
        mocked_resolve.assert_called_once_with(path)
        self.assertFalse(matches)

Reusable django app

I am going to put it on github soon in the form of reusable django app, so it’s even easier to use.

Comments

  1. Matt on

    Thanks for this! Perfect timing, this is exactly the functionality I needed this afternoon. Saved me building my own. :)

    Reply
    • Michał on

      I’m glad I could help. I actually wanted to publish this long time ago but never had a time to sit down and actually do it.

      Reply
  2. Andriy on

    Hi,

    For some reason that does not work for me…
    Could you please take a look on this.

    django 1.4
    In my django app I’m using class based views

    in urls.py

    urlpatterns += patterns('blog.views',
        url(r'^$', RootIndexView.as_view(template_name = "blog/index.html"), name='home'),
    )
    .....
    urlpatterns += patterns('',
        url(r'^weblog/',include('blog.urls.articles'), name='weblog'),
    )

    then in template

    <a href="{% url home %}" rel="nofollow">Home</a>
    <a href="{% url weblog %}" rel="nofollow">Blog</a>

    Tried with/out quotes, like ‘home’, home – does not work. Found that quotes are used beginning from django 1.5. In django 1.4 it should be loaded as {% from future load url %}

    I get this error

    Exception Value:
    Reverse for ‘weblog’ with arguments ‘()’ and keyword arguments ‘{}’ not found.

    Thanks in advance.
    Andriy

    Reply
    • Michał on

      This article is not about {% url … %} tags, these are already well documented.

      Is {% url 'home' %} working for you? It should be put in quotes.

      Your {% url weblog %} is invalid, because you point it to blog app’s urls.py file and then try to use it as a single URL pattern. Take a look at the documentation linked above.

      Thank you for mentioning {% load url from future %} template import, I forgot to include it in the examples.

      Reply
  3. Andriy on

    Thanks for the help, django official blog has really great docs and resources.
    I use old fashioned style without quotes for url, but I have to pass url_name as string(in quotes), otherwise current() tag gets empty string.

    <a href="{% url home %}" rel="nofollow">Home</a>

    Added some printing to debug current() tag,

    context.get(‘request’).path /
    url_name home
    resolved.url_name home

    Reply
  4. Andriy on

    This might be useful for you
    Active page class for selected menu items http://djangosnippets.org/snippets/2825/

    Reply
  5. zibi on

    be sure to put ‘django.core.context_processors.request’ in TEMPLATE_CONTEXT_PROCESSORS

    Reply
  6. Vita on

    Thanks a lot for this. I had to make a small change in current_url_equals to support URL namespaces.

    matches = resolved and ‘{r.namespace}:{r.url_name}’.format(r = resolved) == url_name

    Not sure what happens when you try to use it with a not-namespaced URL. I don’t have any :-)

    Reply
  7. Marco on

    great solution. in this way should works with or without namespace:

    resolved_url_name = resolved.url_name
    if resolved.namespace:
    resolved_url_name = “%s:%s” % (resolved.namespace, resolved.url_name)
    matches = resolved and resolved_url_name == url_name

    Reply
  8. Simon on

    Thanks, works great!

    Reply
  9. Dae on

    For namespaces to work, change:

    matches = resolved and resolved.url_name == url_name

    into

    matches = resolved and resolved.view_name == url_name

    Reply
  10. Christopher Jenkins on

    Cool thanks for this, I’m still amazed even in a few years on there isn’t a built-in template tag for this.

    Reply

Leave a Reply