Django

Formularios y vistas genéricas

Escribiendo un formulario simple

Actualizamos la plantilla encuestas/detalle.html

<h1>{{ pregunta.texto_pregunta }}</h1>

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

<form action="{% url 'encuestas:votar' pregunta.id %}" method="post">
{% csrf_token %}
{% for eleccion in pregunta.eleccion_set.all %}
    <input type="radio" name="eleccion" id="eleccion{{ forloop.counter }}" 
        value="{{ eleccion.id }}" />
    <label for="eleccion{{ forloop.counter }}">
        {{ eleccion.texto_opcion }}
    </label><br />
{% endfor %}
<input type="submit" value="Votar" />
</form>

Un análisis rápido

  • El template de arriba muestra un radio button para cada opción de la encuesta. El value de cada radio es el ID asociado a cada opción de la encuesta. Cuando alguien elige una de las opciones y envía el form, se envía eleccion=3 como data del POST.
  • Establecemos como action del form {% url 'encuestas:votar' pregunta.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".

Un análisis rápido

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

Armamos nuestra vista

Ahora creamos la versión real de nuestra vista votar(). Agregar lo siguiente a encuestas/views.py:

# -*- coding: utf-8 -*-
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse

from encuestas.models import Eleccion, Pregunta
# ...
def votar(request, pregunta_id):
    p = get_object_or_404(Pregunta, pk=pregunta_id)
    try:
        opcion_elegida = p.eleccion_set.get(pk=request.POST['opcion'])
    except (KeyError, Eleccion.DoesNotExist):
        # Volver a mostrar el formulario para votar.
        return render(request, 'encuestas/detalle.html', {
            'pregunta': p,
            'error_message': u"No seleccionaste una opción.",
        })
    else:
        opcion_elegida.votos += 1
        opcion_elegida.save()
        # siempre retornar un HttpResponseRedirect después de lidiar
        # exitosamente con datos de POST. Esto evita que los datos sean
        # posteados 2 veces si el usuario presiona el botón de Atrás.
        return HttpResponseRedirect(reverse('encuestas:resultados', args=(p.id,)))

Lo nuevo de este código:

  • request.POST es un objeto tipo diccionario, nos permite acceder a los datos enviados usando los nombres como clave. Aquí request.POST['choice'] devuelve el ID de la opción elegida. Los valores de request.POST son siempre strings.

    Django también provee request.GET para acceder a los datos en el GET.

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

Lo nuevo de este código:

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

    Uno siempre debería devolver un HttpResponseRedirect después de manejar exitosamente un POST.

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

Mostrando los resultados

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

from django.shortcuts import get_object_or_404, render


def resultados(request, pregunta_id):
    pregunta = get_object_or_404(Pregunta, pk=pregunta_id)
    return render(request, 'encuestas/resultados.html', {'pregunta': pregunta})

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

Plantilla de resultados

encuestas/resultados.html

<h1>{{ pregunta.texto_pregunta }}</h1>

<ul>
{% for opcion in pregunta.eleccion_set.all %}
    <li>{{ opcion.texto_opcion }} -- 
        {{ opcion.votos }} voto{{ opcion.votos|pluralize }}
    </li>
{% endfor %}
</ul>

<a href="{% url 'encuestas:detalle' pregunta.id %}">Votar de nuevo?</a>

Vamos a /encuestas/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.

Vistas genéricas: Menos código es mejor

Las views detalle() resultados() son súper simples – y redundantes. La view index(), que muestra una lista de encuestas, 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”.

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

Ajustar el URLConf

Abrimos el URLconf encuestas/urls.py y lo cambiamos de la siguiente manera:

from django.conf.urls import patterns, url

from encuestas import views

urlpatterns = patterns('',
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>\d+)/$', views.DetalleView.as_view(), name='detalle'),
    url(r'^(?P<pk>\d+)/resultados/$', views.ResultadosView.as_view(), 
        name='resultados'),
    url(r'^(?P<pregunta_id>\d+)/votar/$', views.votar, name='votar'),
)

Notar que el nombre del patrón encontrado en las expresiones regulares del segundo y tercer patrón ha cambiado de <pregunta_id> a <pk>.

Ajustar Vistas

Cambiamos encuestas/views.py a:

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 encuestas.models import Eleccion, Pregunta


class IndexView(generic.ListView):
    template_name = 'encuestas/index.html'
    context_object_name = 'lista_ultimas_preguntas'

    def get_queryset(self):
        """Retorna las ultimas 5 preguntas."""
        return Pregunta.objects.order_by('-pub_date')[:5]


class DetalleView(generic.DetailView):
    model = Pregunta
    template_name = 'encuestas/detalle.html'


class ResultadosView(generic.DetailView):
    model = Pregunta
    template_name = 'encuestas/resultados.html'


def votar(request, pregunta_id):
    ... # lo dejamos igual

Vistas genéricas

Estamos usando dos views genéricas: ListView y DetailView. Estas 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 parámetro model.
  • La view genérica DetailView espera el valor de clave primaria capturado de la URL con nombre "pk", entonces cambiamos pregunta_id a pk.

 

Vistas genéricas

  • Por defecto, la view genérica DetailView usa un template llamado <nombreapp>/<nombremodelo>_detail.html.
  • En nuestro caso, usará el template "encuestas/ppregunta_detail.html". El argumento template_name es usado para indicarle a Django que use un template de nombre específico en lugar del nombre 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.
  • La view genérica ListView usa un template por defecto llamado <nombreapp>/<nombremodelo>_list.html.

Vistas genéricas

  • En partes anteriores de este tutorial, los templates recibían un contexto que contenía las variables pregunta y lista_ultimas_preguntas.
  • Para DetailView la variable pregunta es provista automáticamente – como estamos usando un modelo (Pregunta), Django puede determinar un nombre adecuado para la variable de contexto.
  • Pero para ListView, el nombre de variable de contexto generado automáticamente es pregunta_list.
  • Para sobreescribir esto, pasamos la opción context_object_name, especificando que queremos usar lista_ultimas_preguntas 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.

Vistas genéricas

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.

Vistas genéricas sin vistas!

En algunos casos no es necesario ni siquiera escribir las views. Por ejemplo DetalleView sólo requiere el nombre, cambiamos su url:

from django.views.generic import DetailView, ListView
from encuestas.models import Pregunta

urlpatterns = patterns('',


    # ... Dejamos las otras urls como estaban

    url(r'^(?P<pk>\d+)/$',
        DetailView.as_view(
            model=Pregunta,
            template_name='encuestas/detalle.html'),
        name='detalle'),

    # ...
)

y ahora podemos borrar DetailView

Django - Formularios y vistas genéricas

By Guillermo Nuñez

Django - Formularios y vistas genéricas

  • 3,308