Quantcast
Viewing all articles
Browse latest Browse all 2

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.


Viewing all articles
Browse latest Browse all 2

Trending Articles