Создаём своё первое приложение с Django, часть 4¶
Продолжаем начатое в третьей части учебника. Мы продолжим разрабатывать приложение для голосования и сосредоточимся на обработке форм и упрощении нашего кода.
Где получить помощь:
При наличии проблем с данной инструкцией, пожалуйста, обратитесь к разделу FAQ Получение помощи.
Write a minimal form¶
Отредактируем шаблон страницы опроса («polls/detail.html») из предыдущего раздела учебника, и добавим HTML элемент <form>:
polls/templates/polls/detail.html¶<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>{{ question.question_text }}</h1></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
Краткий обзор:
Данный шаблон отображает radio-поле для каждого варианта ответа вопроса.
valueполя содержит ID ответа.nameкаждого поля равно"choice". Это означает, что при выборе поля и отправке формы, будет отправлены POST данныеchoice=#, где # равно ID выбранного ответа. Это основной принцип работы HTML форм.Значение
actionформы равно{% url 'polls:vote' question.id %}, еще добавленоmethod="post". Использованиеmethod="post"(вместоmethod="get") очень важно, так как отправка формы изменяет данные. Для форм изменяющих данные используйтеmethod="post". Этот совет не относится к Django, это хорошая практика для Web-приложений.forloop.counterсодержит номер итерации цикла тегаforТак как мы создаем POST форму (которая может привести к изменениям данных), нам следует побеспокоится о Cross Site Request Forgeries. Благодаря Django это очень просто. В общем, все POST формы, которые отправляются на внутренний URL, должны использовать тег
{% csrf_token %}.
Теперь создадим представление Django, которое принимает данные отправленные формой и обрабатывает их. Вспомним, в Части 3 мы создали URLconf который содержит следующую строку:
polls/urls.py¶path("<int:question_id>/vote/", views.vote, name="vote"),
Мы также создали функцию-«заглушку» vote(). Давайте создадим настоящую функцию представления. Добавьте следующее в polls/views.py:
polls/views.py¶from django.db.models import F
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from .models import Choice, Question
# ...
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST["choice"])
except (KeyError, Choice.DoesNotExist):
# Redisplay the question voting form.
return render(
request,
"polls/detail.html",
{
"question": question,
"error_message": "You didn't select a choice.",
},
)
else:
selected_choice.votes = F("votes") + 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
Этот код содержит вещи, которые еще не объяснялись в учебнике:
request.POST– это объект с интерфейсом словаря, который позволяет получить отправленные данные через название ключа. В нашем случаеrequest.POST['choice']вернет ID выбранного варианта ответа в виде строки.request.POSTвсегда содержит строки.Заметим что Django также предоставляет
request.GETдля аналогичного доступа к GET данным – но мы используемrequest.POST, чтобы быть уверенным, что данные передаются только при POST запросе.request.POST['choice']вызовет исключениеKeyError, еслиchoiceне находится в POST. Код обрабатывает исключениеKeyErrorи показывает страницу опроса с ошибкой, если не был выбран вариант ответа.F("votes") + 1:ref:` позволяет на уровне базы данных <avoiding-race-conditions-using-f>` увеличить значение vote на 1.После увеличения счетчика ответов представление возвращает
HttpResponseRedirectвместоHttpResponse.HttpResponseRedirectпринимает один аргумент: URL, на который необходимо перенаправить пользователя (обратите внимание, как мы создаем URL).После успешной обработки POST запроса, вы должны возвращать
HttpResponseRedirect. Это не относится конкретно к Django, это просто хорошая практика при разработке Web-приложений.Мы используем функцию
reverse()при созданииHttpResponseRedirect. Это функция помогает избегать «хардкодинга» URL-ов в коде. Она принимает название URL-шаблона и необходимые аргументы для создания URL-а. В этом примере используется конфигурация URL-ов из Части 3 иreverse()вернет:"/polls/3/results/"… где
3значениеquestion.id. При запросе к этому URL-у вызовется представление'results'для отображения результатов опроса.
Как упоминалось в Части 3, request является экземпляром HttpRequest. Подробности о классе HttpRequest смотрите в разделе об объектах запроса и ответа.
После ответа на опрос, представление vote() перенаправит пользователя на страницу с результатами. Давайте создадим представление для этой страницы:
polls/views.py¶from django.shortcuts import get_object_or_404, render
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, "polls/results.html", {"question": question})
Это представление аналогично представлению detail() из Части 3. Единственная разница - используемый шаблон. Повторение кода мы исправим позже.
Теперь создадим шаблон polls/results.html:
polls/templates/polls/results.html¶<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
Теперь откройте страницу /polls/1/ в браузере и ответьте на опрос. Вы должны увидеть страницу с результатами, которые обновляются после каждого ответа на опрос. Если вы отправите форму, не ответив на вопрос, вы должны увидеть сообщение об ошибке.
Базовые представления: меньше кода - меньше проблем¶
Представления detail() (из Части 3) и results() до глупости просты – и, как упоминалось выше, избыточны. Представление index(), которое показывает список опросов, также довольно стандартное.
Эти представления выполняют стандартные операции Веб-приложений: получение данных из базы данных в соответствии с параметрами из URL, загрузка шаблона и возвращение результата выполнения шаблона. Для таких стандартных операций Django предоставляет набор «общих представлений(generic views)».
Базовые представления используют популярные паттерны проектирования так что вам нужно писать минимум кода для создания приложения. К примеру, ListView и DetailView абстрагируют понятия «показать список объектов» и «показать страницу с деталями для определенного типа объектов» соответственно.
Давайте используем их в нашем приложении. Необходимо выполнить следующие действия:
Изменить URLconf.
Удалить старые и ненужные представления.
Создать новые представления из встроенных в Django общих представлений.
Подробности читайте далее.
Зачем мы переписываем код?
Скорее всего, вы будете использовать общие представления с самого начала без необходимости рефакторить проект. Этот учебник специально описывает создание представлений, чтобы описать основные концепции.
Вы должны знать основы математики для использования калькулятора.
Изменим URLconf¶
Первым делом откройте polls/urls.py и измените его следующим образом:
polls/urls.py¶from django.urls import path
from . import views
app_name = "polls"
urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
path("<int:pk>/", views.DetailView.as_view(), name="detail"),
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
path("<int:question_id>/vote/", views.vote, name="vote"),
]
Обратите внимание, что имя совпадающего шаблона в строках пути второго и третьего шаблонов изменилось с <question_id> на <pk>. Это необходимо, мы будем использовать базовое представление DetailView в качестве замены detail() and results() , а оно ожидает, что значение первичного ключа, полученное из URL, будет называться "pk".
Изменим представления¶
Теперь мы собираемся удалить старые представления index, detail и results и воспользуемся встроенными в Django общими представлениями. Для этого измените файл polls/views.py:
polls/views.py¶from django.db.models import F
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by("-pub_date")[:5]
class DetailView(generic.DetailView):
model = Question
template_name = "polls/detail.html"
class ResultsView(generic.DetailView):
model = Question
template_name = "polls/results.html"
def vote(request, question_id):
# same as above, no changes needed.
...
Каждое базовое преставление должно соответствовать той или иной модели. Указать модель можно с помощью атрибута model (в нашем случае, model = Question для DetailView и ResultsView) или определив метод get_queryset() (как показано в IndexView).
По умолчанию представление DetailView использует шаблон <app name>/<model name>_detail.html. В нашем случае будет использоваться "polls/question_detail.html". Атрибут template_name позволяет указать Django какой шаблон использовать. Мы указали template_name также для представления results, чтобы страница результата и подробностей использовали разные шаблоны, несмотря на то, что они используют представление DetailView.
Аналогично ListView использует шаблон <app name>/<model name>_list.html, мы использовали template_name, чтобы определить другой шаблон - "polls/index.html".
В предыдущей части учебника в шаблоне использовался контекст с переменными question и latest_question_list. Для DetailView переменная question добавляется автоматически – так как мы используем модель Question, Django автоматически генерирует подходящее название для переменной контекста. Для ListView переменная будет называться question_list. Чтобы переопределить это название, мы использовали атрибут context_object_name указав latest_question_list. Как вариант, вы можете изменить шаблон и использовать question_list, но проще указать Django какое название переменной контекста использовать.
Запустите сервер и протестируйте наше приложение.
Подробности об общих представлениях смотрите в соответствующем разделе.
Когда вы освоите формы и общие представления, переходите к пятой части учебника, чтобы узнать о тестировании нашего приложения.