Escribiendo su primera aplicación en Django, parte 4

Este tutorial comienza donde quedó el Tutorial 3. Vamos a continuar la aplicación Web encuestas y nos enfocaremos en el procesamiento simple de formularios y en la reducción de nuestro código.

Escriba un formulario sencillo

Vamos a actualizar nuestra plantilla de detalles de encuestas («polls/detail.html») a partir del último tutorial, de modo que la plantilla 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 resumen rápido:

  • La plantilla anterior muestra un botón de opción para cada pregunta de selección. El value de cada botón de opción es la ID de la pregunta de selección asociada. El name de cada botón de opción es "choice". Esto significa que cuando alguien selecciona uno de los botones de opción y envía el formulario, enviará los datos POST choice=# donde # es la ID de la opción seleccionada. Este es el concepto básico de los formularios HTML.
  • Ajustamos la action del formulario a {% url 'polls:vote' question.id %}, y configuramos method="post". Utilizar method="post" (en lugar de method="get") es muy importante porque enviar este formulario modificará los datos del lado del servidor. Cuando cree un formulario que altere los datos del lado del servidor, utilice method="post". Esta sugerencia no es solo para Django, es solo una buena práctica de desarrollo Web.
  • `` forloop.counter`` indica cuántas veces la etiqueta for ha pasado por el bucle
  • Ya que estamos creando un formulario POST (que puede tener el efecto de modificar los datos) tenemos que preocuparnos de las falsificaciones de peticiones entre sitios. Afortunadamente, no tiene que preocuparse demasiado porque Django cuenta con un sistema muy fácil de usar para protegerse contra ello. En pocas palabras, todos los formularios POST que están dirigidos a las URLs internas deben utilizar la etiqueta de plantilla {% csrf_token %}.

A continuación, vamos a crear una vista Django que maneje los datos enviados y haga algo con ello. Recuerde que en el Tutorial 3, creamos un URLconf para la aplicación encuestas que incluye esta línea:

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

También creamos una implementación simulada de la función vote(). Vamos a crear una versión real. Agregue 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.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 += 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 todavía en este tutorial:

  • request.POST es un objeto tipo diccionario que le permite acceder a los datos presentados mediante el nombre clave. En este caso, request.POST['choice'] retorna la ID de la opción seleccionada como una cadena. Los valores request.POST son siempre cadenas.

    Tenga en cuenta que Django también proporciona request.GET para acceder a datos GET de la misma manera, pero estamos usando de manera explícita request.POST en nuestro código, para garantizar que los datos sólo se modifiquen a través de una llamada POST.

  • request.POST['choice'] levantará un excepción :exc: KeyError si choice no fue proporcionado en los datos POST. El código anterior busca la excepción KeyError y muestra de nuevo el formulario de pregunta con un mensaje de error si no se ha proporcionado choice.

  • Después de incrementar el conteo de la elección, el código retorna una HttpResponseRedirect en lugar de una HttpResponse normal HttpResponseRedirect toma un único argumento: La URL a la que el usuario será redirigido (vea el siguiente aspecto de cómo construimos la URL en este caso).

    Como señala el comentario Python anterior, usted siempre debe retornar una HttpResponseRedirect después de gestionar exitosamente los datos POST. Esta sugerencia no es solo para Django, es solo una buena práctica de desarrollo Web.

  • We are using the reverse() function in the HttpResponseRedirect constructor in this example. This function helps avoid having to hardcode a URL in the view function. It is given the name of the view that we want to pass control to and the variable portion of the URL pattern that points to that view. In this case, using the URLconf we set up in Tutorial 3, this reverse() call will return a string like

    '/polls/3/results/'
    

    donde el 3 es el valor de question.id. Esta URL redirigida entonces llamará a la vista 'results' para mostrar la página final.

Como se mencionó en el Tutorial 3, request es un objeto HttpRequest . Para más información sobre los objetos HttpRequest, consulte la documentación de petición y respuesta.

Después de que alguien vota en una pregunta, la vista vote() remite a la página de resultados de la pregunta. Vamos a escribir dicha 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() del Tutorial 3. La única diferencia es el nombre de la plantilla. Solucionaremos esta redundancia más tarde.

Ahora, cree una plantilla 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>

A continuación, vaya a /polls/1/ en su navegador y vote en la pregunta. Debería ver una página de resultados que se actualiza cada vez que usted vota. Si usted envía el formulario sin haber seleccionado una opción, usted debería ver el mensaje de error.

Nota

