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 в модели вообще очень странная, потому что модель со структурой ссылок никак не связана
- так можно задать только URL'ы страниц моделей, URL произвольного view (например формы поиска) так не задашь
В общем-то, сами джанговцы тоже всегда говорили, что архитектурно эта штука кривая. Но единственный ее плюс, из-за которого она и использовалась — это то, что она очень простая и работает в очень многих реальных случаях.
Обратное разрешение 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 корректно отрабатывает включение urlconf'ов друг в друга, url будет содержать текущий абсолютный путь от корня сайта
- параметры
args
иkwargs
(второй в примере не показан) содержат значения, которые будут подставлены на места соответствующих групп в регулярке
Шаблонный тег
Функция 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 %}
Код написан очень быстро и над синтаксисом я очень долго не размышлял, поэтому сразу назову несколько недоработок (некритичных, на мой взгляд):
- поддерживаются только позиционные аргументы, поэтому если URL'ы параметризованы по именам (
^client/(?P<id>\d+)/$
), то это, наверное, работать не будет - список параметров должен идти через запятую без пробелов
Вот. Надеюсь, будет полезно. Если кто это красиво допишет, не поленитесь поделиться!
Комментарии: 22
По-хорошему, надо бы, чтобы примеры в разделе 'Пример проблемы' и 'Обратное разрешение URL' касались одной темы: или клиентов, или форумов.
Нифига не понял. Ждём документации... (:
огромное спасибо за обзор этой фичи!
у меня нет времени постоянно читать все обновления
а это как раз то чего очень давно хотелось!
попробую реализовать у себя и может чего интересного в комментах напишу
Вопрос такой. В случае с
(r'^client/(\d+)/$', 'project_name.app_name.views.client')
всё понятно и просто, параметр просто подставляется вместо скобки. А как быть с чем-нибудь вродеr'^abc/de*f/(\d+)/$'
? Сумеет ли этот модуль правильно отработать звёздочку? Или, скажем, вложенные скобки?Прошу прощения, со звёздочкой я, конечно, ерунду написал, там однозначный реверс вообще невозможен.
Но вообще, регекспами ведь можно очень хитрые вещи задавать. Справится? Или подразумевается, что «правильные» урлы должны задаваться простым образом, как последовательность отдельных блоков-парамеров?
Да, эта штука сделана с полным осознанием того, что она математически все случаи не покроет, но она работает в большинстве реальных ситуаций.
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
Почему-то абсолютно не получается заиспользовать. :/
Пишет:
Caught an exception while rendering: Tried reset in module forum.views. Error was: 'module' object has no attribute 'reset'
Что бы это могло быть? :|
Стандартный совет: Django обновить до самого нового. Если не поможет - с подробным traceback'ом в форуме можно разобраться: http://softwaremaniacs.org/forum/viewforum.php?id=5
Вот версия, которая позволяет использовать переменные в регексах:
Спасибо.
Правда, в Django'вском trac'е довольно давно уже лежит сильно переработанная версия (моя же), которая тоже позволяет именные аргументы. Она слегка другая, конечно...
слона-то я и не заметил, и, как всегда, со своим велосипедом :)
Хм... а как можно использовать {% url ... %} при использовании стандартных views.
Generic'ов? Пока никак, к сожалению. У меня есть пара мыслей на этот счет, но пока руки не доходят их сформулировать и в патч превратить. В качестве кривого workaround'а могу предложить завернуть generic'и в свои wrapper'ы буквально так:
... и ссылаться на них, потому что у них уже будут разные имена.
Есть вопросик на тему работы реверсинга - а почему он требует contrib.admin? Просто тут такое дело, что хочу обойтись без contrib.sessions, ну и подумываю, что явное его отключение - безопаснее, чем просто слежение за собой. ;)
Вот и думаю, для чего ему админка, и как бы это перебороть...
Ему, вообще-то, и не нужна админка. Я подозреваю, что это эффект того, что она где-то в urlconf'ах присутствует, и в процессе поиска reverse через этот паттерн проходит и пытается ее грузить. У меня тоже так один раз было: вычистил пару правил от несуществующих приложений :-)
Ааа! Вот это я дал маху! ;))
Действительно, в урлконфе админка осталась. :D LOL!
Так а на тему скорости, никаких испытаний не было? ;)
Замеров не знаю... Но по идее, это должны быть сущие копейки.
Тег {% url your_view %} уже в джанге svn. Наслаждайтесь :)
Ну и боян! :]