Escribir tu primera aplicación Django, parte 4

Este tutorial comienza donde Tutorial 3 dejó. Continuamos con la aplicación web-poll y nos enfocaremos en el procesamiento de formularios y la reducción del código.

¿Dónde obtener ayuda:

Si tienes problemas al seguir este tutorial, por favor dirígete a la sección Ayuda del FAQ.

Escribe un formulario mínimo

Actualicemos nuestro template de detalles de encuesta («polls/detail.html») desde el último tutorial, para que el template contenga un elemento <form> en HTML:

polls/templates/polls/detail.html
<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>

Un rápido resumen:

  • El template anterior muestra un botón de radio para cada opción de pregunta. El valor de cada botón de radio es el ID asociado a la opción de pregunta correspondiente. El nombre de cada botón de radio es "choice". Esto significa que, cuando alguien selecciona uno de los botones de radio y envía el formulario, enviará los datos POST choice=# donde # es el ID de la opción seleccionada. Este es el concepto básico de formularios HTML.

  • Establecemos la acción del formulario en {% url 'polls:vote' question.id %}, y establecemos method="post". Utilizar method="post" (en lugar de method="get") es muy importante, ya que el acto de enviar este formulario alterará datos en el servidor. Cada vez que crees un formulario que altere datos en el servidor, utiliza method="post". Este consejo no es específico de Django; es una buena práctica general en desarrollo web.

  • forloop.counter indica cuántas veces el for tag ha recorrido su bucle

  • Dado que estamos creando un formulario POST (que puede tener el efecto de modificar datos), debemos preocuparnos por las solicitudes cruzadas de falsificación. Por suerte, no tienes que preocuparte demasiado, porque Django viene con un sistema útil para protegerse contra él. En resumen, todos los formularios POST dirigidos a URLs internas deben utilizar la etiqueta de plantilla {% csrf_token %}.

Ahora, creemos una vista de Django que maneje los datos enviados y haga algo con ellos. Recuerda que en el tutorial 3 <Tutorial 3</tutorial03>>, creamos un URLconf para la aplicación de encuestas que incluye esta línea:

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

También creamos una implementación dummy del método vote(). Vamos a crear una versión real. Agrega lo siguiente a polls/views.py:

polls/views.py
from django.db.models import F
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 = F("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 incluye algunas cosas que no hemos cubierto aún en este tutorial:

  • request.POST es un objeto similar a un diccionario que te permite acceder a los datos enviados por clave de nombre. En este caso, request.POST['choice'] devuelve el ID de la elección seleccionada como una cadena. Los valores de request.POST siempre son cadenas.

    Nota que Django también proporciona request.GET para acceder a los datos GET de manera similar, pero estamos utilizando explícitamente request.POST en nuestro código para asegurarnos de que los datos solo se alteren mediante una llamada POST.

  • request.POST['choice'] levantará un KeyError si choice no fue proporcionado en los datos POST. El código anterior verifica el KeyError y vuelve a mostrar la forma de pregunta con un mensaje de error si choice no está dado.

  • F("votes") + 1 instructs the database para aumentar el recuento de votos en 1.

  • Después de incrementar la cuenta de elecciones, el código devuelve un HttpResponseRedirect en lugar de una normal HttpResponse. HttpResponseRedirect toma un solo argumento: la URL a la que se redirigirá al usuario (consulte el siguiente punto para ver cómo construimos la URL en este caso).

    Como indica el comentario de Python, siempre debes devolver un HttpResponseRedirect después de manejar con éxito los datos POST. Este consejo no es específico de Django; es una buena práctica general de desarrollo web.

  • Estamos utilizando la función reverse() en el constructor de HttpResponseRedirect en este ejemplo. Esta función ayuda a evitar tener que codificar directamente una URL en la función de vista. Se le da el nombre de la vista a la que queremos pasar el control y la parte variable de la patrón de URL que apunta a esa vista. En este caso, utilizando el URLconf que establecimos en el tutorial 3 <Tutorial 3</tutorial03>>, esta llamada a reverse() devolverá una cadena como

    "/polls/3/results/"
    

    where el 3 es el valor de question.id. Esta URL redirigida llamará luego al 'results' view para mostrar la página final.

Como se menciona en Tutorial 3, request es un objeto HttpRequest. Para más información sobre objetos HttpRequest, consulte la documentación de solicitud y respuesta: request and response documentation.

Después de que alguien vota en una pregunta, la vista vote() redirige a la página de resultados para la pregunta. Vamos a escribir esa vista.

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 lo mismo que la vista detail() desde Tutorial 3. La única diferencia es el nombre del template. Vamos a solucionar esta redundancia más adelante.

Ahora, crea un 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>

Ahora, ve a /polls/1/ en tu navegador y vota en la pregunta. Deberías ver una página de resultados que se actualiza cada vez que votas. Si envías el formulario sin haber elegido una opción, deberías ver el mensaje de error.

Utiliza vistas genéricas: menos código es mejor

Las vistas detail() (desde Tutorial 3) y results() son muy cortas – y, como se mencionó anteriormente, redundantes. La vista index(), que muestra una lista de encuestas, es similar.

Estas vistas representan un caso común en el desarrollo web básico: obtener datos desde la base de datos según un parámetro pasado en la URL, cargar un template y devolver el template renderizado. Dado que esto es tan común, Django proporciona una herramienta llamada «vistas genéricas» para simplificar este proceso.

Los textos traducidos manteniendo todas las etiquetas intactas son:

Vamos a convertir nuestra aplicación de encuesta para utilizar el sistema de vistas genericas, así podemos eliminar un montón de nuestro propio código. Tendremos que tomar unos pocos pasos para hacer la conversión.

  1. Convertir el URLconf.

  2. Eliminar algunas de las viejas y no necesarias vistas.

  3. Introducir nuevas vistas basadas en las vistas genericas de Django.

Sigue leyendo para obtener detalles.

¿Por qué la reorganización del código?

Generalmente, cuando estás escribiendo una aplicación de Django, evaluarás si las vistas genericas son una buena opción para tu problema y las utilizarás desde el principio, en lugar de refactorizar tu código a mitad de camino. Pero este tutorial ha tenido como objetivo escribir las vistas «de la manera difícil» hasta ahora, para enfocarse en los conceptos básicos.

Debes conocer matemáticas básicas antes de empezar a utilizar un calculadora.

Corregir el URLconf.

Primero, abre el archivo polls/urls.py y haz los cambios como se muestra a continuación:

polls/urls.py
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"),
]

