URL'ки в Django переводятся в вызовы обработчиков через набор регулярных выражений. Это делает возможным выбирать вид URL'ов, который вам нравится, измененять структуру URL'ов, не трогая код приложений, и писать переносимые приложения, которые можно подключать внутрь структуры URL'ов других сайтов.

На старости лет я найду себе работу, где надо будет писать привлекательные пресс-релизы! Но пока я не буду делать вида, что не знаю о проблеме, с которой сталкиваются практически все джанговцы, и которая сводит на нет два последних пункта из трех в предыдущем абзаце. Поскольку в шаблонах приходится выводить ссылки на другие страницы системы, получается, что приложение таки должно знать структуру URL.

Проблема эта в Django не имела хорошего решения, но имела некие обходные пути. И вот какое-то время назад Adrian реализовал штуку под названием "reverse url lookup", призванную вопрос решить. Сегодня у меня дошли руки ее попробовать, и она работает! И это круто! Но обо всем по порядку...

Я думаю, это всё читают не только пользователи Джанго, но и те, кто к нему еще присматривается, поэтому я продемонстрирую проблему на примере. Если же эта проблема вам более чем знакома, то можно пропустить этот и следующий раздел, а перейти сразу к сути.

Пример проблемы

Предположим, что у нас есть приложение — форум — с такой структурой URL'ов:

/forums/ Список форумов
/forums/1/ Конкретный форум
/forums/1/post/1/ Конкретный пост в форуме

И теперь представим, что в шаблоне поста надо сделать ссылку на индекс форума. Проблема заключается в том, что написать просто "/forums/" нельзя, потому что это ссылка в корень сайта, и этим тут же убивается возможность поставить форум не в корень, а по какому-нибудь пути.

Традиционные подходы

Первый подход — относительные ссылки. Ссылку с поста на корень форума можно поставить как "../../../". Но часто бывает, что один и тот же шаблон может выводиться в разных местах сайта, включаться в другие шаблоны, и относительный путь будет другим. И еще так нельзя поставить ссылку на другое приложение, потому что оно тоже может быть расположено где-то в произвольном месте сайта.

Другой подход, стандартный для Джанго — это соглашение прописывать моделям метод get_absolute_url(), который возвращает URL страницы с отображением этой модели на сайте. Переносимость приложений в это случае делается хаком с переопределением URL'ов в настройке ABSOLUTE_URL_OVERRIDES. Минусы у этого подхода видны тут же:

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

Обратное разрешение URL

Теперь появилось, наконец, правильное решение. Идея его строится на том, что раз выводом страницы занимается view, то view — это и есть уникальный идентификатор страницы внутри проекта. И раз у нас есть маппинг между URL'ами и view, то было бы неплохо сделать обратную операцию: по view восстановить, какой URL его вызывает. Это и было реализовано.

Внимание! Этот код появился недавно, не отражен в официальной документации и еще будет дописываться. Но вообще, там не дописаны всякие экзотические случаи, поэтому для реальной работы он уже пригоден.

Как это работает, проще всего видеть на примере. Возьмем такую строчку в urlconf:

(r'^client/(\d+)/$', 'project_name.app_name.views.client')

Получение URL на страницу конкретного клиента выглядит так:

from django.core.urlresolvers import reverse
url = reverse('project_name.app_name.views.client', args=[5])

Пара пояснений:

Шаблонный тег

Функция reverse сама по себе не очень полезна, потому что ссылки делаются в шаблонах, а никакого шаблонного тега пока в Django для этого нет. И всю эту статью я пишу как раз потому, что сегодня решил попробовать такой тег написать, и он как-то очень быстро и неплохо получился. Вот мой рабочий вариант, им уже вполне можно пользоваться:

class URLNode(template.Node):
  def __init__(self, view_name, args):
    self.view_name = view_name
    self.args = args

  def render(self, context):
    from django.core.urlresolvers import reverse, NoReverseMatch
    args = [arg.resolve(context) for arg in self.args]
    project_name = settings.SETTINGS_MODULE.split('.')[0]
    try:
      return reverse(project_name + '.' + self.view_name, args=args)
    except NoReverseMatch:
      return ''

