Introducción

Django tiene un sistema de formularios bastante completo el cual nos permite crear, actualizar y borrar información de una manera muy simple, pero desafortunadamente no muy intuitiva, al menos cuando no se conocen las bases sobre las cuales se construyó este sistema. Para complicar las cosas, se encuentra mucha información en internet pero no siempre es la correcta, o muestran soluciones usando funciones en lugar de las vistas basadas en clases, y cuando un ejemplo funciona no se explica con claridad el por qué funciona. En este post trataré de explicar como funcionan los formularios.

Validación de los datos

Antes de explicar como mostrar y procesar los formularios, es necesario entender el mecanismo que usa Django para validar la información capturada por el usuario. Cuando creamos un modelo indicamos el tipo de dato de cada campo, y algunos datos extras como la longitud y si se permite campos en blanco. Esta es la información que se usa para validar cada campo. Si tenemos un campo entero y en el formulario escribimos una cadena de texto, entonces no pasará la validación. Si nuestro campo es de texto pero excede el número máximo de caracteres, también se considera inválido. Veamos un modelo de ejemplo:

class Agenda(models.Model):
    nombre_evento = models.CharField(max_length=64)
    fecha = models.DateField()
    hora = models.TimeField(blank=True, null=True)
    duracion_minutos = models.PositiveIntegerField(blank=True, null=True)
    notas = models.TextField(blank=True, null=True)

Con este sencillo ejemplo, tenemos las siguientes validaciones:

  • nombre_evento: es un campo requerido y su longitud máxima es de 64 caracteres.
  • fecha: otro campo requerido.
  • hora: un campo opcional, ya que tanto blank y null son True. En nuestra agenda, si un evento no tiene hora aplica para todo el día.
  • duracion_minutos: otro campo opcional, de tipo entero sin signo.
  • notas: otro campo opcional.

Nota: es importante conocer las diferencias entre blank y null. Cuando blank=True indica que es posible dejar este campo en blanco en el formulario, por lo que no se generará un error si el campo del formulario no contiene información, pero esta validación solo aplica a nivel formulario y no a nivel base de datos. Por otro lado, null=True indica que este campo no es obligatorio en la base de datos. Generalmente se usan conjuntamente ya que se complementan.

Creación de vistas

Django nos proporciona 4 vistas basadas en clase (CBV) que nos simplifican el trabajo con los formularios, cada una con una funcionalidad específica dependiendo de nuestro requerimiento. Estas clases se encuentran en el módulo django.views.generic, por lo que si queremos usar la vista FormView la importaremos como:

from django.views.generic import FormView

FormView

Descripción

Esta es la clase más sencilla de de las cuatro, y su función es:

  1. Mostrar el formulario en base a los datos de un modelo.
  2. Validar que los datos introducidos sean correctos.
  3. En caso de error se mostrará el formulario con las datos capturados indicando que campos no pasaron la validación.
  4. En caso de éxito se redirigirá a una URL que especifiquemos.

Nota: Esta vista no graba el formulario, solo implementa el comportamiento de validación de los datos, puede ser usada para comportamientos más complejos, como se verá más adelante.

Como ya he comentado en el post Sistema de Permisos en Django, las CBV nos simplifica mucho el trabajo, y en el caso de los formularios no es la excepción. Veamos como implementar un formulario en su forma más básica.

Implementación

Tomando como base el modelo de la agenda, en nuestro archivo views.py pondremos lo siguiente:

from django.views.generic import FormView
from .models import Agenda


class AgendaForm(FormView):
    model = Agenda
    fields = ['nombre_evento', 'fecha', 'hora', 'duracion_minutos', 'notas']

Esto es todo lo que nuestra vista requiere. No hay que definir los métodos get() o post(), ya que FormView se encarga de gestionar ambas peticiones por nosotros. Ahora veamos el código de nuestra plantilla, el código va en agenda_form.html:

