Navigation in django

Viewed 101656

I've just done my first little webapp in django and I love it. I'm about to start on converting an old production PHP site into django and as part its template, there is a navigation bar.

In PHP, I check each nav option's URL against the current URL, in the template code and apply a CSS class if they line up. It's horrendously messy.

Is there something better for django or a good way of handling the code in the template?

To start, how would I go about getting the current URL?

30 Answers

You do not need an if to do that, have a look at the following code:

tags.py

@register.simple_tag
def active(request, pattern):
    import re
    if re.search(pattern, request.path):
        return 'active'
    return ''

urls.py

urlpatterns += patterns('',
    (r'/$', view_home_method, 'home_url_name'),
    (r'/services/$', view_services_method, 'services_url_name'),
    (r'/contact/$', view_contact_method, 'contact_url_name'),
)

base.html

{% load tags %}

{% url 'home_url_name' as home %}
{% url 'services_url_name' as services %}
{% url 'contact_url_name' as contact %}

<div id="navigation">
    <a class="{% active request home %}" href="{{ home }}">Home</a>
    <a class="{% active request services %}" href="{{ services }}">Services</a>
    <a class="{% active request contact %}" href="{{ contact }}">Contact</a>
</div>

that's it. for implementation details have a look at:
gnuvince.wordpress.com
110j.wordpress.com

I use template inheritance to customize navigation. For example:

base.html

<html>
    <head>...</head>
    <body>
        ...
        {% block nav %}
        <ul id="nav">
            <li>{% block nav-home %}<a href="{% url 'home' %}">Home</a>{% endblock %}</li>
            <li>{% block nav-about %}<a href="{% url 'about' %}">About</a>{% endblock %}</li>
            <li>{% block nav-contact %}<a href="{% url 'contact' %}">Contact</a>{% endblock %}</li>
        </ul>
        {% endblock %}
        ...
    </body>
</html>

about.html

{% extends "base.html" %}

{% block nav-about %}<strong class="nav-active">About</strong>{% endblock %}

I liked the cleanness of 110j above so I took most of it and refactored to solve the 3 problems I had with it:

  1. the regular expression was matching the 'home' url against all others
  2. I needed multiple URLs mapped to one navigation tab, so I needed a more complex tag that takes variable amount of parameters
  3. fixed some url problems

Here it is:

tags.py:

from django import template

register = template.Library()

@register.tag
def active(parser, token):
    args = token.split_contents()
    template_tag = args[0]
    if len(args) < 2:
        raise template.TemplateSyntaxError, "%r tag requires at least one argument" % template_tag
    return NavSelectedNode(args[1:])

class NavSelectedNode(template.Node):
    def __init__(self, patterns):
        self.patterns = patterns
    def render(self, context):
        path = context['request'].path
        for p in self.patterns:
            pValue = template.Variable(p).resolve(context)
            if path == pValue:
                return "active" # change this if needed for other bootstrap version (compatible with 3.2)
        return ""

urls.py:

urlpatterns += patterns('',
    url(r'/$', view_home_method, {}, name='home_url_name'),
    url(r'/services/$', view_services_method, {}, name='services_url_name'),
    url(r'/contact/$', view_contact_method, {}, name='contact_url_name'),
    url(r'/contact/$', view_contact2_method, {}, name='contact2_url_name'),
)

base.html:

{% load tags %}

{% url home_url_name as home %}
{% url services_url_name as services %}
{% url contact_url_name as contact %}
{% url contact2_url_name as contact2 %}

<div id="navigation">
    <a class="{% active request home %}" href="home">Home</a>
    <a class="{% active request services %}" href="services">Services</a>
    <a class="{% active request contact contact2 %}" href="contact">Contact</a>
</div>

You could apply a class or id to the body element of the page, rather than to a specific nav item.

HTML:

<body class="{{ nav_class }}">

CSS:

body.home #nav_home,
body.about #nav_about { */ Current nav styles */ }

I do it like this:

<a class="tab {% ifequal active_tab "statistics" %}active{% endifequal %}" href="{% url Member.Statistics %}">Statistics</a>

and then all I have to do is in my view add {'active_tab': 'statistics'} to my context dictionary.

If you are using RequestContext you can get current path in your template as:

{{ request.path }}

And in your view:

from django.template import RequestContext

def my_view(request):
    # do something awesome here
    return template.render(RequestContext(request, context_dict))

You could use the reverse function with the appropriate parameters to get the current url.

Thanks for your answers so far, gents. I've gone for something slightly different again..

In my template:

<li{{ link1_active }}>...link...</li>
<li{{ link2_active }}>...link...</li>
<li{{ link3_active }}>...link...</li>
<li{{ link4_active }}>...link...</li>

Once I've worked out which page I'm on in the logic (usually in urls.py), I pass class="selected" as part of the context under the right name to the template.

Eg if I'm on the link1 page, I'll append {'link1_active':' class="selected"'} to the context for the template to scoop up and inject.

It appears to work and it's fairly clean.

Edit: to keep HTML out of my controller/view, I've modified this a bit:

<li{% if link1_active %} class="selected"{% endif %}>...link...</li>
<li{% if link2_active %} class="selected"{% endif %}>...link...</li>
...

It makes the template a little less readable, but I agree, it's better to not push through raw HTML from the urls file.

I found the best is to use an inclusion tag:

templates/fnf/nav_item.html

<li class="nav-item">
    <a class="nav-link {% if is_active %}active{% endif %}" href="{% url url_name %}">{{ link_name }}</a>
</li>

This is just my basic bootstrap nav item I wish to render.

