sábado, 17 de março de 2018

Tutorial 6 de 10

Codificando uma app simples

Continuando...

Criação de um Formulário:

Atualize o template de detalhe da enquete:
'templates/votacao/detail.html'
último tutorial, para que ele contenha um elemento HTML <form>:

<h1>{{ questao.texto_questao }}</h1>

{% if error_message %}
<p><strong>{{ error_message }}</strong></p>
{% endif %}

<form action="{% url 'votacao:vote' questao.id %}" method="post">
{% csrf_token %}
{% for escolha in questao.escolha_set.all %}
<input type="radio" name="escolha_form" id="escolha{{ forloop.counter }}" value="{{ escolha.id }}" />
<label for="escolha{{ forloop.counter }}"> {{ escolha.texto_escolha }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>
-------------------------------------------------------------------
Rápida explicação:
O template acima exibe um Radio Button para cada opção da Enquete, acompanhado de um Label.
O 'value' de cada Radio Button está associado ao ID da opção. 
O 'name' de cada Radio Button é a referência de 'escolha'. Quando o formulário for submetido, vai enviar o dado 'escolha=#' por POST, onde # é o seu value (ID da escolha selecionada).

Foi definido o parâmetro action do formulário para 
{% url'votacao:vote' questao.id %}, questao.id é parâmetro para a URL.

O comando forloop.counter indica quantas vezes a tag 'for' passou pelo laço.

Desde que estamos criando um formulário POST (que pode ter o efeito de modificação de dados), temos que nos preocupar com "Cross Site Request Forgeries". Felizmente, o Django vem com um sistema muito fácil de usar para se proteger contra isto. 
Para tal, todas as formas de POST que são direcionadas a URLs internas devem usar a tag {% csrf_token%} de template.
-------------------------------------------------------------------
Codifique a nova view vote em votacao/views.py:

...
from django.http import HttpResponseRedirect
from django.urls import reverse

from .models import Questao, Escolha
...
def vote(request, questao_id):
    questao = get_object_or_404(Questao, pk=questao_id)
    try:
        escolha_marcada = questao.escolha_set.get(pk=request.POST['escolha_form'])
    except (KeyError, Escolha.DoesNotExist):
        # Reexibe o formulário de Questão para votação.
        return render(request, 'votacao/detail.html', {
            'questao': questao,
            'error_message': "Você não selecionou uma escolha.",
        })
    else:
        escolha_marcada.votos += 1
        escolha_marcada.save()
        return HttpResponseRedirect(reverse('votacao:results', args=(questao.id,)))
...        

Dica: Sempre retorne um HttpResponseRedirect após lidar com envio de dados com POST. Previne os dados de serem postados 2 vezes se o usuário voltar com o botão Retornar.
--------------------------------------------------------------------
Explicações gerais:

request.POST é um objeto como dicionários que lhe permite acessar os dados submetidos pelos seus nomes chaves. 
Neste caso, request.POST['escolha_form'] retorna o 'value' da opção selecionada, sempre como uma string.

Django também fornece request.GET para acessar dados GET da mesma 
forma.

request.POST['escolha_form'] irá levantar a exceção KeyError caso uma escolha não seja fornecida via dados POST. O código acima checa por KeyError e reexibe o formulário da enquete com as mensagens de erro.

Após incrementar uma opção (escolha), o código retorna um class: django.http.HttpResponseRedirect em vez de um normal HttpResponse. 

HttpResponseRedirect recebe um único argumento: a URL para o qual o usuário será redirecionado.

Você deve sempre retornar uma HttpResponseRedirect depois de lidar com sucesso com dados POST. Esta dica não é específica do Django; é uma boa prática para o desenvolvimento Web.

Estamos usando a função reverse() no construtor da HttpResponseRedirect neste exemplo. Essa função nos ajuda a evitar colocar a URL dentro da view de maneira literal. A ele é dado então o nome da view que queremos que ele passe o controle e a parte variável do padrão de formato da URL que aponta para a view. Neste caso, usando o URLconf definida anteriormente, esta chamada de reverse() irá retornar uma string como '/votacao/3/results/'
onde o 3 é o valor para questao.id. Esta URL redirecionada irá então chamar a view 'results' para exibir a página final.

Lembre-se de que request é um objeto HttpRequest object. 
--------------------------------------------------------------------
Depois que alguém votar em uma enquete, a view vote() redireciona para a página de resultados da enquete. 
Codifique em views.py a nova view results:

def results(request, questao_id):
    questao = get_object_or_404(Questao, pk=questao_id)
    return render(request, 'votacao/results.html', {'questao': questao})
--------------------------------------------------------------------
Crie o template votacao/results.html. Codifique:

<h1>{{ questao.texto_questao }}</h1>

<ul>
{% for escolha in questao.escolha_set.all %}
    <li>{{ escolha.texto_escolha }} -- {{ escolha.votos }} voto{{ escolha.votos|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'votacao:detail' questao.id %}">Votar de novo?</a>
--------------------------------------------------------------------
Agora, vá para /votacao/1/ no browser e vote em uma enquete. Você deverá ver uma página de resultados que será atualizada cada vez que você votar. Se você enviar o formulário sem ter escolhido uma opção, você deverá ver a mensagem de erro.

Obs.: O código da view vote() tem um pequeno problema. Ele primeiro pega o objeto de selected_escolha do banco de dados, calcula o novo valor de votes e os salva novamente. Se dois usuários do seu site tentarem votar ao mesmo tempo: o mesmo valor, digamos 42, será obtido para votos. Então, para ambos usuários, o novo valor 43 é calculado e salvo, quando o valor esperado seria 44. Isto é chamado de condição de concorrência. Se você estiver interessado, pode ler "Avoiding race conditions using F()" para aprender a resolver este problema.
--------------------------------------------------------------------
Views Genéricas:

Em nossa app, as views detail() e results() são muito simples e redundantes. A view index(), que mostra a lista de votações, é similar.

Estas views representam um caso comum do desenvolvimento Web básico: obter dados do banco de dados de acordo com um parâmetro passado na URL, carregar um template e devolvê-lo renderizado. 
Por isto ser muito comum, o Django fornece um atalho, chamado sistema de “views genéricas”.

Views genéricas abstraem padrões comuns para um ponto onde você nem precisa escrever código Python para escrever uma aplicação.
Vamos converter a nossa app de enquete para utilizar o sistema de views genéricas, por isso podemos excluir um monte do nosso próprio código. 
Vamos executar alguns passos para fazer a conversão:
1. Alterar o URLconf.
2. Deletar algumas das views antigas, desnecessárias.
3. Introduzir novas views baseadas em views genéricas.

Obs.: Geralmente, quando estiver escrevendo uma aplicação Django, você vai avaliar se views genéricas são uma escolha adequada para o seu problema e você irá utilizá-las desde o início em vez de refatorar seu código no meio do caminho. 
--------------------------------------------------------------------
Alterar URLconf:
Edite o arquivo votacao/urls.py:

...
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:questao_id>/vote/', views.vote, name='vote'),
]
...

Note que o 1º parâmetro do path (pattern de ligação) do 2º e 3º path foi alterado de <questao_id> para <pk>.

Agora, remova as views index, detail e results. Vamos usar views genéricas em seu lugar.
Edite o arquivo votacao/views.py:

...
from django.views import generic

class IndexView(generic.ListView):
    template_name = 'votacao/index.html'
    context_object_name = 'latest_questao_list'

    def get_queryset(self):
        """Retorna as últimas 5 questões publicadas."""
        return Questao.objects.order_by('-data_pub')[:5]

class DetailView(generic.DetailView):
    model = Questao
    template_name = 'votacao/detail.html'

class ResultsView(generic.DetailView):
    model = Questao
    template_name = 'votacao/results.html'
...
--------------------------------------------------------------------
Nós usamos 2 views genéricas: ListView e DetailView. Respectivamente, essas duas views abstraem o conceito de exibir uma lista de objetos e exibir uma página de detalhe para um tipo particular de objeto.

• Cada view genérica precisa saber qual é o modelo que ela vai agir. Isto é fornecido usando o atributo model.
• A view genérica DetailView espera o valor de chave primaria capturado da URL chamada "pk", então mudamos questao_id para pk para as views genéricas.

Por padrão, a view genérica DetailView utiliza um template chamado <appname>/<model name>_detail.html

Em nosso caso, ela vai utilizar o template "votacao/questao_detail.html". O atributo template_name é usado para indicar ao Django que use um nome de template em vez de auto gerar uma nome de template. 

Nós também especificamos template_name para a view de listagem de results – isto garante que a view de detalhe tem uma aparencia quando rederizada, mesmo que sejam tanto um class: django.views.generic.detail.DetailView por trás das cenas.
Semelhantemente, a view genérica ListView utiliza um template chamado <appname>/<model name>_list.html; Nós usamos template_name para informar ListView para usar nossos templates "votacao/index.html".

Os templates tem sido fornecidos com um contexto que contém as variáveis questao e latest_questao_list. 
Para a DetailView a variavel questao é fornecida automaticamente – já que estamos usando um model Django (Questao), Django é capaz de determinar um nome apropriado para a variável context.

Contudo, para for ListView, o context automaticamente gerado é a variável questao_list. Para sobreescrever esta nós fornecemos o atributo context_object_name, especificando que nós queremos usar latest_questao_list. 

Execute o servidor, e faça testes. O resultado deve ser o mesmo.
--------------------------------------------------------------------

Nenhum comentário:

Postar um comentário

Tutorial 10 de 10

Codificando uma app simples Continuando a alteração no admin... Personalizando a listagem da página de Administração: Vamos fazer melh...