Nota que el nombre del patrón coincidente en las cadenas de ruta de los segundos y terceros patrones ha cambiado de <question_id> a <pk>. Esto es necesario porque vamos a utilizar la vista genérica DetailView para reemplazar nuestras vistas detail() y results(), y ella espera el valor primario capturado desde la URL llamarse "pk".

Ampliar vistas

A continuación, vamos a eliminar nuestras viejas vistas index, detail y results y usar en su lugar las vistas genéricas de Django. Para hacerlo, abre el archivo polls/views.py y haz los cambios como se muestra a continuación:

polls/views.py
from django.db.models import F
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.
    ...

Cada vista genérica necesita saber qué modelo estará actuando sobre. Esto se proporciona utilizando la atributo model (en este ejemplo, model = Question para las vistas DetailView y ResultsView) o definiendo el método get_queryset() (como se muestra en IndexView).

Por defecto, la vista genérica DetailView utiliza un template llamado <app name>/<model name>_detail.html. En nuestro caso, utilizaría el template "polls/question_detail.html". El atributo template_name se utiliza para decir a Django que utilice un nombre de template específico en lugar del nombre de template generado por defecto. También especificamos el template_name para la vista lista de resultados – esto asegura que las vistas de resultados y detalles tengan una apariencia diferente cuando se rendericen, aunque estén siendo ambas una DetailView detrás de escena.

De manera similar, la vista genérica ListView utiliza un template por defecto llamado <app name>/<model name>_list.html; utilizamos template_name para decir a ListView que utilice nuestro existente template "polls/index.html".

En las partes anteriores del tutorial, los templates se han proporcionado con un contexto que contiene las variables de contexto question y latest_question_list. Para la vista DetailView, la variable de contexto question se proporciona automáticamente – ya que estamos utilizando un modelo de Django (Question), Django es capaz de determinar un nombre apropiado para la variable de contexto. Sin embargo, para ListView, la variable de contexto generada automáticamente es question_list. Para sobreescribir esto proporcionamos el atributo context_object_name, especificando que queremos utilizar latest_question_list en su lugar. Como alternativa, podrías cambiar tus templates para coincidir con las nuevas variables de contexto por defecto – pero es mucho más fácil decir a Django que utilice la variable que quieres.

Ejecuta el servidor y utiliza tu nueva aplicación de encuestas basada en vistas genéricas.

Para obtener detalles completos sobre vistas genéricas, consulta la documentación de vistas genéricas.

When estás cómodo con las formas y las vistas generales, lee parte 5 de esta tutoría para aprender a probar nuestra aplicación de encuestas.