<html>
  <head>
    <title>Agregar evento</title>
  </head>
  <body>
    <h2>Agregar evento a la agenda</h2>
    <form method="post">
      {% csrf_token %} {{ form.as_p }}
      <input type="submit" value="Agregar" />
    </form>
  </body>
</html>

El código Html puede ser tan elaborado o simple como deseemos, los únicos requisitos es que contenga la etiqueta <form method="post"> y la variable {{ form.as_p }}, esta variable es en la que Django pone todos los widgets (cajas de texto, select, radio botones, etc.) que nuestro modelo requiere. En este punto es necesario hacer dos aclaraciones:

Nuestra vista está determinando el nombre de la plantilla por convención. Como nuestro modelo se llama Agenda, espera que la plantilla se llame agenda_form.html, si queremos usar una platilla con nombre diferente, usaremos la variable template_name.

En la vista usamos la variable fields para indicar los campos del modelo que queremos mostrar en el formulario, esto es útil por que hay campos automáticos (como la fecha de creación y modificación) que no los captura el usuario. Si solo queremos mostrar nombre_evento y fecha y además queremos usar una plantilla con nombre diferente, nuestra vista cambia a:

from django.views.generic import FormView
from .models import Agenda


class AgendaCreate(CreateView):
    model = Agenda
    fields = ['nombre_evento', 'fecha']
    template_name = 'agenda/agenda_sin_duracion.html'

CreateView

Descripción

Como ya comenté, FormView no graba datos, pero CreateView sí nos permite crear nuevos registros en la base de datos. Su función es:

  1. Mostrar el formulario en base a los datos de un modelo.
  2. Validar que los datos introducidos sean correctos.
  3. Si los datos son válidos, se graba en la base de datos.
  4. En caso de error se mostrará el formulario con las datos capturados indicando que campos no pasaron la validación.
  5. En caso de éxito se redirigirá a una URL que especifiquemos.

Como se puede observar, el comportamiento es muy similar a FormView, con la única diferencia de grabar los datos si estos pasan las validaciones.

Nota: En este punto se genera la validación a nivel base de datos para asegurar la integridad referencial y los índices únicos, por dar un ejemplo. Si queremos insertar un registro duplicado Django marcará el campo duplicado como no válido.

Implementación

from django.views.generic import CreateView
from .models import Agenda


class AgendaCreate(CreateView):
    model = Agenda
    fields = ['nombre_evento', 'fecha', 'hora', 'duracion_minutos', 'notas']

El código parece idéntico al que usamos con FormView, y casi lo es, la única diferencia es que estamos usado CreateView en lugar de FormView, Django se encarga de hacer todo el trabajo por nosotros. La plantilla Html no sufre ningún cambio, así que la omitiré en esta sección.

UpdateView

Descripción

Como podemos imaginar, esta clase nos permite modificar un registro que ya fue creado con anterioridad. Su función es:

  1. Mostrar el formulario en base a los datos de un modelo, llenando los campos con la información del registro en la base de datos.
  2. Validar que los datos introducidos sean correctos.
  3. Si los datos son válidos, se actualiza la información en la base de datos.
  4. En caso de error se mostrará el formulario con las datos capturados indicando que campos no pasaron la validación.
  5. En caso de éxito se redirigirá a una URL que especifiquemos.

Las funciones son muy parecidas a las de CreateView, excepto por que el formulario ya contiene datos, y en lugar de crear un registro nuevo actualiza uno existente.

Implementación

La implementación es muy parecida a las clases que ya vimos:

from django.views.generic import UpdateView
from .models import Agenda


class AgendaUpdate(UpdateView):
    model = Agenda
    fields = ['nombre_evento', 'fecha', 'hora', 'duracion_minutos', 'notas']

Si la implementación es la misma que con CreateView ¿Cómo determina Django que registro es el que se tiene que modificar? UpadateView hereda del mixin django.views.generic.detail.SingleObjectMixin, el cual implementa el método get_object(). La función de este método es regresar un objeto único que se mostrará en el formulario. La forma de determinar el objeto único depende de varios factores.

