Создаем свое первое приложение с Django, часть 3

Этот раздел – продолжение второй части учебника. Мы продолжим разрабатывать приложение для голосования и сосредоточимся на создании страниц сайта – “представлений”.

Философия

Представление – это “тип” страниц вашего приложения, которое является функцией для обработки запроса и использует шаблон для генерации страницы. Например, блог может состоять из следующих представлений:

  • Главная страница – показывает несколько последний записей блога.

  • Страница записи – страница отображения одной записи блога.

  • Страница-архив записей по годам – показывает все месяца года и записи блога сгруппированные по этим месяцам.

  • Страница-архив записей по месяцам – показывает все дни месяца и записи блога сгруппированные по этим дням.

  • Страница-архив записей по дням – показывает все записи за указанные день.

  • Форма комментариев – предоставляет возможность добавить комментарий к записи блога.

В нашем приложении для голосования мы реализуем следующие представления:

  • Главная страница – показывает несколько последних голосований.

  • Страница опроса – показывает вопрос без результатов но с формой для ответа.

  • Страница результата опроса – показывает результаты опроса.

  • Обрабатывает процесс голосования – обрабатывает ответ на опрос.

В Django каждое представление является функцией Python.

Структура URL-ов

Первый шаг создания представлений – это определить структуру URL-ов. Вы можете сделать это создав модуль Python, который называется конфигурация URL-ов(URLconf). URLconfs определяет как Django ассоциирует переданный URL с кодом Python.

При запросе к странице управляемой Django, система просматривает настройку ROOT_URLCONF, которая содержит строку в формате импорта модуля Python(с точкой). Django загружает указанный в настройке модуль и ищет в нем переменную urlpatterns, которая является последовательностью кортежей следующего формата:

(regular expression, Python callback function [, optional dictionary])

Django проверяет соответствие запрошенного URL-а с регулярным выражением(первый элемент кортежа), начиная с первого и далее по списку, пока не будет найдено подходящее.

При нахождении подходящего регулярного выражения, Django вызывает функцию Python передавая: первым аргументом объект HttpRequest, все значения “распознанные” регулярным выражением как именованные аргументы, и, при наличии, дополнительные аргументы указанные в URL-шаблоне (не обязательный третий аргумент кортежа).

Подробности о HttpRequest смотрите в разделе Объекты ответа и запроса. Подробности о URLconfs в разделе Менеджер URL-ов.

После выполнения команды django-admin.py startproject mysite в Части 1 учебника, была создана конфигурация URL-ов по умолчанию в mysite/urls.py. Настройка ROOT_URLCONF`(в ``settings.py`) автоматически установлена что бы указывать на этот файл:

ROOT_URLCONF = 'mysite.urls'

Перейдем к примерам. Отредактируем mysite/urls.py следующим образом:

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^polls/$', 'polls.views.index'),
    url(r'^polls/(?P<poll_id>\d+)/$', 'polls.views.detail'),
    url(r'^polls/(?P<poll_id>\d+)/results/$', 'polls.views.results'),
    url(r'^polls/(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
    url(r'^admin/', include(admin.site.urls)),
)

Давайте рассмотрим этот код внимательней. Когда кто-то запрашивает страницу вашего сайта – например, “/polls/23/”, Django загружает этот модуль Python, так как он указан в настройке ROOT_URLCONF. Находится переменная urlpatterns и перебираются регулярные выражения. При нахождении подходящего регулярного выражения – r'^polls/(?P<poll_id>\d+)/$' – загружается функция detail() из polls/views.py. Функция detail() вызывается следующим образом:

detail(request=<HttpRequest object>, poll_id='23')

Аргумент poll_id='23' получен из (?P<poll_id>\d+). Использование скобок позволяет передать значения распознанные регулярным выражением в представление, ?P<poll_id> определяет название переменной при передаче и \d+ выражение, которое распознает цифры.

Так как URL-шаблоны это регулярные выражения, вы можете распознать какой угодно URL. И нет необходимости добавлять в URL всякий хлам вроде .php – если вы конечно не счастливый обладатель больного чувства юмора, в таком случае можете использовать что-то вроде:

(r'^polls/latest\.php$', 'polls.views.index'),

Но не делайте так. Это глупо.

Обратите внимание что регулярные выражения не обрабатывают GET и POST параметры или название домена. Например, при запросе к http://www.example.com/myapp/, URLconf будет обрабатывать myapp/. При запросе к http://www.example.com/myapp/?page=3, URLconf так же получит myapp/.

Информацию о регулярных вы можете найти в Википедии и документации модуля re. Так же очень полезна книга издательства O’Reilly “Mastering Regular Expressions”, автор Jeffrey Friedl.

Немного о производительности: регулярные выражения компилируются при первой загрузки модуля URLconf. Они работают очень быстро.

Создадим первое представление

У нас пока нет ни одного представления – только URLconf. Но давайте для начала убедимся, что Django “видит” наш URLconf.

Запустим сервер для разработки:

python manage.py runserver

Откройте страницу “http://localhost:8000/polls/” в браузере. Вы должны увидеть страницу с ошибкой следующего содержания:

ViewDoesNotExist at /polls/

Could not import polls.views.index. View does not exist in module polls.views.

Это потому что мы не создали функцию index() в модуле polls/views.py.

Откройте “/polls/23/”, “/polls/23/results/” и “/polls/23/vote/”. Сообщение ошибки укажет какое представление Django пытается использовать (и не может найти т.к. мы их еще не создали).

Настало время создать первое представление. Откройте файл polls/views.py и добавьте следующий код:

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the poll index.")

Это самое простое представление, которое только может быть. Откройте страницу “/polls/” в браузере и увидите ваш текст.

Теперь создадим еще парочку представлений. Эти представления немного отличаются так как принимают аргументы (которые, как мы помним, получаем из URL с помощью регулярных выражений из URLconf):

def detail(request, poll_id):
    return HttpResponse("You're looking at poll %s." % poll_id)

def results(request, poll_id):
    return HttpResponse("You're looking at the results of poll %s." % poll_id)

def vote(request, poll_id):
    return HttpResponse("You're voting on poll %s." % poll_id)

Откройте страницу “/polls/34/”. Будет выполнена функция detail() и показан ID, который вы указали в URL. Откройте “/polls/34/results/” и “/polls/34/vote/” – вы увидите наши будущие страницы результатов и голосования.

Добавим функционал в наши представления

Каждое представление должно выполнить одно из двух действий: вернуть экземпляр HttpResponse с содержимым страницы, или вызвать исключения такое как Http404. Все остальное на ваше усмотрение.

Ваше представление может обращаться к базе данных или нет. Может использовать систему шаблонов Django – или любую другую – или не использовать. Может генерировать PDF файл, возвращать XML, создавать ZIP архив “на лету”, все что угодно, используя любые библиотеки Python.

Все что нужно Django – это HttpResponse. Или исключение.

Мы будем использовать API Django для работы с базой данных, которое мы рассматривали в Части 1. Изменим index() так, чтобы оно отображало последние 5 опросов разделенные запятой от самого нового к самому старому:

from polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    output = ', '.join([p.question for p in latest_poll_list])
    return HttpResponse(output)

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

from django.template import Context, loader
from polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    t = loader.get_template('polls/index.html')
    c = Context({
        'latest_poll_list': latest_poll_list,
    })
    return HttpResponse(t.render(c))

Этот код загружает шаблон “polls/index.html” и передает ему контекст. Контекст это словарь, содержащий название переменных шаблона и соответствующие им значения.

Перезагрузите страницу. Вы увидите следующую ошибку:

TemplateDoesNotExist at /polls/
polls/index.html

Мы не создали шаблон. Сначала создайте каталог к которому Django имеет доступ. (Django используется пользователя от которого запущен сервер.) Лучше не добавляйте его в корень сервера(document root), что бы они не были доступны из вне, для безопасности. Теперь отредактируйте TEMPLATE_DIRS в файле settings.py что бы указать Django где искать шаблоны – как и в разделе “Настраиваем внешний вид” во второй части учебника.

