Django Book: третье представление — динамические URL-ы

Автор: | 02/07/2015
 

django_logo_2Предыдущая часть.

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

Давайте создадим третье представление, которое будет отображать время и дату — но со смещением в несколько часов. Наша цель — создать веб-приложение, в котором страница /time/plus/1/ отображала бы время и дату со смещением на один час вперёд, страница /time/plus/2/ — на два часа, и так дале.

Можно было бы создать несколько функций для каждого смещения времени, и для каждой создать новую запись в URLconf-е, что-то вроде такого:

urlpatterns = patterns('',
    url(r'^time/$', current_datetime),
    url(r'^time/plus/1/$', one_hour_ahead),
    url(r'^time/plus/2/$', two_hours_ahead),
    url(r'^time/plus/3/$', three_hours_ahead),
    url(r'^time/plus/4/$', four_hours_ahead),
)

Очевидно, что такой подход имеет недостатки. Мало того, что мы создаём лишние представления, но кроме того — мы ограничены несколькиими заранее определёнными периодами для смещения времени — 1, 2, 3 и 4 часа. Если мы захотим создать страницу, которая бы выводила время на 5 часов вперёд — нам придётся создавать новую функцию и добавлять строку в URLconf.

Кратко об URL-ах

Если вы ранее занимались разработкой под другие платформы, такие как PHP или Java, вы могли бы подумать «Давайте просто используем строку параметров в запросе!» — что-то вроде такого: /time/plus?hours=3, в котором количество часов передавался бы параметром hours прямо в URL-е.

Вы можете сделать это и с Django (мы рассмотрим такую возможность позже), но одна из ключевых особенностей философии Django — это «URL должен быть прекрасен». URL в виде /time/plus/3/  намного проще, более читаемый, его легче передать кому-то. Приятный URL — это показатель качества вашего приложения.

Модуль URLconf в Django поддерживает создание таких URL-ов, поэтому лучше пользоваться такой возможностью и использовать красивые адреса.

Итак, как нам настроить наше приложение, что бы обрабатывать произвольное смещение времени? Ключевым решением является использование групповых символов в URLpattern. Как мы уже упоминали, URLpattern — это регулярное выражение, поэтому — мы можем использовать регулярные выражения вида 'd+' что бы получить один или более цифр в URL-е:

urlpatterns = patterns('',
    # ...
    url(r'^time/plus/d+/$', hours_ahead),
    # ...
)

(мы удалили весь предыдущий код, заменив его символами #)

Такой URLpattern будет перехватывать все адреса вида /time/plus/2//time/plus/25/ и даже /time/plus/100000000000/. Мы можем изменить его, что бы установить ограничение в 99 часов. Что бы реализовать это — нам необходимо установить лимит от одной до двух цифр в регулярном выражении, которое будет выглядеть как 'd{1,2}':

url(r'^time/plus/d{1,2}/$', hours_ahead),

Примечание

При разработке веб-приложения — всегда лучше предполагать получение самых необычных данных, и заранее решить — должно или нет приложение их обабатывать. Мы тут решили ограничить здесь такие данные 99-ю часами.

Теперь, когда мы создали групповой символ для URL-а, нам необходимо передать полученные из него данные в функцию представления, что бы мы могли использовать одну функцию для любого смещения времени. Мы можем сделать это разместив скобки () вокруг данных в URLpattern. В случае с нашим примером — мы хотим передать цифры, которые были переданы в URL-е, поэтому изменим наш код:

url(r'^time/plus/(d{1,2})/$', hours_ahead),

Если вы знакомы с регулярными выражениями — то в URLpattern вы будете себя чувствовать как дома. Мы используем скобки, что бы захватить текст, который попадает под шаблон.

В результате, мы получим такой URLconf, включая наши предыдущие представления:

from django.conf.urls.defaults import patterns, include, url
from example.views import hello, current_datetime, hours_ahead

urlpatterns = patterns('',
    url(r'^$', hello),
    url(r'^hello/$', hello),
    url(r'^time/$', current_datetime),
    url(r'^another-time-page/$', current_datetime),
    url(r'^time/plus/(d{1,2})/$', hours_ahead),
)

С этим мы разобрались, теперь время заняться представлением hours_ahead.

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

def hours_ahead(request, offset):
    try:
        offset = int(offset)
    except ValueError:
        raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)