queryset

Primero busca la variable de clase queryset, y si está definida, utiliza ese queryset para obtener el registro:

from django.views.generic import UpdateView
from .models import Agenda


class AgendaUpdate(UpdateView):
    model = Agenda
    fields = ['nombre_evento', 'fecha', 'hora', 'duracion_minutos', 'notas']
    queryset = Agenda.objects.get(pk=1)

Esta es la opción más limitada, ya que no podemos usar un parámetro dinámico para obtener el registro. Esta variable generalmente se usa para obtener todos los registros con Agenda.objects.all(), o como se puso en el ejemplo, un valor fijo, pero que no sirve de mucho en nuestro caso.

get_queryset()

Si no existe la variable queryset, entonces se busca al método get_queryset(), y usa el queryset regresado:

from django.views.generic import UpdateView
from .models import Agenda


class AgendaUpdate(UpdateView):
    model = Agenda
    fields = ['nombre_evento', 'fecha', 'hora', 'duracion_minutos', 'notas']

    def get_queryset(self):
        return Agenda.objects.get(pk=1)

Aquí ya tenemos más flexibilidad, ya que al ser una función podemos hacer varias consultas y construir un queryset dinámico. Aunque con esta función podemos obtener el registro que queremos, para nuestro caso de uso es suficiente con lo explicado en el siguiente punto. El método get_queryset() es más usado en la vista ListView, donde los datos a usar generalmente involucran más de un modelo.

Por parámetros de la vista

Si tampoco está definido el método get_queryset() entonces busca los argumentos pk_url_kwarg y slug_url_kwarg en los parámetros de la vista en ese orden, y si solo existe el primero usa el campo pk para buscar el registro; si solo se encuentra el slug_url_kwarg, entonces se usará el campo del modelo slug; y si se encuentran ambos argumentos, se usará un and lógico con los dos valores. A continuación veremos como es que Django construye el queryset en base a los parámetros obtenidos:


# Si solo está definido pk_url_kwarg
Agenda.objects.get(pk=pk_url_kwarg)

# Si solo está definido slug_url_kwarg
Agenda.objects.get(slug=slug_url_kwarg)

# Si ambos están definidos
Agenda.objects.get(pk=pk_url_kwarg, slug=sulg_url_kwarg)

Hay que recalcar que toda esta lógica la realiza Django por nosotros, nuestra única tarea es crear los endpoints necesarios en urls.py:

from django.urls import path
from .views import AgendaUpdate


urlpatterns = [
    # Solo definimos pk
    path('agenda/update/<int:pk>/,
         AgendaUpdate.as_view(),
         name='agenda_update'),
    # Solo definimos slug
    path('agenda/update/<slug:slug>/",
         AgendaUpdate.as_view(),
         name='agenda_update'),
    # Definimos pk y slug
    path('agenda/update/<int:pk>/<slug:slug>/,
         AgendaUpdate.as_view(),
         name='agenda_update'),
]

Nota: aunque estamos nombrando a nuestro parámetros pk y slug, los argumentos que se pasarán a la vista son pk_url_kwarg y slug_url_kwarg.

Como vimos al inicio inicio de esta sección, cuando usamos los argumentos de la vista el código se reduce al mínimo, repito la misma vista como referencia:

from django.views.generic import UpdateView
from .models import Agenda


class AgendaUpdate(UpdateView):
    model = Agenda
    fields = ['nombre_evento', 'fecha', 'hora', 'duracion_minutos', 'notas']

Ahora podemos prescindir de la variable queryset y del método get_queryset() solo usando los endpoints correctos.

DeleteView

Descripción

Cuando necesitamos borrar un registro necesitamos usar a la CBV DeleteView. Su función es:

  1. Si el método es de tipo GET, mostrar una página de confirmación de la eliminación del registro.
  2. Si el método es de tipo POST borrar el registro y redireccionarnos a una página de éxito.