El código para nuestra vista vote() tiene un pequeño problema. Primero, obtiene el objeto selected_choice de la base de datos, luego, calcula el nuevo valor de votes, y después lo guarda de nuevo en la base de datos. Si dos usuarios de su sitio Web intentan votar exactamente en el mismo momento, esto podría salir mal: El mismo valor, digamos 42, será recuperado para votes. Entonces, se calcula y se guarda el nuevo valor de 43 para ambos usuarios, pero el 44 sería el valor esperado.

Esto se conoce como una condición de carrera. Si usted está interesado, puede leer :ref: avoiding-race-conditions-using-f para aprender cómo solucionar este problema.

Utilice vistas genéricas: Menos código es mejor

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

Estas vistas representan un caso común de desarrollo Web básico: obtener datos de la base de datos según un parámetro pasado en la URL, cargar una plantilla y retornar la plantilla creada. Debido a que esto es tan común, Django proporciona un atajo denominado sistema de «vistas genéricas».

Las vistas genéricas abstraen los patrones comunes hasta el punto en que ni siquiera es necesario escribir código Python para escribir una aplicación.

Vamos a convertir nuestra aplicación encuesta para utilizar el sistema de vistas genéricas, por lo que podemos eliminar mucho de nuestro propio código. Solo tendremos que seguir algunos pasos para hacer la conversión. Vamos a:

  1. Convertir el URLconf.
  2. Eliminar algunas de las vistas viejas e innecesarias.
  3. Introducir nuevas vistas basadas en las vistas genéricas de Django.

Siga leyendo para conocer más detalles.

¿Por qué el intercambio de código?

Generalmente, cuando se escribe una aplicación de Django, usted podrá evaluar si las vistas genéricas son una buena opción para su problema y ​​las utilizará desde el principio en lugar de reestructurar su código a medio camino. Sin embargo, este tutorial intencionadamente se ha centrado hasta ahora en escribir las vistas «de la manera difícil» para centrarse en los conceptos esenciales.

Debe saber matemáticas básicas antes de comenzar a utilizar una calculadora.

Modifique el URLconf

Primero, abra el URLconf polls/urls.py y modifíquelo de la siguiente manera:

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'),
]

Note that the name of the matched pattern in the path strings of the second and third patterns has changed from <question_id> to <pk>.

Modifique las vistas

A continuación, vamos a eliminar nuestras viejas vistas index, detail y results y en su lugar vamos a usar las vistas genéricas de Django. Para ello, abra el archivo polls/views.py y modifíquelo de la siguiente manera:

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

Estamos utilizando dos vistas genéricas aquí: ListView y DetailView. Respectivamente, esas dos vistas abstraen los conceptos de «mostrar una lista de objetos» y «mostrar una página de detalles para un tipo específico de objeto.»

  • Cada vista genérica tiene que saber cuál es el modelo sobre el que estará actuando. Esto se proporciona utilizando el atributo model.
  • La vista genérica DetailView espera que el valor de la clave primaria capturado desde la URL sea denominado "pk", por lo que hemos cambiado``question_id`` a pk para las vistas genéricas.

Por defecto, la vista genérica DetailView utiliza una plantilla llamada <app name>/<model name>_detail.html. En nuestro caso, utilizaría la plantilla "polls/question_detail.html". El atributo template_name se utiliza para indicarle a Django que utilice un nombre de plantilla específico en vez del nombre de plantilla generado de forma automática. También especificamos el atributo template_name para la vista de lista results, esto garantiza que la vista de resultados y la vista detalle tengan un aspecto diferente cuando sean creadas, a pesar de que las dos son una vista genérica DetailView en segundo plano.

Del mismo modo, la vista genérica ListView utiliza una plantilla predeterminada llamada <app name>/<model name>_list.html; utilizamos el atributo template_name para indicarle a ListView que utilice nuestra plantilla "polls/index.html" existente.

En partes anteriores del tutorial, las plantillas se han dotado de un contexto que contiene las variables contextuales question y latest_question_list. Para DetailView se suministra la variable question de forma automática, ya que estamos utilizando un modelo (Question) de Django, Django puede determinar un nombre adecuado para la variable contextual. Sin embargo, para ListView, la variable contextual generada de forma automática es question_list. Para anular esto, proporcionamos el atributo``context_object_name`` especificando que queremos utilizar en cambio latest_question_list. Como un método alternativo, usted puede modificar sus plantillas para que coincidan con las nuevas variables contextuales predeterminadas, pero es mucho más sencillo solo indicarle a Django que utilice la variable que usted quiere.

Ejecute el servidor y utilice su nueva aplicación encuesta basada en las vistas genéricas.

Para más detalles sobre las vistas genéricas, consulte la documentación de las vistas genéricas.

Cuando esté familiarizado con los formularios y las vistas genéricas, lea la parte 5 de este tutorial para aprender sobre cómo probar nuestra aplicación encuestas.

Back to Top