Написание вашей первой Django-приложения, часть 4
Этот учебник начинается там, где Учебник 3 закончился. Мы продолжаем веб-приложение для опросов и сосредоточимся на обработке форм и сокращении кода.
Где получить помощь:
Если у вас возникли проблемы с прохождением этого учебника, перейдите к разделу Получение помощи раздела FAQ.
Напишите минимальную форму
Обновите шаблон детали опроса (“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>
Краткое описание:
- Вышеупомянутый шаблон отображает радиокнопку для каждого варианта ответа.
valueкаждой радиокнопки — это ID соответствующего варианта ответа.nameкаждой радиокнопки —"choice". Это означает, что когда кто-то выбирает одну из радиокнопок и отправляет форму, он отправит данные POSTchoice=#, где # — ID выбранного варианта. Это базовый принцип HTML-форм. - Мы устанавливаем
actionформы в{% url 'polls:vote' question.id %}, и мы устанавливаемmethod="post". Использованиеmethod="post"(вместоmethod="get") очень важно, потому что действие отправки этой формы изменит данные на стороне сервера. Всякий раз, когда вы создаете форму, изменяющую данные на стороне сервера, используйтеmethod="post". Этот совет не специфичен для Django; это хорошая практика веб-разработки в целом. -
forloop.counterуказывает, сколько раз тегforпрошел по циклу - Поскольку мы создаем форму POST (которая может изменить данные), нам нужно беспокоиться о межсайтовых атаках с подделкой запросов. К счастью, вам не нужно слишком сильно об этом беспокоиться, поскольку Django поставляется с полезной системой защиты от неё. Короче говоря, все формы POST, направленные на внутренние URL-адреса, должны использовать тег шаблона
{% csrf_token %}.
Теперь давайте создадим Django-представление, которое обрабатывает отправленные данные и что-то с ними делает. Помните, что в Учебнике 3 мы создали URLconf для приложения опросов, который включает эту строку:
polls/urls.pypath("<int:question_id>/vote/", views.vote, name="vote"),
Мы также создали имитацию функции vote(). Давайте создадим реальную версию. Добавьте следующее в polls/views.py:
polls/views.pyfrom 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и снова отображает форму вопроса с сообщением об ошибке, еслиchoiceне указано. -
F("votes") + 1инструктирует базу данных увеличить счет голосов на 1. -
После увеличения счетчика выбора код возвращает
HttpResponseRedirect, а не обычныйHttpResponse.HttpResponseRedirectпринимает один аргумент: URL, на который будет перенаправлен пользователь (см. следующий пункт, как мы строим URL в этом случае).Как указано в комментарии Python выше, вы всегда должны возвращать
HttpResponseRedirectпосле успешной обработки данных POST. Этот совет не специфичен для Django; это хорошая практика веб-разработки в целом. -
Мы используем функцию
reverse()в конструктореHttpResponseRedirectв этом примере. Эта функция помогает избежать необходимости жестко задавать URL в функции представления. Она получает имя представления, которому мы хотим передать управление, и переменную часть URL-шаблона, которая указывает на это представление. В данном случае, используя URLconf, который мы настроили в Учебнике 3, этот вызовreverse()вернет строку, такую как"/polls/3/results/"
где
3— значениеquestion.id. Этот URL перенаправления затем вызовет представление'results'для отображения конечной страницы.
Как упоминалось в Учебнике 3, request — это объект HttpRequest. Подробнее об объектах HttpRequest см. в документации по запросам и ответам.
После того, как кто-то проголосует за вопрос, представление vote() перенаправит на страницу результатов для этого вопроса. Давайте напишем это представление:
polls/views.pyfrom 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 предоставляет сокращение под названием «система общих представлений».
Общие представления абстрагируют распространённые шаблоны до такой степени, что вам даже не нужно писать код Python для создания приложения. Например, общие представления ListView и DetailView абстрагируют концепции «отображение списка объектов» и «отображение страницы деталей для определенного типа объекта» соответственно.
Давайте переведём наше приложение опросов на использование системы общих представлений, чтобы удалить часть нашего собственного кода. Нам придётся сделать несколько шагов для преобразования.
- Преобразовать URLconf.
- Удалить некоторые старые, ненужные представления.
- Ввести новые представления на основе общих представлений Django.
Подробнее об этом далее.
Почему перестановка кода?
Как правило, при написании Django-приложения вы оцените, подходят ли общие представления для вашей задачи, и будете использовать их с самого начала, а не переписывать код на полпути. Но этот учебник намеренно сосредоточился на написании представлений «сложным способом» до сих пор, чтобы сосредоточиться на основных понятиях.
Вы должны знать основы математики, прежде чем начать использовать калькулятор.
Изменить URLconf
Сначала откройте polls/urls.py URLconf и измените его следующим образом:
polls/urls.pyfrom 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() и results() представлений, а он ожидает, что значение первичного ключа, полученного из URL, будет называться "pk".
Изменение представлений
Далее мы удалим наши старые index, detail, и results представления и вместо них воспользуемся обобщенными представлениями Django. Для этого откройте файл polls/views.py и измените его следующим образом:
polls/views.pyfrom 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 для указания ListView использовать наш существующий шаблон "polls/index.html".
В предыдущих частях учебника шаблоны получали контекст, содержащий переменные контекста question и latest_question_list. Для DetailView переменная question предоставляется автоматически – так как мы используем Django-модель (Question), Django может определить подходящее имя для переменной контекста. Однако для ListView автоматически сгенерированная переменная контекста – question_list. Чтобы переопределить это, мы предоставляем атрибут context_object_name, указывая, что хотим использовать latest_question_list вместо неё. В качестве альтернативного подхода, вы могли бы изменить ваши шаблоны для соответствия новым переменным контекста по умолчанию – но намного проще указать Django использовать нужную вам переменную.
Запустите сервер и используйте ваше новое приложение опросов, основанное на обобщенных представлениях.
Для получения подробной информации об обобщенных представлениях см. документацию по обобщенным представлениям.
Когда вы будете готовы к формам и обобщенным представлениям, прочтите часть 5 этого учебника, чтобы узнать о тестировании нашего приложения опросов.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/intro/tutorial04/