А поностью файл views.py — так:

from django.http import HttpResponse, Http404
import datetime

def hello(request):
    return HttpResponse("Hello world")

def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

def hours_ahead(request, offset):
    try:
        offset = int(offset)
    except ValueError:
        raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)

Давайте пройдемся построчно по коду функции hours_ahead:

  • функция hours_ahead принимает два аргумента — request и offset:

    • request это объект HttpRequest , точно такой же, как в наших примерах с hello и current_datetime. Повторим — каждое представление всегда должно принимать объект HttpRequest  в качестве первого аргумента;
    • offset — строка, захваченная скобками в URLpattern. Например, если переданный URL будет /time/plus/3/ — тогда offset будет строкой '3'. Если URL будет /time/plus/21/ — то offset будет '21'. Помните, что полученные из URLpattern данные всегда будут строкового типа, даже если они состоят из цифр;(технически — эти данные всегда будут объектом Unicode, а не Python-строкой, но пока об этом не беспокойтесь)Мы решли иназвать переменную offset — но вы можете назвать её любым другим именем, главное — что бы это был второй аргумент функции, после request. Так же — можно использовать ключевые слова вместо аргументов функции, но это мы рассмотрим позже).
  • Первое действие нашей функции — это вызов int() для переменной offset, что бы изменить тип str() в int().
    Python должен вызвать исключение ValueError, если вы вызываете метод int() для значения, которое не может быть приведено к типу int(), например — ‘foo‘. В нашем примере, если мы встречаем ValueError — мы вызываем исключение django.http.Http404, которое вернёт ошибку «404 — Страница не найдена».Внимательные читатели могут удивиться — как мы можем получить ValueError, если наше регулярное выражение (d{1,2}) перехватывает только цифры и, следовательно, полученные данные в переменной offset должны быть только цифрами, хотя и в строковом виде? Ответ — верно, мы не должны получить другие данные, так как URLpattern  предоставляет нам простой и удобный инструмент проверки полученных данных. Но — мы должны проверять эти данные на предмет вызова исключения ValueError на тот случай, если эта функция будет вызываться каким-то другим способом. Вы ведь помните про «слабые связи«?
  • В следущей строке функции мы вычисляем время и дату, и добавляем полученное количесво часов. Вы уже видели  datetime.datetime.now() в функции current_datetime. Новое в данном случае — это то, что мы можем выполнить арифметические действия над временем и датой, создав объект datetime.timedelta и добавив  результат к объекту datetime.datetime. Результат сохраняется в переменную dt.
    В этой строке так же становится понятно, почему мы приводим занчение переменной offset к типу int() — функция datetime.timedelta требует, что бы параметр  hours был типа integer.
  • Далее, мы создаём вывод HTML, так же, как мы делали это в функции current_datetime. Небольшое отличие тут — что бы производим замещение данных с помощью %s двух переменных, а не одной. Поэтому тут мы используем %s два раза в строке, и передаём кортеж из двух переменных (offset, dt), которые надо в неё вставить.
  • >И последним действием — мы возвращаем HttpResponse.

С новой функцией, и новым URLconf, запустите сервер разработки Django, если он ещё не запущен, и откройте страницу http://127.0.0.1:8000/time/plus/3/, что бы проверить как всё работает. Потом — попробуйте перейти на http://127.0.0.1:8000/time/plus/5/. Попробуйте зайти на http://127.0.0.1:8000/time/plus/100/, что бы убедиться что ваш URLconf принимает только две цифры — Django должен отобразить страницу 404. Ту же ошибку должна вернуть страница http://127.0.0.1:8000/time/plus/ — так как цифры в URL не переданы вообще.

Порядок программирования

В этом пример мы сначала написали наш URLpattern, а потом представление, но в предыдущем примере — наоборот, сначала представление, а потом URLpattern. Как правильно делать?

Процесс разработки очень разный.

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

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

В конце-концов — всё зависит то того, какой способ больше подходит вашей манере разработки. Оба они достойны использования.

Продолжение — в статье Django Book: страница ошибки Django.