Login

template tag for highlighting currently active page

Author:
adunar
Posted:
October 23, 2008
Language:
Python
Version:
1.0
Score:
4 (after 6 ratings)

This module defines a template tag {% ifactive %} that lets you determine which page is currently active. A common use case is for highlighting the current page in a navigation menu.

It uses the same syntax for specifying views as the {% url %} tag, and determines whether a particular page is active by checking if the same view is called with the same arguments, instead of just comparing URLs. As a result, it can handle cases where different URLs map to the same view.

Example usage:

<a {% ifactive request page1 %}class='active'{% endifactive %} 
          href='{% url page1 %}'>Page 1</a>
<a {% ifactive request page2 %}class='active'{% endifactive %} 
          href='{% url page2 %}'>Page 2</a>
... 
<a {% ifactive request pageN %}class='active'{% endifactive %} 
          href='{% url pageN %}'>Page N</a>

It also can be extended to further reduce the amount of repetitive code. For instance, you could write a template tag that has class='active' as the block contents, and always gets the variable from context['request']:

def do_activeif(parser, token):
    "e.g. <a {% activeif page1 %} href='{% url page1 %}'>Page 1</a>"
    tag_args = token.contents.split(' ')
    view_name = tag_args[1]
    args, kwargs = _parse_url_args(parser, tag_args[2:])
    return ActiveNode('request', view_name, args, kwargs, NodeList(TextNode('class="active"')))
register.tag('activeif', do_activeif)

Note that you will have to add the ActiveViewMiddleware to your settings.py:

MIDDLEWARE_CLASSES = (
    ...,
   'path.to.this.module.ActiveViewMiddleware'
)

You may also want to add this template tag as a built-in, so you don't have to call {% load %}. In somewhere that's imported by default (e.g. where you define your views), add:

from django import template
template.add_to_builtins('path.to.this.module')
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from django.utils.encoding import smart_str
from django.core.urlresolvers import get_callable, RegexURLResolver, get_resolver
from django.template import Node, NodeList, TextNode, TemplateSyntaxError, Library, resolve_variable
from django.conf import settings

register = Library()

def do_ifactive(parser, token):
    """
    Defines a conditional block tag, "ifactive" that switches based on whether the active request 
    is being handled by a particular view (with optional args and kwargs).
    
    Has the form:
    
    {% ifactive request path.to.view %}
        [Block to render if path.to.view is the active view]
    {% else %}
        [Block to render if path.to.view is not the active view]
    {% endifactive %}

    'request' is a context variable expression which resolves to the HttpRequest object for the current
    request. (Additionally, the ActiveViewMiddleware must be installed for this to work.)

    'path.to.view' can be a string with a python import path (which must be mentioned in the urlconf),
    or a name of a urlpattern (i.e., same as the argument to the {% url %} tag).
    
    You can also pass arguments or keyword arguments in the same form as accepted by the {% url %} tag,
    e.g.:
    
    {% ifactive request path.to.view var1="bar",var2=var.prop %}...{% endifactive %}   
    
    or:
    
    {% ifactive request path.to.view "bar",var.prop %}...{% endifactive %}   
    
    The else block is optional.
    """

    end_tag = 'endifactive'

    active_nodes = parser.parse((end_tag,'else'))    
    end_token = parser.next_token()
    if end_token.contents == 'else':
        inactive_nodes = parser.parse((end_tag,))
        parser.delete_first_token()
    else:
        inactive_nodes = None

    tag_args = token.contents.split(' ')
    if len(tag_args) < 3:
        raise TemplateSyntaxError("'%s' takes at least two arguments"
                                  " (context variable with the request, and path to a view)" % tag_args[0])
    
    request_var = tag_args[1]
    view_name = tag_args[2]
    args, kwargs = _parse_url_args(parser, tag_args[3:])
    
    return ActiveNode(request_var, view_name, args, kwargs, active_nodes, inactive_nodes)
       
register.tag('ifactive', do_ifactive)

class ActiveNode(Node):
    
    def __init__(self, request_var, view_name, args, kwargs, active_nodes, inactive_nodes=None):
        self.request_var = request_var
        self.view_name = view_name
        self.args = args
        self.kwargs = kwargs
        self.active_nodes = active_nodes
        self.inactive_nodes = inactive_nodes
    
    def render(self, context):        

        request = resolve_variable(self.request_var, context)

        view, default_args = _get_view_and_default_args(self.view_name)        
                
        if getattr(request, '_view_func', None) is view:        
            
            resolved_args = [arg.resolve(context) for arg in self.args]                                               
            if request._view_args == resolved_args:
            
                resolved_kwargs = dict([(k, v.resolve(context)) for k, v in self.kwargs.items()])
                resolved_kwargs.update(default_args)
                                               
                if request._view_kwargs == resolved_kwargs:
                    return self.active_nodes.render(context)
        
        if self.inactive_nodes is not None:    
            return self.inactive_nodes.render(context)
        else:
            return ''

def _get_patterns_map(resolver, default_args=None):
    """
    Recursively generates a map of 
    (pattern name or path to view function) -> (view function, default args)    
    """

    patterns_map = {}
    
    if default_args is None:
        default_args = {}

    for pattern in resolver.url_patterns:
    
        pattern_args = default_args.copy()
    
        if isinstance(pattern, RegexURLResolver):
            pattern_args.update(pattern.default_kwargs)
            patterns_map.update(_get_patterns_map(pattern, pattern_args))
        else:
            pattern_args.update(pattern.default_args)
        
            if pattern.name is not None:
                patterns_map[pattern.name] = (pattern.callback, pattern_args)
            
            # HACK: Accessing private attribute of RegexURLPattern
            callback_str = getattr(pattern, '_callback_str', None)
            if callback_str is not None:    
                patterns_map[pattern._callback_str] = (pattern.callback, pattern_args)
        
    return patterns_map    

_view_name_cache = None

def _get_view_and_default_args(view_name):
    """
    Given view_name (a path to a view or a name of a urlpattern,
    returns the view function and a dict containing any default kwargs
    that are specified in the urlconf for that view.
    """

    global _view_name_cache
    
    if _view_name_cache is None:
        _view_name_cache = _get_patterns_map(get_resolver(None))               
        
    try:
        return _view_name_cache[view_name]
    except KeyError:
        raise KeyError("%s does not match any urlpatterns" % view_name)   
    
def _parse_url_args(parser, bits):
    """
    Parses URL parameters in the same way as the {% url %} tag.    
    """

    args = []
    kwargs = {}
    
    for bit in bits:
        for arg in bit.split(","):
            if '=' in arg:
                k, v = arg.split('=', 1)
                k = k.strip()
                kwargs[smart_str(k,'ascii')] = parser.compile_filter(v)
            elif arg:
                args.append(parser.compile_filter(arg))
    
    return args, kwargs
                        

class ActiveViewMiddleware(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        """
        Records the view function used on this request and its *args and **kwargs.
        Needed by {% ifactive %} to determine if a particular view is currently active.
        """
        request._view_func = view_func
        request._view_args = list(view_args)
        request._view_kwargs = view_kwargs

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

Please login first before commenting.