Теперь создадим каталог polls в каталоге шаблонов. В нем создадим файл index.html. Код из представления loader.get_template('polls/index.html') ищет файл “[template_directory]/polls/index.html” в файловой системе.

Добавьте следующий код в шаблон:

{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Загрузите страницу в браузере, вы должны увидеть список с опросом “What’s up” из Части 1. Ссылка указывает ведет на страницу опроса.

Функция: render_to_response()

Процесс загрузки шаблона, добавления контекста и возврат объекта HttpResponse вполне тривиальный. Django предоставляет функция для всех этих операций. Вот как будет выглядеть наш index():

from django.shortcuts import render_to_response
from polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})

Так как мы используем такой подход во всех наших представлениях, нет необходимости импортировать loader, Context и HttpResponse.

Функция render_to_response() первым аргументом принимает название шаблона, вторым – необязательный словарь значений контекста. Возвращает объект HttpResponse содержащий выполненный шаблон с указанным контекстом.

404 исключение

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

from django.http import Http404
# ...
def detail(request, poll_id):
    try:
        p = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        raise Http404
    return render_to_response('polls/detail.html', {'poll': p})

Представление вызывает исключение Http404, если опрос с указанным ID не существует.

Содержимое шаблона polls/detail.html обсудим позже, сейчас просто добавьте:

{{ poll }}

что бы можно было загрузить страницу.

Функция get_object_or_404()

Вызов get() и Http404 при отсутствии объекта – обыденные операции. Django предоставляет функцию, которая выполняет эти действия. Вот как будет выглядеть наше представление detail():

from django.shortcuts import render_to_response, get_object_or_404
# ...
def detail(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p})

Функция get_object_or_404() первым аргументом принимает Django модель и произвольное количество именованных аргументов, которые передаются в метод get(). Если объект не найден, вызывается исключение Http404.

Философия

Зачем мы используем функцию get_object_or_404() вместо того, что бы автоматически перехватывать исключения ObjectDoesNotExist уровнем выше, или вызывать на уровне API моделей исключение Http404 вместо ObjectDoesNotExist?

Потому что это вяжет уровень моделей с уровнем представления. Один из главных принципов проектирования Django – слабая связанность.

Существует так же функция get_list_or_404(), которая работает аналогично get_object_or_404(), но использует filter() вместо get(). Вызывает Http404 если получен пустой список.

Создадим 404 (страница не найдена) представление

При вызове исключения Http404 в представлении, Django загружает специальное представление, которое обрабатывает 404 ошибку. Django ищет его проверяя переменную handler404 в модуле корневого URLconf (и только в корневом), которая должна содержать строку в формате импорта модуля Python(dotted syntax) – аналогичный определению функции представления в URL-шаблоне. Это представление являет собой ничего особенного.

Вам не обязательно самостоятельно определять 404 представление. Если вы не определите handler404, будет использовать встроенная функция django.views.defaults.page_not_found(). В этом случае вам просто нужно создать шаблон 404.html в корне каталога шаблонов. 404 представление по умолчанию будет использовать его для всех 404 ошибок. При DEBUG равном False (в модуле настроек) и отсутствии шаблона 404.html, будет вызвано исключение Http500. Так что не забудьте создать шаблон 404.html.

Так же:

  • При DEBUG равном True 404 представление не используется (соответственно шаблон 404.html так же не используется) и отображается отладочная информация.

  • 404 представление так же используется если Django не находит подходящего URL-шаблона.

Создадим 500 (ошибка сервера) представление

Так же ваш URLconf может определить переменную handler500, которая указывает на представление вызываемое при ошибке.

Использование системы шаблонов

Вернемся к представлению detail(). Используя переменную контекста poll, вот как может выглядеть наш шаблон “polls/detail.html”:

<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }}</li>
{% endfor %}
</ul>

