Processamento de Formulários e Redução de Código

Beginner

Introdução

Este tutorial começa onde Criando as Visões da Interface Pública parou. Estamos continuando com a aplicação de enquete web e focaremos no processamento de formulários e na redução do nosso código.

Escreva um formulário mínimo

Vamos atualizar nosso template de detalhes da enquete (polls/detail.html) do último tutorial, para que o template contenha um elemento 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>

Uma rápida análise:

  • O template acima exibe um botão de rádio para cada opção da pergunta. O value de cada botão de rádio é o ID da opção da pergunta associada. O name de cada botão de rádio é "choice". Isso significa que, quando alguém seleciona um dos botões de rádio e envia o formulário, ele enviará os dados POST choice=#, onde ## é o ID da opção selecionada. Este é o conceito básico de formulários HTML.
  • Definimos o action do formulário como {% url 'polls:vote' question.id %}, e definimos method="post". Usar method="post" (em oposição a method="get") é muito importante, porque o ato de enviar este formulário alterará dados no lado do servidor. Sempre que você criar um formulário que altera dados no lado do servidor, use method="post". Esta dica não é específica do Django; é uma boa prática de desenvolvimento web em geral.
  • forloop.counter indica quantas vezes a tag for passou por seu loop
  • Como estamos criando um formulário POST (que pode ter o efeito de modificar dados), precisamos nos preocupar com Cross Site Request Forgeries. Felizmente, você não precisa se preocupar muito, porque o Django vem com um sistema útil para se proteger contra isso. Em resumo, todos os formulários POST que são direcionados a URLs internas devem usar a tag de template {% csrf_token %}<csrf_token>.

Agora, vamos criar uma view do Django que lida com os dados enviados e faz algo com eles. Lembre-se, em **Criando as Visões da Interface Pública**, criamos um URLconf para a aplicação de enquetes que inclui esta linha:

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

Também criamos uma implementação dummy da função vote(). Vamos criar uma versão real. Adicione o seguinte a 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,)))

Este código inclui algumas coisas que ainda não cobrimos neste tutorial:

  • request.POST <django.http.HttpRequest.POST> é um objeto semelhante a um dicionário que permite acessar dados enviados por nome de chave. Neste caso, request.POST['choice'] retorna o ID da opção selecionada, como uma string. Os valores de request.POST <django.http.HttpRequest.POST> são sempre strings.

    Observe que o Django também fornece `request.GET

<django.http.HttpRequest.GET>para acessar dados GET da mesma forma - mas estamos usando explicitamenterequest.POST <django.http.HttpRequest.POST>` em nosso código, para garantir que os dados sejam alterados apenas por meio de uma chamada POST.

  • request.POST['choice'] levantará KeyError se choice não foi fornecido nos dados POST. O código acima verifica KeyError e exibe novamente o formulário da pergunta com uma mensagem de erro se choice não for fornecido.

  • Após incrementar a contagem de opções, o código retorna um ~django.http.HttpResponseRedirect em vez de um ~django.http.HttpResponse normal. ~django.http.HttpResponseRedirect recebe um único argumento: a URL para a qual o usuário será redirecionado (veja o ponto seguinte para saber como construímos a URL neste caso).

    Como o comentário em Python acima aponta, você sempre deve retornar um ~django.http.HttpResponseRedirect após lidar com sucesso com os dados POST. Esta dica não é específica do Django; é uma boa prática de desenvolvimento web em geral.

  • Estamos usando a função ~django.urls.reverse no construtor ~django.http.HttpResponseRedirect neste exemplo. Esta função ajuda a evitar ter que codificar uma URL no código da view. Ela recebe o nome da view para a qual queremos passar o controle e a parte variável do padrão de URL que aponta para essa view. Neste caso, usando o URLconf que configuramos em **Criando as Visões da Interface Pública**, esta chamada ~django.urls.reverse retornará uma string como:

    "/polls/3/results/"
    

    onde o 3 é o valor de question.id. Esta URL redirecionada então chamará a view 'results' para exibir a página final.

Como mencionado em **Criando as Visões da Interface Pública**, request é um objeto ~django.http.HttpRequest. Para mais informações sobre objetos ~django.http.HttpRequest, consulte a documentação de request and response </ref/request-response>.

Depois que alguém vota em uma pergunta, a view vote() redireciona para a página de resultados da pergunta. Vamos escrever essa view:

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})

Esta é quase exatamente a mesma da view detail() de Criando as Visões da Interface Pública. A única diferença é o nome do template. Vamos corrigir essa redundância mais tarde.

Agora, crie um template 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>

Agora, vá para /polls/1/ no seu navegador e vote na pergunta. Você deve ver uma página de resultados que é atualizada cada vez que você vota. Se você enviar o formulário sem ter escolhido uma opção, você deve ver a mensagem de erro.

