Навигация в шаблонах Django

    Наверно в каждом проекте есть система навигации — пользователи кликают по ссылкам, менюшкам и нам(разработчикам\дизайнерам\верстальщикам) надо как-то «подсвечивать» страницу\ссылку на которой сейчас находится пользователь.

    Предоставляю не тривиальное решение очень тривиальной задачи при разработки навигации в Django проектах.

    Для не терпеливых можно сразу взглянуть на проект на github.

    Самое тривиальное решение


    В каждой вьюхе в контексте передаем некую переменную, пусть будет «location» и потом беспощадно хардкодим:

    <ul>
      <li {% if location=='random_page' %} class="active" {% endif %}>
        <a href="{% url 'random_page' %}">
          {% trans 'random page' %}
        </a>
      </li>
    </ul>
    

    Получается — к каждой ссылки уникальное значение «location», что порождает очень много лишнего кода, когда ссылок\вьюх становится много практически не возможно запомнить какой «location» к какой ссылке, а так же верстальщик может быть крайне не доволен необходимостью вникать в логику питоновских вьюх и какое значение от куда попадает в контекст.

    Можно передавать «location» такой же как именная ссылка данной вьюхи и навигация станет слегка наглядней.

    Реализация через простой «template filter»


    собственно фильтр:

    from django import template
    
    
    register = template.Library()
    
    
    def active(url, request):
        if url == request.get_full_path():
            return True
        else:
            return False
    
    
    register.filter('active', active)
    

    дальше в шаблоне:

    {% load active %}
    
    <ul>
      {% url 'random_page' as random_page %}
      <li {% if random_page|active:request %} class="active" {% endif %}>
        <a href="{{ random_page }}">
          {% trans 'random page' %}
        </a>
      </li>
    </ul>
    

    Передавать переменные с вьюх уже не нужно, можно заменить сравнение на .startswith() или вовсе на регулярки и добавить поддержу меню, например светить ссылку на "/menu/" когда пользователь находится на странице "/menu/submenu/". Но рутина никуда не делась…

    Мои велосипеды — django-activeurl


    Рутинные действия напрочь отсутствуют, а именно:

    {% load activeurl %}
    
    {% activeurl %}
      <ul>
        <li>
          <a href="{% url 'random_page' %}">
            {% trans 'random page' %}
          </a>
        </li>
      </ul>
    {% endactiveurl %}
    

    немного магии…

    <ul>
      <li class="active">
        <a href="/random_page/">
          random page
        </a>
      </li>
    </ul>
    

    Установка как и у всех питоновских батареек:

    pip install django-activeurl
    

    Все нюансы описаны тут.

    Теперь, немного о том как всё это работает — из переданного html с помощью lxml строится дерево, в нем идет поиск HTML элементов которые надо подсветить(по умолчанию «li»), потом в этом тэге берутся все ссылки и их параметр «href» сравнивается с «request.get_full_path()».

    Реализованные фичи:


    Поменьше всем рутинных задач при разработке!
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 11
    • +2
      ИМХО, корректнее реализовывать данную задачу следующим образом:

      {% url 'menu_1' as menu_url_1 %}
      {% url 'menu_2' as menu_url_2 %}
      {% ifequal request.path menu_url_1 %}menu 1 is active{% endifequal %}
      {% ifequal request.path menu_url_2 %}menu 2 is active{% endifequal %}
      

      Разумеется не забываем подключить django.core.context_processors.request
      • +1
        На дворе django 1.6, а вы до сих пор пользуетесь ifequal? Серьезно?

        Заодно вот такой велосипед:
        _urls = (
            (reverse_lazy('app1.index'), u'App 1'),
            (reverse_lazy('app2.index'), u'App 2'),
            (reverse_lazy('app3.index'), u'App 3'),
        )
        
        @register.inclusion_tag('menu.html', takes_context=True)
        def navigation_menu(context):
            request = context['request']
        
            urls = []
            full_path = request.get_full_path()
            for href, name in _urls:
                is_current = full_path.startswith(href)
                urls.append([href, name, is_current])
        
            return {
                'urls': urls,
            }
        

        • 0
          На дворе django 1.6, а вы до сих пор пользуетесь ifequal? Серьезно?

          Я знаю про {% if a == b %}, но вот момент когда ifequal признан устаревшим, честно говоря, я пропустил, тем более что в dev версии доков об этом ни слова (разве что упомянуто об эквивалентности этих записей и всё). Так что дело тут просто в привычке, хотя да, через if запись будет получаться на несколько символов короче.
      • –1
        Я делаю так:
        templatetags/navigation.py
        import re
        from django import template
        
        register = template.Library()
        
        @register.simple_tag
        def active(request, pattern):
            if re.search(pattern, request.path):
                return 'active'
            return ''
        


        template.html
        {% load navigation %}
        ... class="first {% active request "^/url1/" %}" ...
        ... class="{% active request "^/url2/" %}" ...
        
        • +3
          Хм. Парсить хтмл, регулярками находить нужный пункт навигации ради того чтобы его подсветить… Вам не кажется что это оверкил?! Я правда вот так сразу не предложу простого и универсального решения. Ваш вариант я пожалуй применял бы только в случае когда менюшки автоматически генерируются.

          Когдато давно в своем проектике я решил эту проблему на уровне наследования в шаблонах. В главном шаблоне
          ...
          <li class="{%block about%}{%endblock%}"><a href="/">О проекте</a></li>
          <li class="{%block search%}{%endblock%}"><a href="/search">Поиск</a></li>
          <li class="{%block contacts%}{%endblock%}"><a href="/contact">Обратная связь</a></li>
          ...
          

          а в шаблонах отдельных страниц просто доблял
          {%block contacts%} active{%endblock%}

          Тупое решение в лоб, но для небольшого проекта в самый раз. Сейчас я бы поискал более изящное решение.
          • 0
            Неужели никто не использует django-treenav?
            • 0
              Ну не всем же нужно древовидное меню и django-mptt:

              Requirements

              django-mptt >= 0.5.2
            • 0
              Более сложный вариант. Пользуюсь им уже года два

              import re
              from django import template
              register = template.Library()
              
              @register.simple_tag
              def active_tag(request, patterns, out='selected'):
                  """
                  usage:
                      <a class="url{% active_tag request "default /home/" %}" href="#">url item 1</a>
                      -> <a class="url seleced">url item 1</a>
              
                      <a class="url{% active_tag request "/posts/ /allposts/" "custom-css" %}" href="#">url item 2</a>
                      -> <a class="url custom-css">url item 2</a>
                      
                      {% active_tag request '^/account/-/(\w+)/$ ^/account/-/(\w+)/edit/$ ^/account/-/(\w+)/password/$' 'class="selected"'%}
                      
                      {% active_tag request obj.get_absolute_url %}
                  """
                  if "default" in patterns.split() and request.path == '/':
                      return " %s" % out
                  else:
                      return " %s" % out if len([p for p in patterns.split() 
                                          if re.search(p, request.path) ]) else ''
              
              • 0
                0) таки тянет на оверкилл
                1) а как поступать со вложенной навигацией и вообще случаями менее очевидными, чем request.url === page.url?
                2) свой велосипед — хорошо, но не стоило бы начать с обзора существующих? в том числе и эту статью…
                • 0
                  Я пользуюсь menus из django-cms
                  docs.django-cms.org/en/2.4.3/getting_started/navigation.html
                  • 0
                    А почему все так любят привязываться к конкретному URL, типа {% active request "^/url1/" %}, забывая про reverse()? Хардкодинг URL — зло.

                    Вот и мой двухколёсный друг:
                    @register.filter
                    def is_current_page(request, param):
                        params = param.split(',')
                        name = params[0]
                        args, kwargs, as_var = parse_args_and_kwargs(params[1:])
                        # Do not mix args and kwargs in reverse() - it is forbidden!
                        if args:
                            return request.path == reverse(name, args=args)
                        elif kwargs:
                            return request.path == reverse(name, kwargs=kwargs)
                        else:
                            return request.path == reverse(name)
                    


                    В шаблоне:
                    {% if request|is_current_page:'shop/product,id=1' %} active {% endif %}
                    


                    Как уже говорили выше, парсить собственноручно написанный шаблон (не какой-то там чужой сайт!) — это точно оверкилл. Напишите шаблон так, чтоб не парсить.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.