Система шаблонов использует точку для доступа к атрибутам переменной. Например, для {{ poll.question }} Django сначала пытается обратиться к poll как к словарю. При неудаче ищется атрибут переменной, в данном случае он и используется. Если атрибут не найден, будет искаться индекс в списке.

В теге {% for %} выполняется вызов метода: poll.choice_set.all интерпретируется как код Python poll.choice_set.all(), который возвращает список объектов Choice для использования в теге {% for %}.

Подробности о шаблонах смотрите в разделе о Языке шаблонов Django.

Упрощаем конфигурацию URL-ов

Создавай URLconf вы могли заметить, что в нем присутствует избыточность:

urlpatterns = patterns('',
    url(r'^polls/$', 'polls.views.index'),
    url(r'^polls/(?P<poll_id>\d+)/$', 'polls.views.detail'),
    url(r'^polls/(?P<poll_id>\d+)/results/$', 'polls.views.results'),
    url(r'^polls/(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
)

А именно, polls.views присутствует в каждом URL-шаблоне.

URLconf предоставляет возможность использовать префиксы. Вы можете удалить повторяющийся префикс и указать его первым аргументом patterns():

urlpatterns = patterns('polls.views',
    url(r'^polls/$', 'index'),
    url(r'^polls/(?P<poll_id>\d+)/$', 'detail'),
    url(r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
    url(r'^polls/(?P<poll_id>\d+)/vote/$', 'vote'),
)

Этот вариант работает аналогично предыдущему, но выглядит проще.

Так как различные представления содержат различный префикс, вы можете объединить несколько patterns() с различными префиксами. Ваш mysite/urls.py может выглядеть следующим образом:

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('polls.views',
    url(r'^polls/$', 'index'),
    url(r'^polls/(?P<poll_id>\d+)/$', 'detail'),
    url(r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
    url(r'^polls/(?P<poll_id>\d+)/vote/$', 'vote'),
)

urlpatterns += patterns('',
    url(r'^admin/', include(admin.site.urls)),
)

Разъединение конфигурации URL-ов

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

Наше приложение вполне независимо благодаря структуре каталогов, созданной python manage.py startapp. Но одна часть все еще связана с проектом – URLconf.

Мы использовали mysite/urls.py, но строение URL-ов приложения относится к приложению, а не всему проекту – давайте перенесем URL-шаблоны в каталог приложения.

Скопируйте mysite/urls.py в polls/urls.py. Измените mysite/urls.py удалив URL-шаблоны приложения и замените их на include():

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^polls/', include('polls.urls')),
    url(r'^admin/', include(admin.site.urls)),
)

include() просто ссылается на другой URLconf. Заметим что регулярное выражение не содержит $ (признак конца строки) но содержит завершающий слэш. Когда Django встречает include(), он отрезает распознанную часть URL, все что осталось передает в указанный URLconf для дальнейшей обработки.

Вот что произойдет при запросе к “/polls/34/”:

  • Django найдет '^polls/'

  • Затем Django обрежет распознанную часть ("polls/") и передаст остаток – "34/" – в ‘polls.urls’ для дальнейшей обработки.

Теперь необходимо упростить polls.urls удалив “polls/” из каждой строки, и удалив активацию интерфейса администратора. Файл polls/urls.py должен выглядеть следующим образом:

from django.conf.urls import patterns, include, url

urlpatterns = patterns('polls.views',
    url(r'^$', 'index'),
    url(r'^(?P<poll_id>\d+)/$', 'detail'),
    url(r'^(?P<poll_id>\d+)/results/$', 'results'),
    url(r'^(?P<poll_id>\d+)/vote/$', 'vote'),
)

Идея использования include() и разделения URLconf состоит в том, что бы легко подключать и изменять конфигурацию URL-ов. Тепер, когда приложение голосования содержит собственный URLconf, вы можете подключить его в “/polls/”, или “/fun_polls/”, или в “/content/polls/”, или другой путь и приложение будет работать.

Приложение голосования работает с относительными путями, а не абсолютными.

Научившись создавать представления перейдем к Части 4 учебника, что бы научиться обрабатывать формы и использовать встроенные представления.