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 span
s 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
Matt on
Thanks for this! Perfect timing, this is exactly the functionality I needed this afternoon. Saved me building my own. :)
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.
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
then in template
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
Thanks in advance.
Andriy
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’surls.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.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
Andriy on
This might be useful for you
Active page class for selected menu items http://djangosnippets.org/snippets/2825/
zibi on
be sure to put ‘django.core.context_processors.request’ in TEMPLATE_CONTEXT_PROCESSORS
Michał Ochman on
Thanks for reminding that zibi.
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 :-)
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
Simon on
Thanks, works great!
Dae on
For namespaces to work, change:
matches = resolved and resolved.url_name == url_name
into
matches = resolved and resolved.view_name == url_name
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.