@register.tag
def url(parser, token):
  bits = token.contents.split()
  if len(bits) > 2:
    args = [parser.compile_filter(arg) for arg in bits[2].split(',')]
  else:
    args = []
  return URLNode(bits[1], args)

Вызывается примерно так:

{% url app_name.views.search %}
{% url app_name.views.artist artist.id %}
{% url app_name.views.profile "some_username" %}
{% url app_name.views.comments article.id,comment.id %}

Код написан очень быстро и над синтаксисом я очень долго не размышлял, поэтому сразу назову несколько недоработок (некритичных, на мой взгляд):

Вот. Надеюсь, будет полезно. Если кто это красиво допишет, не поленитесь поделиться!

Комментарии: 22

  1. pythy

    По-хорошему, надо бы, чтобы примеры в разделе 'Пример проблемы' и 'Обратное разрешение URL' касались одной темы: или клиентов, или форумов.

  2. dp_wiz

    Нифига не понял. Ждём документации... (:

  3. ilya

    огромное спасибо за обзор этой фичи!
    у меня нет времени постоянно читать все обновления
    а это как раз то чего очень давно хотелось!
    попробую реализовать у себя и может чего интересного в комментах напишу

  4. Давид Мзареулян

    Вопрос такой. В случае с (r'^client/(\d+)/$', 'project_name.app_name.views.client') всё понятно и просто, параметр просто подставляется вместо скобки. А как быть с чем-нибудь вроде r'^abc/de*f/(\d+)/$'? Сумеет ли этот модуль правильно отработать звёздочку? Или, скажем, вложенные скобки?

  5. Давид Мзареулян

    Прошу прощения, со звёздочкой я, конечно, ерунду написал, там однозначный реверс вообще невозможен.

    Но вообще, регекспами ведь можно очень хитрые вещи задавать. Справится? Или подразумевается, что «правильные» урлы должны задаваться простым образом, как последовательность отдельных блоков-парамеров?

  6. Иван Сагалаев

    Да, эта штука сделана с полным осознанием того, что она математически все случаи не покроет, но она работает в большинстве реальных ситуаций.

  7. xavier

    Hello there !

    Wery useful tip, even if I could'nt undrstand the body of your post, your piece of code helped me a lot :)

    Kind regards

    xav

  8. Alexander Solovyov

    Почему-то абсолютно не получается заиспользовать. :/

    Пишет:

    Caught an exception while rendering: Tried reset in module forum.views. Error was: 'module' object has no attribute 'reset'

    Что бы это могло быть? :|

  9. Иван Сагалаев

    Стандартный совет: Django обновить до самого нового. Если не поможет - с подробным traceback'ом в форуме можно разобраться: http://softwaremaniacs.org/forum/viewforum.php?id=5

  10. adarovsky

    Вот версия, которая позволяет использовать переменные в регексах:

    from django import template
    from django.conf import settings
    import re
    
    register = template.Library()
    
    class URLNode(template.Node):
        def __init__(self, view_name, args, kwargs):
            self.view_name = view_name
            self.args = args
            self.kwargs = kwargs
    
        def render(self, context):
            from django.core.urlresolvers import reverse, NoReverseMatch
            args = [arg.resolve(context) for arg in self.args]
            kwargs = dict( map( lambda (k, v): (k, v.resolve(context)), self.kwargs.iteritems() ) )
            project_name = settings.SETTINGS_MODULE.split('.')[0]
            try:
                return reverse(self.view_name, args=args, kwargs=kwargs)
            except NoReverseMatch:
                return ''
    
    @register.tag
    def url(parser, token):
        bits = token.contents.split()
        args = []
        kwargs = {}
        if len(bits) > 2:
            rEq = re.compile(r'(^\w+)=(.*)')
            for arg in bits[2].split(','):
                m = rEq.match(arg)
                if m:
                kwargs[m.group(1)] = parser.compile_filter(m.group(2))
                else:
                    args += parser.compile_filter(arg)
    
        return URLNode(bits[1], args, kwargs)
    
    from django import template
    from django.conf import settings
    import re
    
    register = template.Library()
    
    class URLNode(template.Node):
        def __init__(self, view_name, args, kwargs):
            self.view_name = view_name
            self.args = args
            self.kwargs = kwargs
    
        def render(self, context):
            from django.core.urlresolvers import reverse, NoReverseMatch
            args = [arg.resolve(context) for arg in self.args]
            kwargs = dict( map( lambda (k, v): (k, v.resolve(context)), self.kwargs.iteritems() ) )
            project_name = settings.SETTINGS_MODULE.split('.')[0]
            try:
                return reverse(self.view_name, args=args, kwargs=kwargs)
            except NoReverseMatch:
                return ''
    
    @register.tag
    def url(parser, token):
        bits = token.contents.split()
        args = []
        kwargs = {}
        if len(bits) > 2:
            rEq = re.compile(r'(^\w+)=(.*)')
            for arg in bits[2].split(','):
                m = rEq.match(arg)
                if m:
                kwargs[m.group(1)] = parser.compile_filter(m.group(2))
                else:
                    args += parser.compile_filter(arg)
    
        return URLNode(bits[1], args, kwargs)
    
  11. Иван Сагалаев

    Спасибо.

    Правда, в Django'вском trac'е довольно давно уже лежит сильно переработанная версия (моя же), которая тоже позволяет именные аргументы. Она слегка другая, конечно...

  12. adarovsky

    Правда, в Django’вском trac’е довольно давно уже лежит сильно переработанная версия (моя же), которая тоже позволяет именные аргументы. Она слегка другая, конечно…

    слона-то я и не заметил, и, как всегда, со своим велосипедом :)

  13. [...] и обратный резолвинг URL в шаблонах есть. И приведут пример. Да, есть способ, однако в стандартной библиотеке [...]

  14. hidded

    Хм... а как можно использовать {% url ... %} при использовании стандартных views.

  15. Иван Сагалаев

    Generic'ов? Пока никак, к сожалению. У меня есть пара мыслей на этот счет, но пока руки не доходят их сформулировать и в патч превратить. В качестве кривого workaround'а могу предложить завернуть generic'и в свои wrapper'ы буквально так:

    def someobject_list(*args, **kwargs):
      return object_list(*args, **kwargs)
    

    ... и ссылаться на них, потому что у них уже будут разные имена.

  16. Alexander Solovyov

    Есть вопросик на тему работы реверсинга - а почему он требует contrib.admin? Просто тут такое дело, что хочу обойтись без contrib.sessions, ну и подумываю, что явное его отключение - безопаснее, чем просто слежение за собой. ;)

    Вот и думаю, для чего ему админка, и как бы это перебороть...

  17. Иван Сагалаев

    Ему, вообще-то, и не нужна админка. Я подозреваю, что это эффект того, что она где-то в urlconf'ах присутствует, и в процессе поиска reverse через этот паттерн проходит и пытается ее грузить. У меня тоже так один раз было: вычистил пару правил от несуществующих приложений :-)

  18. Alexander Solovyov

    Ааа! Вот это я дал маху! ;))

    Действительно, в урлконфе админка осталась. :D LOL!

    Так а на тему скорости, никаких испытаний не было? ;)

  19. Иван Сагалаев

    Замеров не знаю... Но по идее, это должны быть сущие копейки.

  20. Nick

    Тег {% url your_view %} уже в джанге svn. Наслаждайтесь :)

  21. Alexander Solovyov

    Тег {% url your_view %} уже в джанге svn. Наслаждайтесь :)

    Ну и боян! :]

  22. [...] Во многом помогла уже довольно старая статья http://softwaremaniacs.org/blog/2006/08/04/url-reverse/ [...]

Добавить комментарий