Написание вашей первой Django-приложения, часть 3
Этот учебник начинается там, где закончился Учебник 2. Мы продолжаем приложение Web-poll и сосредоточимся на создании публичного интерфейса — «представлений».
Обзор
Представление — это «тип» веб-страницы в вашем Django-приложении, которое, как правило, выполняет определённую функцию и имеет определённую шаблонную разметку. Например, в приложении блога у вас могут быть следующие представления:
- Главная страница блога — отображает последние несколько записей.
- Страница «детали» записи — страница с постоянной ссылкой на одну запись.
- Страница архива по годам — отображает все месяцы с записями в данном году.
- Страница архива по месяцам — отображает все дни с записями в данном месяце.
- Страница архива по дням — отображает все записи в данный день.
- Действие комментария — обрабатывает размещение комментариев к данной записи.
В нашем приложении опросов у нас будут следующие четыре представления:
- Страница вопроса «index» — отображает последние несколько вопросов.
- Страница вопроса «detail» — отображает текст вопроса, без результатов, но с формой для голосования.
- Страница вопроса «results» — отображает результаты для конкретного вопроса.
- Действие голосования — обрабатывает голосование за конкретный вариант ответа в конкретном вопросе.
В 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 загрузит модуль mysite.urls Python, так как он указан в настройке 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-адреса
В проекте учебника всего один модуль, 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.10/intro/tutorial03/