11 minutos
Implementación de Formularios en Django
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
ynull
sonTrue
. 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
ynull
. Cuandoblank=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:
- Mostrar el formulario en base a los datos de un modelo.
- Validar que los datos introducidos sean correctos.
- En caso de error se mostrará el formulario con las datos capturados indicando que campos no pasaron la validación.
- 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 llameagenda_form.html
, si queremos usar una platilla con nombre diferente, usaremos la variabletemplate_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:
- Mostrar el formulario en base a los datos de un modelo.
- Validar que los datos introducidos sean correctos.
- Si los datos son válidos, se graba en la base de datos.
- En caso de error se mostrará el formulario con las datos capturados indicando que campos no pasaron la validación.
- 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:
- 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.
- Validar que los datos introducidos sean correctos.
- Si los datos son válidos, se actualiza la información en la base de datos.
- En caso de error se mostrará el formulario con las datos capturados indicando que campos no pasaron la validación.
- 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
yslug
, los argumentos que se pasarán a la vista sonpk_url_kwarg
yslug_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:
- Si el método es de tipo
GET
, mostrar una página de confirmación de la eliminación del registro. - 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:
- El formulario debe de apuntar a la misma URL.
- El método del formulario tiene que ser
POST
. - La variable
object
es una representación del registro a borrar (ver la nota al final de esta lista) - El nombre de la plantilla tiene que ser
modelo_confirm_delete.html
(en nuestro caso esagenda_confirm_delete.html
). Si queremos usar un nombre diferente tenemos que usar la variabletemplate_name
cuando declaramos la clase.
Nota: En nuestra plantilla tenemos
{{ object }}
, por lo que será reemplazado por el valor regresado enAgenda.__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.