Обработка форм и сокращение кода

DjangoDjangoBeginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Этот туториал начинается там, где закончился раздел Создание представлений для общедоступного интерфейса. Мы продолжаем работу над веб-опросом и сосредоточимся на обработке форм и сокращении кода.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL django(("Django")) -.-> django/CoreConfigurationandRoutingGroup(["Core Configuration and Routing"]) django(("Django")) -.-> django/DatabaseModelsandMigrationsGroup(["Database, Models, and Migrations"]) django(("Django")) -.-> django/UserInterfaceandInteractionGroup(["User Interface and Interaction"]) django/CoreConfigurationandRoutingGroup -.-> django/django_urls("Django Urls") django/DatabaseModelsandMigrationsGroup -.-> django/request_and_response("Request and Response") django/UserInterfaceandInteractionGroup -.-> django/built_in_views("Built-in Views") subgraph Lab Skills django/django_urls -.-> lab-153744{{"Обработка форм и сокращение кода"}} django/request_and_response -.-> lab-153744{{"Обработка форм и сокращение кода"}} django/built_in_views -.-> lab-153744{{"Обработка форм и сокращение кода"}} end

Напишите минимальную форму

Обновим шаблон деталей опроса (polls/detail.html) из предыдущего туториала, чтобы он содержал HTML-элемент <form>:

<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 %}<csrf_token>.

Теперь создадим представление Django, которое обрабатывает отправленные данные и делает с ними что-то. Помните, в разделе Создание представлений для общедоступного интерфейса мы создали URL-конфигурацию для приложения опросов, которая включает в себя эту строку:

path("<int:question_id>/vote/", views.vote, name="vote"),

Мы также создали заглушку реализации функции vote(). Создадим настоящую версию. Добавьте следующее в 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 <django.http.HttpRequest.POST> — это объект, похожий на словарь, который позволяет вам получать отправленные данные по имени ключа. В этом случае request.POST['choice'] возвращает ID выбранного варианта в виде строки. request.POST <django.http.HttpRequest.POST> значения всегда являются строками.

    Обратите внимание, что Django также предоставляет request.GET <django.http.HttpRequest.GET> для доступа к GET-данным аналогичным образом — но мы явно используем request.POST <django.http.HttpRequest.POST> в нашем коде, чтобы убедиться, что данные изменяются только через вызов POST.

  • request.POST['choice'] вызовет KeyError, если choice не был предоставлен в POST-данных. Код выше проверяет наличие KeyError и переотображает форму вопроса с сообщением об ошибке, если choice не указан.

  • После увеличения количества голосов код возвращает ~django.http.HttpResponseRedirect, а не обычный ~django.http.HttpResponse. ~django.http.HttpResponseRedirect принимает один аргумент: URL-адрес, на который будет перенаправлен пользователь (см. следующий пункт, как мы строим URL-адрес в этом случае).

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

  • В этом примере мы используем функцию ~django.urls.reverse в конструкторе ~django.http.HttpResponseRedirect. Эта функция помогает избежать жесткой кодировки URL-адреса в функции представления. Ей передается имя представления, на которое мы хотим передать управление, и переменная часть URL-шаблона, которая указывает на это представление. В этом случае, используя URL-конфигурацию, которую мы установили в разделе Создание представлений для общедоступного интерфейса, вызов ~django.urls.reverse вернет строку, похожую на:

    "/polls/3/results/"
    

    где 3 — это значение question.id. Затем этот перенаправленный URL-адрес вызовет представление 'results', чтобы отобразить конечную страницу.

Как упоминалось в разделе Создание представлений для общедоступного интерфейса, request — это объект ~django.http.HttpRequest. Для получения дополнительной информации о объектах ~django.http.HttpRequest см. документацию по запросам и ответам </ref/request-response>.

После того, как кто-то голосует за вопрос, представление vote() перенаправляет на страницу результатов для этого вопроса. Напишем это представление:

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() из раздела Создание представлений для общедоступного интерфейса. Единственная разница — это имя шаблона. Мы исправим эту избыточность позже.

Теперь создайте шаблон 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/ в вашем браузере и голосуйте за вопрос. Вы должны увидеть страницу результатов, которая обновляется каждый раз, когда вы голосуете. Если вы отправите форму, не выбрав вариант ответа, вы должны увидеть сообщение об ошибке.

cd ~/project/mysite
python manage.py runserver 0.0.0.0:8080
Poll voting form interface

Примечание:

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

Это называется условием гонки. Если вы заинтересованы, вы можете прочитать статью avoiding-race-conditions-using-f, чтобы узнать, как можно решить эту проблему.

Используйте общие представления: меньше кода - лучше

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

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

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

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

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

Дальше вы найдете подробности.

Почему перемешивать код?

一般来说,在编写Django应用程序时,您会评估通用视图是否适合您的问题,并从一开始就使用它们,而不是在中途重构代码。但本教程到目前为止有意专注于“艰难地”编写视图,以专注于核心概念。

在开始使用计算器之前,您应该先了解基本数学。

Измените URL-конфигурацию

首先,打开URL-конфигурацию 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>

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

接下来,我们将删除旧的 indexdetailresults 视图,并改用Django的通用视图。为此,打开文件 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):
  ...  ## то же, что и выше, изменения не требуются.

我们在这里使用了两个通用视图:~django.views.generic.list.ListView~django.views.generic.detail.DetailView。相应地,这两个视图分别抽象了“显示对象列表”和“显示特定类型对象的详细页面”的概念。

  • 每个通用视图都需要知道它将作用于哪个模型。这通过 model 属性提供。
  • 通用视图 ~django.views.generic.detail.DetailView 期望从URL捕获的主键值称为 "pk",因此我们将通用视图的 question_id 更改为 pk

默认情况下,通用视图 ~django.views.generic.detail.DetailView 使用名为 <应用名称>/<模型名称>_detail.html 的模板。在我们的例子中,它将使用模板 "polls/question_detail.html"template_name 属性用于告诉Django使用特定的模板名称,而不是自动生成的默认模板名称。我们还为结果列表视图指定了 template_name —— 这确保了结果视图和详细视图在渲染时具有不同的外观,尽管它们在幕后都是 ~django.views.generic.detail.DetailView

类似地,通用视图 ~django.views.generic.list.ListView 使用默认模板 <应用名称>/<模型名称>_list.html;我们使用 template_name 告诉 ~django.views.generic.list.ListView 使用我们现有的 "polls/index.html" 模板。

在本教程的前几部分中,模板已提供了一个上下文,其中包含 questionlatest_question_list 上下文变量。对于 DetailViewquestion 变量会自动提供 —— 因为我们使用的是Django模型 (Question),Django能够为上下文变量确定合适的名称。但是,对于 ListView,自动生成的上下文变量是 question_list。为了覆盖此设置,我们提供了 context_object_name 属性,指定我们希望使用 latest_question_list 代替。作为另一种方法,您可以更改模板以匹配新的默认上下文变量 —— 但告诉Django使用您想要的变量要容易得多。

运行服务器,并使用基于通用视图的新投票应用程序。

Резюме

Поздравляем! Вы завершили лабораторную работу по обработке форм и сокращению кода. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.