cd ~/project/mysite
python manage.py runserver 0.0.0.0:8080

Poll voting form interface

Observação:

O código para nossa view vote() tem um pequeno problema. Ele primeiro obtém o objeto selected_choice do banco de dados, então calcula o novo valor de votes e, em seguida, o salva de volta no banco de dados. Se dois usuários do seu site tentarem votar exatamente ao mesmo tempo, isso pode dar errado: O mesmo valor, digamos 42, será recuperado para votes. Então, para ambos os usuários, o novo valor de 43 é calculado e salvo, mas 44 seria o valor esperado.

Isso é chamado de condição de corrida (race condition). Se você estiver interessado, pode ler avoiding-race-conditions-using-f para aprender como você pode resolver esse problema.

Use generic views: Menos código é melhor

As views detail() (de **Criando as Visões da Interface Pública**) e results() são muito curtas -- e, como mencionado acima, redundantes. A view index(), que exibe uma lista de enquetes, é semelhante.

Essas views representam um caso comum de desenvolvimento web básico: obter dados do banco de dados de acordo com um parâmetro passado na URL, carregar um template e retornar o template renderizado. Como isso é tão comum, o Django fornece um atalho, chamado de sistema de "views genéricas".

As views genéricas abstraem padrões comuns a ponto de você nem precisar escrever código Python para escrever um aplicativo.

Vamos converter nosso aplicativo de enquete para usar o sistema de views genéricas, para que possamos excluir um monte de nosso próprio código. Teremos que seguir algumas etapas para fazer a conversão. Nós iremos:

  1. Converter o URLconf.
  2. Excluir algumas das views antigas e desnecessárias.
  3. Introduzir novas views baseadas nas views genéricas do Django.

Leia para obter detalhes.

Por que a troca de código?

Geralmente, ao escrever um aplicativo Django, você avaliará se as views genéricas são adequadas para o seu problema e as usará desde o início, em vez de refatorar seu código no meio do caminho. Mas este tutorial intencionalmente se concentrou em escrever as views "da maneira difícil" até agora, para focar nos conceitos principais.

Você deve saber matemática básica antes de começar a usar uma calculadora.

Amend URLconf (Alterar URLconf)

Primeiro, abra o URLconf polls/urls.py e altere-o assim:

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"),
]

Observe que o nome do padrão correspondente nas strings de caminho dos segundo e terceiro padrões mudou de <question_id> para <pk>.

Amend views (Alterar views)

Em seguida, vamos remover nossas antigas views index, detail e results e usar as views genéricas do Django em vez disso. Para fazer isso, abra o arquivo polls/views.py e altere-o assim:

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.

Estamos usando duas views genéricas aqui: ~django.views.generic.list.ListView e ~django.views.generic.detail.DetailView. Respectivamente, essas duas views abstraem os conceitos de "exibir uma lista de objetos" e "exibir uma página de detalhes para um tipo específico de objeto".

  • Cada view genérica precisa saber em qual model ela estará atuando. Isso é fornecido usando o atributo model.
  • A view genérica ~django.views.generic.detail.DetailView espera que o valor da chave primária capturado da URL seja chamado "pk", então mudamos question_id para pk para as views genéricas.

Por padrão, a view genérica ~django.views.generic.detail.DetailView usa um template chamado <nome do aplicativo>/<nome do model>_detail.html. Em nosso caso, ele usaria o template "polls/question_detail.html". O atributo template_name é usado para dizer ao Django para usar um nome de template específico em vez do nome do template padrão autogerado. Também especificamos o template_name para a view de lista results -- isso garante que a view de resultados e a view de detalhes tenham uma aparência diferente quando renderizadas, embora ambas sejam um ~django.views.generic.detail.DetailView nos bastidores.

Da mesma forma, a view genérica ~django.views.generic.list.ListView usa um template padrão chamado <nome do aplicativo>/<nome do model>_list.html; usamos template_name para dizer ao ~django.views.generic.list.ListView para usar nosso template "polls/index.html" existente.

Nas partes anteriores do tutorial, os templates foram fornecidos com um contexto que contém as variáveis de contexto question e latest_question_list. Para DetailView, a variável question é fornecida automaticamente -- como estamos usando um model Django (Question), o Django é capaz de determinar um nome apropriado para a variável de contexto. No entanto, para ListView, a variável de contexto gerada automaticamente é question_list. Para substituir isso, fornecemos o atributo context_object_name, especificando que queremos usar latest_question_list em vez disso. Como uma abordagem alternativa, você pode alterar seus templates para corresponder às novas variáveis de contexto padrão -- mas é muito mais fácil dizer ao Django para usar a variável que você deseja.

Execute o servidor e use seu novo aplicativo de enquete baseado em views genéricas.

Resumo

Parabéns! Você concluiu o laboratório de Processamento de Formulários e Redução de Código. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.