It gets the href value, and optionally the link_name value. is_active is calculated based on the current request.

templatetags/nav.py

from django import template

register = template.Library()


@register.inclusion_tag('fnf/nav_item.html', takes_context=True)
def nav_item(context, url_name, link_name=None):
    return {
        'url_name': url_name,
        'link_name': link_name or url_name.title(),
        'is_active': context.request.resolver_match.url_name == url_name,
    }

Then use it in a nav: templates/fnf/nav.html

{% load nav %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
        <ul class="navbar-nav mr-auto">
                {% nav_item 'dashboard' %}
            </ul>

I have multiple menus on the same page that are created dynamically through a loop. The posts above relating to the context gave me a quick fix. Hope this helps somebody. (I use this in addition to the active template tag - my fix solves the dynamic issue). It seems like a silly comparison, but it works. I chose to name the variables active_something-unique and something-unique, this way it works with nested menus.

Here is a portion of the view (enough to understand what i am doing):

def project_list(request, catslug):
    "render the category detail page"
    category = get_object_or_404(Category, slug=catslug, site__id__exact=settings.SITE_ID)
    context = {
        'active_category': 
            category,
        'category': 
            category,
        'category_list': 
            Category.objects.filter(site__id__exact=settings.SITE_ID),

    }

And this is from the template:

<ul>
  {% for category in category_list %}
    <li class="tab{% ifequal active_category category %}-active{% endifequal %}">
      <a href="{{ category.get_absolute_url }}">{{ category.cat }}</a>
    </li>
  {% endfor %}
</ul>

My solution was to write a simple context processor to set a variable based on the request path:

def navigation(request):
"""
Custom context processor to set the navigation menu pointer.
"""
nav_pointer = ''
if request.path == '/':
    nav_pointer = 'main'
elif request.path.startswith('/services/'):
    nav_pointer = 'services'
elif request.path.startswith('/other_stuff/'):
    nav_pointer = 'other_stuff'
return {'nav_pointer': nav_pointer}

(Don't forget to add your custom processor to TEMPLATE_CONTEXT_PROCESSORS in settings.py.)

Then in the base template I use an ifequal tag per link to determine whether to append the "active" class. Granted this approach is strictly limited to the flexibility of your path structure, but it works for my relatively modest deployment.

I know I'm late to the party. I didn't like any of the popular solutions though:

The block method seems wrong: I think the navigation should be self contained.

The template_tag method seems wrong: I don't like that I have to get the url from the url-tag first. Also, I think the css-class should be defined in the template, not the tag.

I therefore wrote a filter that doesn't have the drawbacks I described above. It returns True if a url is active and can therefore be used with {% if %}:

{% load navigation %}
<li{% if request|active:"home" %} class="active"{% endif %}><a href="{% url "home" %}">Home</a></li>

The code:

@register.filter(name="active")
def active(request, url_name):
    return resolve(request.path_info).url_name == url_name

Just make sure to use RequestContext on pages with navigation or to enable the request context_processor in your settings.py

TEMPLATE_CONTEXT_PROCESSORS = (
    ...
    'django.core.context_processors.request',
)

Here's my go at it. I ended up implementing a class in my views that contains my navigation structure (flat with some metadata). I then inject this to the template and render it out.

My solution deals with i18n. It probably should be abstracted out a bit more but I haven't really bothered with that really.

views.py:

from django.utils.translation import get_language, ugettext as _


class Navi(list):
    items = (_('Events'), _('Users'), )

    def __init__(self, cur_path):
        lang = get_language()
        first_part = '/' + cur_path.lstrip('/').split('/')[0]

        def set_status(n):
            if n['url'] == first_part:
                n['status'] == 'active'

        for i in self.items:
            o = {'name': i, 'url': '/' + slugify(i)}
            set_status(o)
            self.append(o)

# remember to attach Navi() to your template context!
# ie. 'navi': Navi(request.path)

I defined the template logic using includes like this. Base template:

{% include "includes/navigation.html" with items=navi %}

Actual include (includes/navigation.html):

 <ul class="nav">
     {% for item in items %}
         <li class="{{ item.status }}">
             <a href="{{ item.url }}">{{ item.name }}</a>
         </li>
     {% endfor %}
 </ul>

Hopefully someone will find this useful! I guess it would be pretty easy to extend that idea to support nested hierarchies etc.

I also used jQuery to highlight it and find it more elegant than cluttering the template with non-semantic Django template tags.

The code below works with nested dropdowns in bootstrap 3 (highlights both the parent, and the child <li> element.

// DOM Ready
$(function() {
    // Highlight current page in nav bar
    $('.nav, .navbar-nav li').each(function() {
        // Count the number of links to the current page in the <li>
        var matched_links = $(this).find('a[href]').filter(function() {
            return $(this).attr('href') == window.location.pathname; 
        }).length;
        // If there's at least one, mark the <li> as active
        if (matched_links)
            $(this).addClass('active');
    });
});

It's also quite easy to add a click event to return false (or change the href attribute to #) for the current page, without changing the template/html markup:

        var matched_links = $(this).find('a[href]').filter(function() {
            var matched = $(this).attr('href') == window.location.pathname;
            if (matched)
                $(this).click(function() { return false; });
            return matched;
        }).length;

**

Just add url and name in jinja format like this

**

 <ul class="nav navbar-nav">
        <li>
           <a href="{% url 'index' %}">Cities</a>
        </li>
        <li>
           <a href="{% url 'all_cafes' %}">Cafes</a>
        </li>
    </ul>
Related