Escribiendo tu primera Django app, parte 4

Este tutorial comienza donde dejó el Tutorial 3. Vamos a continuar la aplicación de preguntas concentrándonos en procesar forms simples y reducir nuestro código.

Escribir un form simple

Actualicemos el template de detalle de una encuesta (“polls/detail.html”) del último tutorial para que contenga un elemento HTML <form>:

polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>

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

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% 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 %}
<input type="submit" value="Vote" />
</form>

Un repaso rápido:

  • El template de arriba muestra un radio button para cada opción de la pregunta. El value de cada radio es el ID asociado a cada opción de la pregunta. El name, es "choice". Esto significa que cuando alguien elige una de las opciones y envía el form, se envía choice=# como data del POST, donde # es el ID de la opción elegida. Este es un concepto básico de forms HTML.

  • Establecemos como action del form {% url 'polls:vote' question.id %}, y method="post". Usar method="post" (en contraposición a method="get") es muy importante, porque la acción de enviar el form va a modificar datos del lado del servidor. Cada vez que uno crea un form que altere datos del lado del servidor, usar method="post". Este consejo no es particular para Django; es una buena práctica de desarrollo web.

  • forloop.counter indica cuantas veces el tag for iteró en el ciclo

  • Como estamos creando un form POST (que puede tener el efecto de modificar datos), necesitamos preocuparnos por Cross Site Request Forgeries (CSRF). Afortunadamente no hace falta demasiado, porque Django viene con un sistema fácil de usar para protegerse contra este tipo de ataques. Simplemente todos los forms que hagan POST contra una URL interna deberían usar el template tag {% csrf_token %}.

Ahora vamos a crear una view Django que maneje los datos enviados y haga algo con ellos. Recordemos que en el Tutorial 3 creamos un URLconf para nuestra app que incluía la siguiente línea:

polls/urls.py
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),

También habíamos creado una implementación boba de la función vote(). Hagamos una implementación real. Agregamos lo siguiente a polls/views.py:

polls/views.py
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    p = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': p,
            '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=(p.id,)))

Este código incluye algunas cosas que no hemos cubierto hasta el momento:

  • request.POST es un objeto diccionario-like que nos permite acceder a los datos enviados usando los nombres como clave. En este caso request.POST['choice'] devuelve el ID de la opción elegida, como string. Los valores de request.POST son siempre strings.

    Notar que Django también provee request.GET para acceder a los datos en el GET de la misma manera – pero estamos usando explícitamente request.POST en nuestro código para asegurarnos de que los datos solamente se alteren vía una llamada POST.

  • request.POST['choice'] va a levantar KeyError si choice no estuviera en los datos del POST. El código de arriba chequea por esta excepción y en ese caso vuelve a mostrar el form con un mensaje de error.

  • Después de incrementar el contador de la opción, el código devuelve un HttpResponseRedirect en lugar de un HttpResponse. HttpResponseRedirect toma un único argumento: la URL a la que el usuario será redirigido (ver el punto siguiente sobre cómo construir la URL en este caso).

    Como dice el comentario en el código de arriba, uno siempre debería devolver un HttpResponseRedirect después de manejar exitosamente un POST. Este consejo no es específico a Django; es una buena práctica de desarrollo web.

  • Estamos usando la función reverse() en el constructor de HttpResponseRedirect. Esta función nos ayuda a no escribir explícitamente una URL en la función de view. Se le pasa el nombre de la view a la que queremos pasar el control y los argumentos variables del patrón de URL que apunta a esa view. En este caso, usando el URLconf que configuramos en el Tutorial 3, esta llamada a reverse() devolvería un string como el siguiente:

    '/polls/3/results/'
    

    ... donde 3 es el valor de p.id. Esta URL nos va a redirigir, llamando a la view 'results' para mostrar la página final.

Como se mencionó en el Tutorial 3, request es un objeto HttpRequest. Para más detalles sobre este tipo de objetos, se puede ver la documentación sobre request y response.

Después de que alguien vota en una encuesta, la view vote() lo redirige a la página de resultados de la pregunta. Escribamos esta view:

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

Esto es casi exactamente igual a la view detail() del Tutorial 3. La única diferencia es el nombre del template. Vamos a solucionar esta redundancia luego.

