Active links in Django

Django. Django. Nog eens? Django! Het is een geweldig framework, maar bij ieder framework zal je moeten vaststellen dat je wat mist.

Het probleem

De link voor de huidige pagina moet verdwijnen of er tenminste anders uitzien. Django komt niet met een directe oplossing en de meest relevante andere oplossing werkt met hardcoded paths. De beste oplossing zou zijn om met named URLs te werken.

Stel je voor dat dit de URL patterns zijn:


urlpatterns = patterns('',
    url(r'^archive/(?P\d{4})/(?P\d{2})/(?P\d{2})/$’,
        ‘proj.app.views.archive_for_a_day’,
        name=’proj_app_archive_for_a_day’),
    url(r’^search/$’,
        ‘proj.app.views.search’,
        name=’proj_app_search’),
)

In mijn menu wil ik linken naar het archief voor een bepaalde dag en de zoekpagina. Maar als ik op één van die pagina’s ben wil ik dat het list item met de overeenkomstige link de class active krijgt. Zoiets:


<ul>
    <li{{ request|on_active_link:"proj_app_archive_for_a_day,active" }}>
        <a href="{%url proj_app_archive_for_a_day year,month,day %}">Archive</a>
    </li>
    <li{{ request|on_active_link:"proj_app_search,active" }}>
        <a href="{%url proj_app_search %}">Search</a>
    </li>
</ul>

De oplossing

Die volgt uit 2 eenvoudige template filters:

  • matches_url_pattern zal True teruggeven bij een match, waardoor die kan gebruikt worden met een {% if %} om de link te verwijderen
  • on_active_link, uit bovenstaand voorbeeld, zal het tweede argument gebruiken als een class indien de naam van de URL pattern (het eerste argument) overeenkomt met het request path.

from django import template

register = template.Library()

@register.filter(name='matches_url_pattern')
def matches_url_pattern(value,args):
    return is_path_a_match_for_named_url(value, args)

@register.filter(name='on_active_link')
def on_active_link(value,args):
    parts = args.split(',')
    if is_path_a_match_for_named_url(value, parts[0]):
        return ' class="%s"' % parts[1]
    return ''

def is_path_a_match_for_named_url(request, named_url):
    from django.core.urlresolvers import get_resolver, Resolver404
    resolver = get_resolver(None)
    path = request.path
    tried = []
    match = resolver.regex.search(path)
    if match:
        new_path = path[match.end():]
        for pattern in resolver.urlconf_module.urlpatterns:
            try:
                sub_match = pattern.resolve(new_path)
            except Resolver404, e:
                tried.extend([(pattern.regex.pattern + '   ' + t) for t in e.args[0]['tried']])
            else:
                if sub_match:
                    return pattern.name == named_url
    return False

De laatste functie vergt het meeste uitleg. is_path_a_match_for_named_url gebruikt code die overgenomen is van de RegexURLResolver. Deze kan bepalen welk URL pattern met het huidige request path overeenkomt, maar biedt in de huidige versie geen mogelijkheid om de naam van dat pattern te achterhalen. Vandaar deze kopie die wel kan checken of de naam van het gevonden pattern overeenstemt met de gegeven parameter.

Het resultaat

Stel nu dat ik ben beland op /search/. De HTML zal er als volgt uitzien:


<ul>
    <li>
        <a href="/archive/2008/07/21/">Archive</a>
    </li>
    <li class="active">
        <a href="/search/">Search</a>
    </li>
</ul>

Een mooiere oplossing voor dit probleem zal de filters vervangen door tags en geen expliciete verwijzing naar de request variabele nodig hebben.

Dit artikel werd opgenomen in ontwikkeling.