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.