Ahora, creamos el template 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>

Vamos a /polls/1/ en el browser y votamos en la encuesta. Deberíamos ver una página de resultados que se actualiza cada vez que uno vota. Si se envía el form sin elegir una opción, se debería mostrar un mensaje de error.

Usando views genéricas (generic views): Menos código es mejor

Las views detail() (del Tutorial 3) y results() son súper simples – y, cómo se menciona arriba, redundantes. La view index() (también del Tutorial 3), que muestra una lista de preguntas, es similar.

Estas views representan un caso común en el desarrollo web básico: obtener datos de una base de datos de acuerdo a un parámetro pasado en la URL, cargar un template y devolver el template renderizado. Por ser tan usual, Django provee un atajo, el sistema de “generic views” (views genéricas).

Las views genéricas abstraen patrones comunes, al punto en que uno no necesita prácticamente escribir código Python en una app.

Vamos a convertir nuestra app para usar views genéricas, y poder borrar parte de nuestro código original. Son solamente unos pocos pasos:

  1. Convertir el URLconf.

  2. Borrar algunas de las views que teníamos, ya no necesarias.

  3. Arreglar el manejo de URL para las nuevas views.

Para más detalles, seguir leyendo.

Por qué cambiar nuestro código?

Generalmente, cuando uno escribe una app Django, evaluará si usar views genéricas es una buena solución para el problema en cuestión, y las utilizará desde el comienzo, en lugar de refactorear el código a mitad de camino. Este tutorial intencionalmente se centró en escribir views “sin ayuda” hasta ahora, para aprender los conceptos principales.

Uno debería aprender matemática básica antes de empezar a usar una calculadora.

Arreglando URLconf

Primero, abrimos el URLconf polls/urls.py y lo cambiamos de la siguiente manera:

polls/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
    url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Notar que el nombre del patrón que se busca en las segunda y tercera expresiones regulares cambió de <question_id> a <pk>.

Arreglando views

A continuación vamos a borrar nuestras viejas views index, detail, y results para usar las generic views de Django en vez. Para ello, abrimos el archivo polls/views.py y cambiamos:

polls/views.py
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers 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

Estamos usando dos views genéricas: ListView y DetailView. Estas dos views nos abstraen de los conceptos de “mostrar una lista de objetos” y “mostrar el detalle de un objeto particular”, respectivamente.

  • Cada view genérica necesita saber sobre qué modelo actuar. Esto se define usando el atributo model.

  • La view genérica DetailView espera el valor de clave primaria capturado de la URL con nombre \"pk\", entonces cambiamos question_id a pk.

Por defecto, la view genérica DetailView usa un template llamado <app name>/<model name>_detail.html. En nuestro caso, usará el template "polls/question_detail.html". El argumento template_name es usado para indicarle a Django que use un template de nombre específico en lugar de usar el nombre de template autogenerado por defecto. También especificamos template_name para la view results – esto nos asegura que la view de resultados y la de detalle tiene un aspecto diferente al renderizarse, aún cuando ambas usan DetailView por detrás.

De forma similar, la view genérica ListView usa un template por defecto llamado <app name>/<model name>_list.html; usamos template_name para indicarle a ListView que use el template ya existente "polls/index.html".

En partes anteriores de este tutorial, los templates recibían un contexto que contenía las variables question y latest_question_list. Para DetailView la variable question es provista automáticamente – como estamos usando un modelo Django (Question), Django puede determinar un nombre adecuado para la variable de contexto. Sin embargo, para ListView, el nombre de variable de contexto generado automáticamente es question_list. Para sobreescribir este valor, pasamos la opción context_object_name, especificando que queremos usar latest_question_list como nombre. Otra alternativa sería cambiar los templates para adecuarlos a los nombres por defecto – pero es mucho más fácil decirle a Django que use el nombre de variable que queremos.

Corremos el servidor, y usamos nuestra app, ahora basada en views genéricas.

Para más detalles sobre views genéricas, se puede ver la documentación sobre views genéricas.

Una vez que estés cómodo con forms y views genéricas, podés leer la parte 5 de este tutorial para aprender sobre cómo testear nuestra app de preguntas.