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

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

Философия

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

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

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

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

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

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

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

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

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

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

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

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

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

В наше время в интернете можно наткнуться на такие прелести как “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B”. Рады вам сообщить, что Django позволяет использовать более элегантные URL шаблоны чем этот.

URL-шаблон - это общая форма URL-а. Например: /newsarchive/<year>/<month>/.

Чтобы из URL-а получить представление, Django используется так называемый ‘URLconf’. URLconf определяет соответствие URL-шаблонов(являются регулярными выражениями) и представлений.

В этом разделе мы опишем основы использования настроек URL-ов, больше информации вы можете найти в разделе django.core.urlresolvers.

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

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

from django.http import HttpResponse

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

Это самое простое из возможных представлений в Django. Необходимо прикрепить это представление к какому-то URL-у -для этого воспользуемся URLconf.

Создайте файл urls.py в каталоге приложения polls. Теперь ваше приложение должно выглядеть следующим образом:

polls/
    __init__.py
    admin.py
    models.py
    tests.py
    urls.py
    views.py

Добавим следующие строки в polls/urls.py:

from django.conf.urls import patterns, url

from polls import views

urlpatterns = patterns('',
    url(r'^$', views.index, name='index')
)

Теперь в главном URLconf подключим модуль polls.urls. В mysite/urls.py добавьте 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)),
)

Вы привязали представление index к URL-у используя URLconf. Откройте http://localhost:8000/polls/ в браузере, вы должны увидеть текст “Hello, world. You’re at the poll index.”, который вы указали в представлении index.

Функция url() принимает четыре аргумента, два обязательных: regex и view, и два необязательных: kwargs и name. Дайте разберемся, для чего они нужны.

url() argument: regex

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

Обратите внимание, что регулярные выражения не обрабатывают 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. Они работают очень быстро(если не очень-очень сложные).

url() argument: view

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

url() argument: kwargs

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

url() argument: name

Название URL-а позволяет однозначно указать на него в любом месте в Django, особенно полезно это в шаблонах. Это позволяет глобально изменять шаблоны URL-ов в одном месте.

Добавь парочку представлений

Теперь создадим еще парочку представлений в polls/views.py. Эти представления немного отличаются, так как принимают аргументы:

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.urls добавив вызов url():

from django.conf.urls import patterns, url

from polls import views

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

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

При запросе страницы – например, “/polls/34/”, Django загружает модуль mysite.urls т.к. он указан в ROOT_URLCONF. Находит переменную urlpatterns и перебирает все регулярные выражения по порядку. include() просто ссылается на другой URLconf. Заметим, что регулярное выражение не содержит $ (признак конца строки) но содержит завершающий слэш. Когда Django встречает include(), он отрезает распознанную часть URL, все, что осталось, передает в указанный URLconf для дальнейшей обработки.

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

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

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

  • Затем Django обрежет распознанную часть ("polls/") и передаст остаток – "34/" – в ‘polls.urls’ для дальнейшей обработки, который будет распознан r'^(?P<poll_id>\d+)/$' и будет вызвана функция detail():

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

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

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

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

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

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

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

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

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

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

from django.http import HttpResponse

from polls.models import Poll

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

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

Для начала создайте каталог templates в каталоге приложения polls. Django будет искать шаблоны в этом каталоге.

Настройка TEMPLATE_LOADERS содержит список модулей, которые знают, как загружать шаблоны из разных источников. Один из них django.template.loaders.app_directories.Loader, который проверяет подкаталог “templates” в приложениях указанных в INSTALLED_APPS - таким образом Django сможет найти наши шаблоны даже если мы не меняем TEMPLATE_DIRS, как мы делали в Части 2.

Organizing templates

Мы можем содержать все шаблоны в одном каталоге и это будет отлично работать. Однако, этот шаблон относится к приложению для голосования, поэтому мы добавим его в каталог шаблонов приложения (polls/templates), а не проекта (templates). Мы рассмотрим подробнее в разделе о независимых приложениях почему мы так сделали.

В только что созданом каталоге templates, создайте каталог polls, и в нем создайте файл index.html. То есть создайте файл polls/templates/polls/index.html. Учитывая как работает загрузчик шаблонов app_directories , вы сможете обращаться к шаблону как polls/index.html.

Template namespacing

Мы бы могли создать наш шаблон непосредственно в polls/templates (а не подкаталоге polls), но это плохая идея. Django будет использовать первый найденный шаблон, и если существует шаблон с аналогичным названием в другом приложении, Django не сможет различить их. Чтобы этого избежать, мы будем использовать пространство имен. Точнее, просто добавим их в еще один подкаталог с названием, аналогичным названию приложения.

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

{% 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 %}

Теперь изменим наше представление index в polls/views.py, чтобы использовать шаблон:

from django.http import HttpResponse
from django.template import RequestContext, loader

from polls.models import Poll

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

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

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

Функция render()

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

from django.shortcuts import render

from polls.models import Poll

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

Так как мы используем такой подход во всех наших представлениях, нет необходимости импортировать loader, RequestContext и HttpResponse (HttpResponse еще нужен, если остались старые detail, results и vote).

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

Вызов 404 исключения

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

from django.http import Http404
from django.shortcuts import render

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

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

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

{{ poll }}

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

Функция get_object_or_404()

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

from django.shortcuts import render, get_object_or_404

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

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

Философия

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

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

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

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

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

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

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

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

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

Избавляемся от “хардкода” URL-ов в шаблонах

Помните, когда мы указывали ссылку в шаблоне polls/index.html, она была прописана прямо в коде:

<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>

Проблема в том, что нам будет очень сложно поменять URL-ы в проекте с большим количеством шаблонов. Однако, так как мы указали названия при вызове url() в модуле polls.urls, мы можем ссылаться на шаблоны URL-ов используя шаблонный тег {% url %}:

<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>

Примечание

Если {% url 'detail' poll.id %} (с кавычками) не работает, а {% url detail poll.id %} (без кавычек) работает, значит вы используете версию Django < 1.5. В таком случае добавьте следующее в начале шаблона:

{% load url from future %}

Определение URL-а будет найдено в модуле polls.urls. Вот где определен наш URL с названием ‘detail’:

...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<poll_id>\d+)/$', views.detail, name='detail'),
...

Теперь, если вы захотите поменять URL, например на polls/specifics/12/, вам не придется менять все шаблоны, вы можете сделать это в polls/urls.py:

...
# added the word 'specifics'
url(r'^specifics/(?P<poll_id>\d+)/$', views.detail, name='detail'),
...

Пространства имен в названиях URL-ов

Наш проект содержит только одно приложение polls. В реальных проектах может быть 5, 10, 20 и больше приложений. Как же Django понимает где чей URL по его названию? Например, приложение polls содержит представление detail, аналогичное представление может быть и в приложении для блогов. Как же Django понимает для какого представления создается URL при использовании тега {% url %}?

Для этого используются пространства имен в URLconf. Изменим mysite/urls.py (urls.py проекта, не приложения) и добавим использование пространства имен:

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

from django.contrib import admin
admin.autodiscover()

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

Теперь поменяем в шаблоне polls/index.html:

<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>

чтобы использовать пространство имен URL-ов:

<li><a href="{% url 'polls:detail' poll.id %}">{{ poll.question }}</a></li>

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