Создание вашей первой Django-приложения, часть 3
Этот учебник начинается там, где Учебник 2 закончился. Мы продолжаем приложение Web-poll и сосредоточимся на создании публичного интерфейса — «представлениях».
Обзор
Представление — это «тип» веб-страницы в вашем Django-приложении, которое обычно выполняет определённую функцию и имеет определённую шаблон. Например, в приложении блога у вас могут быть следующие представления:
- Главная страница блога — отображает последние несколько записей.
- Страница «детали» записи — страница с постоянной ссылкой на одну запись.
- Архив по годам — отображает все месяцы с записями в данном году.
- Архив по месяцам — отображает все дни с записями в данном месяце.
- Архив по дням — отображает все записи в данный день.
- Действие комментария — обрабатывает публикацию комментариев к данной записи.
В нашем приложении опросов у нас будут следующие четыре представления:
- Страница «список» вопросов — отображает последние несколько вопросов.
- Страница «детали» вопроса — отображает текст вопроса без результатов, но с формой для голосования.
- Страница «результатов» вопроса — отображает результаты по конкретному вопросу.
- Действие голосования — обрабатывает голосование за определённый вариант ответа в конкретном вопросе.
В 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 использует то, что известно как «URLconfs». URLconf сопоставляет URL-паттерны (описанные как регулярные выражения) с представлениями.
Этот учебник предоставляет базовые инструкции по использованию URLconfs, и вы можете обратиться к django.urls за дополнительной информацией.
Написание дополнительных представлений
Теперь добавим несколько дополнительных представлений в polls/views.py. Эти представления немного отличаются, потому что они принимают аргумент:
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
Подключите эти новые представления к модулю polls.urls, добавив следующие вызовы url():
from django.conf.urls import url
from . import views
urlpatterns = [
# ex: /polls/
url(r'^$', views.index, name='index'),
# ex: /polls/5/
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
# ex: /polls/5/vote/
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
Посмотрите в вашем браузере на «/polls/34/». Он запустит метод detail() и отобразит любой ID, который вы предоставите в URL. Попробуйте также «/polls/34/results/» и «/polls/34/vote/» — они отобразят страницы результатов и голосования.
Когда кто-то запрашивает страницу с вашего сайта — например, «/polls/34/», Django загрузит модуль Python mysite.urls, потому что он указан в настройке ROOT_URLCONF. Он находит переменную с именем urlpatterns и последовательно проходит по регулярным выражениям. Найдя соответствие в '^polls/', он удаляет совпадающий текст ("polls/") и отправляет оставшийся текст — "34/" — в URLconf ‘polls.urls’ для дальнейшей обработки. Там он находит r'^(?P<question_id>[0-9]+)/$', что приводит к вызову представления detail() следующим образом:
detail(request=<HttpRequest object>, question_id='34')
Часть question_id='34' происходит из (?P<question_id>[0-9]+). Использование скобок вокруг паттерна «захватывает» текст, соответствующий этому шаблону, и отправляет его как аргумент в функцию представления; ?P<question_id> определяет имя, которое будет использоваться для идентификации совпадающего паттерна; и [0-9]+ — это регулярное выражение для поиска последовательности цифр (т.е. числа).
Поскольку URL-паттерны являются регулярными выражениями, у вас нет ограничений на то, что вы можете с ними сделать. И нет необходимости добавлять URL-фрагменты, такие как .html — если вам это не нужно, вы можете сделать что-то вроде этого:
url(r'^polls/latest\.html$', views.index),
Но не делайте этого. Это глупо.
Создайте представления, которые действительно что-то делают
Каждое представление отвечает за выполнение одного из двух действий: возвращение объекта HttpResponse, содержащего содержимое запрашиваемой страницы, или вызов исключения, такого как Http404. Остальное зависит от вас.
Ваше представление может считывать записи из базы данных или нет. Оно может использовать систему шаблонов, такую как Django, или стороннюю систему шаблонов Python, или нет. Оно может генерировать файл PDF, выводить XML, создавать ZIP-архив на лету, что угодно, используя любые библиотеки Python.
Всё, что нужно Django, это HttpResponse. Или исключение.
Для удобства давайте воспользуемся собственным API базы данных Django, который мы рассмотрели в Учебнике 2. Вот пример нового представления index() , которое отображает последние 5 вопросов опросов в системе, разделённые запятыми, по дате публикации:
from django.http import HttpResponse
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ', '.join([q.question_text for q in latest_question_list])
return HttpResponse(output)
# Leave the rest of the views (detail, results, vote) unchanged
Однако есть проблема: дизайн страницы жёстко закодирован в представлении. Если вы хотите изменить внешний вид страницы, вам придётся редактировать этот код Python. Поэтому давайте воспользуемся системой шаблонов Django, чтобы отделить дизайн от Python, создав шаблон, который может использовать представление.
Сначала создайте каталог с именем templates в своём каталоге polls. Django будет искать шаблоны там.
Настройка TEMPLATES вашего проекта описывает, как Django будет загружать и отображать шаблоны. Файл настроек по умолчанию настраивает DjangoTemplates бэкенд, в котором параметр APP_DIRS установлен в True. По соглашению DjangoTemplates ищет подкаталог «templates» в каждом из приложений INSTALLED_APPS.
Внутри каталога templates, который вы только что создали, создайте другой каталог с именем polls, а в нём создайте файл под названием index.html. Другими словами, ваш шаблон должен находиться по адресу polls/templates/polls/index.html. Благодаря тому, как работает загрузчик шаблонов app_directories, вы можете ссылаться на этот шаблон в Django просто как на polls/index.html.
Именование шаблонов
Сейчас мы можем обойтись без размещения шаблонов непосредственно в polls/templates (а не создавать другой подкаталог polls), но это была бы плохая идея. Django выберет первый шаблон, имя которого соответствует имени, и если у вас был шаблон с таким же именем в другом приложении, Django не смог бы отличить их друг от друга. Нам нужно иметь возможность указать Django нужный шаблон, и самый простой способ гарантировать это — использовать имена пространств имён. То есть поместив эти шаблоны в другой каталог с именем приложения.
Вставьте следующий код в этот шаблон:
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</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 loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = {
'latest_question_list': latest_question_list,
}
return HttpResponse(template.render(context, request))
Этот код загружает шаблон с именем polls/index.html и передаёт ему контекст. Контекст — это словарь, сопоставляющий имена переменных шаблона с объектами Python.
Загрузите страницу, указав в браузере адрес «/polls/», и вы увидите список с пунктами, содержащим вопрос «Что происходит» из Учебника 2. Ссылка указывает на страницу с подробностями о вопросе.
Сокращение: render()
Это очень распространённый приём — загрузить шаблон, заполнить контекст и вернуть объект HttpResponse с результатом отрисованного шаблона. Django предоставляет сокращение. Вот полное представление index() , переписанное:
from django.shortcuts import render
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)
Обратите внимание, что после выполнения этого во всех представлениях нам больше не нужно импортировать loader и HttpResponse (вам нужно оставить HttpResponse , если у вас все ещё есть заглушки методов для detail, results, и vote).
Функция render() принимает объект запроса в качестве первого аргумента, имя шаблона — в качестве второго и словарь — в качестве необязательного третьего аргумента. Она возвращает объект HttpResponse с заданным шаблоном, отрисованным с данным контекстом.
Вызов ошибки 404
Теперь давайте рассмотрим представление деталей вопроса — страницу, на которой отображается текст вопроса для заданного опроса. Вот представление:
from django.http import Http404
from django.shortcuts import render
from .models import Question
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})
Новая концепция здесь: представление вызывает исключение Http404, если вопрос с запрошенным ID не существует.
Мы обсудим, что вы могли бы поместить в шаблон polls/detail.html немного позже, но если вы хотите быстро запустить пример выше, файл, содержащий только:
{{ question }}
вам пока поможет.
Сокращение: get_object_or_404()
Очень распространённый приём — использовать get() и поднимать Http404, если объект не существует. Django предоставляет сокращение. Вот detail() представление, переписанное:
from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
Функция 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() представлению для нашего приложения опросов. Учитывая переменную контекста question, вот как может выглядеть polls/detail.html шаблон:
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
Система шаблонов использует синтаксис точечного доступа для доступа к атрибутам переменных. В примере {{ question.question_text }}, Django сначала выполняет поиск в словаре по объекту question. Если это не удаётся, она пытается найти атрибут — что в данном случае работает. Если бы поиск атрибута тоже не удался, она бы попыталась найти индекс в списке.
Вызов метода происходит в цикле {% for %}: question.choice_set.all интерпретируется как Python-код question.choice_set.all(), который возвращает итерируемый объект Choice и подходит для использования в теге {% for %}.
Дополнительную информацию о шаблонах см. в руководстве по шаблонам.
Удаление жёстко закодированных URL-адресов в шаблонах
Помните, когда мы писали ссылку на вопрос в шаблоне polls/index.html, ссылка частично была жёстко закодирована так:
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
Проблема с этим жёстко закодированным, тесно связанным подходом заключается в том, что при изменении URL-адресов в проектах с большим количеством шаблонов это становится сложным. Однако, поскольку вы определили аргумент name в функциях url() в модуле polls.urls, вы можете избавиться от зависимости от конкретных URL-путей, определённых в ваших URL-конфигурациях, используя тег шаблона {% url %}:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
Этот метод работает, выполняя поиск определения URL-адреса, как указано в модуле polls.urls.
Вы можете увидеть, где определён URL-имя «detail», ниже:
...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...
Если вы хотите изменить URL-адрес представления детального просмотра опросов на что-то другое, например, на polls/specifics/12/ вместо того, чтобы изменять его в шаблоне (или шаблонах), вы измените его в polls/urls.py:
... # added the word 'specifics' url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'), ...
Использование пространств имён для URL-имен
В проекте учебника есть только один app, polls. В реальных проектах Django может быть пять, десять, двадцать или более приложений. Как Django различает имена URL-адресов между ними? Например, приложение polls имеет представление detail, и то же самое может быть в приложении для блога в том же проекте. Как сделать так, чтобы Django знал, какое представление приложения создать для URL-адреса при использовании тега шаблона {% url %}?
Ответ заключается в добавлении пространств имён в вашу URLconf. В файле polls/urls.py добавьте app_name для установки пространства имён приложения:
from django.conf.urls import url
from . import views
app_name = 'polls'
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
Теперь измените свой шаблон polls/index.html с:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
на указание на представление с пространством имён detail:
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
Когда вы будете готовы к написанию представлений, прочитайте часть 4 этого учебника, чтобы узнать о простой обработке форм и универсальных представлениях.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.11/intro/tutorial03/