Implementación

Aunque el funcionamiento de esta clase es algo diferente a las que hemos visto, su implementación es bastante similar:

from django.urls import reverse_lazy
from django.views.generic import DeleteView
from .models import Agenda


class AgendaDelete(DeleteView):
    model = Agenda
    success_url = reverse_lazy('agenda-list')

Aunque en la descripción hicimos mención de los métodos GET y POST no es necesario definirlos en nuestra vista, ya que DeleteView los implementa por nosotros. Solo tenemos que indicar el modelo (en este caso Agenda) que usará la vista y la url a la que se redireccionará en caso de éxito (agenda_list).

Nota: Es necesario usar la función reverse_lazy() por que en este punto aun no se tiene acceso a otras vistas.

Al igual que UpdateView, esta vista hereda del mixin django.views.generic.detail.SingleObjectMixin, por lo que aplican las mismas reglas para determinar el registro a borrar. La mayoría de las veces usaremos parámetros de la vista. Para esto definiremos un nuevo path en urls.py:

from django.contrib import admin
from django.urls import path
from encuesta.views import AgendaCreate, AgendaEdit, AgendaDelete

urlpatterns = [
    path("admin/", admin.site.urls),
    path("encuesta/agenda/", AgendaCreate.as_view(), name="agenda_create"),
    path(
        "encuesta/agenda/update/<int:pk>/", AgendaEdit.as_view(), name="agenda_update"
    ),
    path(
        "encuesta/agenda/delete/<int:pk>/", AgendaDelete.as_view(), name="agenda_delete"
    ),
    # Aquí es donde se redirecciona cuando se borra un registro:
    path("encuesta/agendas/", AgendaView.as_view(), name="agenda_list")
]

Ahora que ya tenemos identificado al registro a borrar, hay que crear la plantilla de confirmación:

<form method="post">
  {% csrf_token %}
  <p>¿Estás seguro de borrar el registro "{{ object }}"?</p>
  <input type="submit" value="Confirmar" />
</form>

Hay cuatro puntos importantes de esta plantilla:

  1. El formulario debe de apuntar a la misma URL.
  2. El método del formulario tiene que ser POST.
  3. La variable object es una representación del registro a borrar (ver la nota al final de esta lista)
  4. El nombre de la plantilla tiene que ser modelo_confirm_delete.html (en nuestro caso es agenda_confirm_delete.html). Si queremos usar un nombre diferente tenemos que usar la variable template_name cuando declaramos la clase.

Nota: En nuestra plantilla tenemos {{ object }}, por lo que será reemplazado por el valor regresado en Agenda.__str__(). Pero también podemos usar cualquier campo del modelo, por ejemplo: {{ object.nombre_evento }}.

Por último, falta definir la vista que mostrará un listado de todos los eventos cuando se borre un registro:

from django.views.generic import CreateView, DeleteView, FormView, UpdateView, ListView
from django.urls import reverse_lazy
from .models import Agenda


class AgendaView(ListView):
    model = Agenda

Como podemos ver, estamos usando la CBV ListView, la cual en su forma más básica nos regresa una lista con todos los registros del modelo que especifiquemos (básicamente invoca a Agenda.objects.all()). Ahora solo hay que especificar una plantilla que muestre esta lista, y el nombre de la plantilla tiene que ser agenda_list.html:

<h1>Eventos</h1>
<ul>
{% for evento in object_list %}
    <li>{{ evento.fecha|date }} - {{ evento.nombre_evento }}</li>
{% empty %}
    <li>No articles yet.</li>
{% endfor %}
</ul>

Conclusión

Como hemos visto, las CBV nos simplifican mucho el trabajo a la hora de trabajar con formularios, solo necesitamos conocer las convenciones y los métodos requeridos por cada una de ellas. En otros posts veremos como crear nuestras propias validaciones, y como personalizar los widgets de los formularios.