Spec-Zone.ru › Django 4.0

Написание вашего первого приложения 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". Это означает, что когда кто-то выбирает одну из радиокнопок и отправляет форму, он отправит данные POST choice=#, где # — 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, который включает эту строку:

polls/urls.py
path('<int:question_id>/vote/', views.vote, name='vote'),

Мы также создали фиктивное реализацию функции vote(). Давайте создадим реальную версию. Добавьте следующее к polls/views.py:

polls/views.py
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 += 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 не указано.
  • После увеличения счётчика выбора код возвращает HttpResponseRedirect, а не обычный HttpResponse. HttpResponseRedirect принимает один аргумент: URL-адрес, на который будет перенаправлен пользователь (см. следующий пункт, как мы создаём URL-адрес в данном случае).

    Как указано в комментарии Python выше, после успешной обработки данных POST всегда следует возвращать HttpResponseRedirect. Этот совет не специфичен для Django; это хороший веб-практический совет в целом.

  • Мы используем функцию reverse() в конструкторе HttpResponseRedirect в этом примере. Эта функция помогает избежать необходимости жёсткого кодирования URL-адреса в функции представления. Она получает имя представления, которое мы хотим передать, и переменную часть URL-шаблона, указывающую на это представление. В данном случае, используя URLconf, который мы настроили в Уроке 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/ в вашем браузере и проголосуйте за вопрос. Вы должны увидеть страницу с результатами, которая обновляется каждый раз, когда вы голосуете. Если вы отправите форму, не выбрав вариант, вы должны увидеть сообщение об ошибке.

Примечание

Код нашего представления vote() имеет небольшую проблему. Сначала он получает объект selected_choice из базы данных, затем вычисляет новое значение votes, а затем сохраняет его обратно в базу данных. Если два пользователя вашего сайта попытаются проголосовать в абсолютно одно и то же время, это может пойти не так: для votes будет получено одно и то же значение, скажем, 42. Затем для обоих пользователей вычисляется и сохраняется новое значение 43, но ожидаемым значением должно быть 44.

Это называется голосовой гонкой. Если вас интересует, вы можете прочитать Избегание гонок с использованием F(), чтобы узнать, как можно решить эту проблему.

Использование обобщённых представлений: Меньше кода — лучше

Представления detail() (из Урока 3) и results() очень короткие — и, как упоминалось выше, избыточные. Представление index(), которое отображает список опросов, аналогично.

Эти представления представляют собой распространённый случай в веб-разработке: получение данных из базы данных в соответствии с параметром, переданным в URL, загрузка шаблона и возвращение отрендеренного шаблона. Поскольку это так часто встречается, Django предоставляет сокращение, называемое системой «обобщённых представлений».

Обобщённые представления абстрагируют общие шаблоны до такой степени, что вам даже не нужно писать код Python, чтобы написать приложение.

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

  1. Преобразовать URLconf.
  2. Удалить некоторые старые, ненужные представления.
  3. Ввести новые представления на основе обобщённых представлений Django.

Подробности читайте ниже.

Почему такая перестановка кода?

Как правило, при написании приложения Django вы оцениваете, подходят ли обобщённые представления для вашей задачи, и используете их с самого начала, а не переписываете свой код на полпути. Но в этом руководстве умышленно уделялось внимание написанию представлений «трудным способом» до сих пор, чтобы сосредоточиться на основных концепциях.

Вы должны знать основы математики, прежде чем начать пользоваться калькулятором.

Изменение URLconf

Сначала откройте polls/urls.py URLconf и измените его следующим образом:

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>.

Изменение представлений

Далее мы удалим старые index, detail, и results представления и вместо них будем использовать общие представления Django. Для этого откройте файл polls/views.py и измените его следующим образом:

polls/views.py
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.

Здесь мы используем два общих представления: ListView и DetailView. Соответственно, эти два представления абстрагируют понятия «отображение списка объектов» и «отображение страницы деталей для конкретного типа объекта».

  • Каждое общее представление должно знать, с какой моделью оно будет работать. Это задается с помощью атрибута model.
  • Общее представление DetailView ожидает, что значение первичного ключа, полученное из URL, будет называться "pk", поэтому мы изменили question_id на pk для общих представлений.

По умолчанию общее представление 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/4.0/intro/tutorial04/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API