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>
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,)))
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.
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.
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.
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.
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”.
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:
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>.
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
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.
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.
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