Formularios¶
Django básico
Los formularios son componentes web que permiten al usuario introducir información en una aplicación web. Veremos cómo manejar y gestionar los formularios a través de Django.
Tipos de formularios¶
En función de la forma de implementarlos, podemos distinguir los siguientes tipos de formularios:
Formularios de plantilla.
Formularios de clase.
Formularios de modelo.
Formularios de plantilla¶
Este tipo de formularios se construyen a partir de una plantilla HTML.
Supongamos por ejemplo que creamos un formulario en una plantilla para añadir un nuevo «post» en un «blog». Tendríamos algo similar a lo siguiente:
<h1>Add post</h1>
<form method="post" novalidate><!--(1)!-->
{% csrf_token %}<!--(2)!-->
<p>
Title:<br>
<input type="text" name="post-title"><!--(3)!-->
</p>
<p>
Content:<br>
<textarea name="post-content" cols="40" rows="10"></textarea><!--(4)!-->
</p>
<input type="submit" value="Enviar"><!--(5)!-->
</form>
-
- Un formulario se puede enviar con método «get» o con método «post». Cada uno tiene sus ventajas e inconvenientes.
- En un modelo SSR es recomendable validar el envío del formulario en el servidor. Para ello desactivamos la validación HTML →
<form method="post" novalidate>
- Django proporciona este mecanismo de seguridad contra CSRF. Genera un token único que debe ser enviado en la petición para que sea válida.
- Los nombres que damos a los «widgets» son importantes. En este caso el nombre es
post-titley contendrá el título del post que introduzca el usuario. - Los nombres que damos a los «widgets» son importantes. En este caso el nombre es
post-contenty contendrá el contenido del post que introduzca el usuario. - Necesitamos un botón para realizar el envío.
El envío de este formulario llegará al correspondiente fichero urls.py que ejecutará una determinada vista dentro del fichero views.py.
Veamos cómo procesar esta solicitud, siguiendo con el ejemplo anterior de creación de un «post»:
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.utils.text import slugify
from .models import Post
def add_post(request):
if request.method == 'POST':#(1)!
post_title = request.POST.get('post-title')#(2)!
post_content = request.POST.get('post-content')#(3)!
if post_title and post_content:#(4)!
post_slug = slugify(post_title)#(5)!
Post.objects.create(#(6)!
title=post_title,
content=post_content,
slug=post_slug,
)
return redirect('posts:post-list')#(7)!
else:
return HttpResponse('Title and content are required!')#(8)!
return render(request, 'posts/post/add.html')#(9)!
- Distinguimos el método de la petición HTTP.
- En
request.POSTtenemos un diccionario con todos los datos que provienen de la petición «post». La clave que buscamos debe coincidir con el atributonamedel correspondiente «input» del formulario HTML. - En
request.POSTtenemos un diccionario con todos los datos que provienen de la petición «post». La clave que buscamos debe coincidir con el atributonamedel correspondiente «input» del formulario HTML. - «Validación» del formulario → debe existir un título y un contenido para el post.
- Creamos el «slug» a partir del título del «post».
- Se crea un nuevo «post» a partir del título, contenido y «slug».
- Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
- En el caso de que falte algún campo de entrada, habrá que informar del error.
- Devolvemos la plantilla renderizada.
Formularios de clase¶
Este tipo de formularios se construyen a partir de una clase Python.
Django nos ofrece funcionalidades para poder escribir los formularios usando código Python en vez de tener que usar código HTML.
En realidad lo que hacemos es definir una clase Python que posteriormente se transformará en el correspondiente código HTML («widget») inyectándolo en la plantilla.
Campos de formulario¶
La siguiente tabla muestra la información más relevante de los distintos campos de formulario que ofrece Django:
| Campo | Objeto Python | «Widget» | Parámetros |
|---|---|---|---|
BooleanField |
bool |
CheckboxInput |
|
CharField |
str |
TextInput |
max_length(1)min_length(2)strip(3)empty_value(4) |
ChoiceField |
str |
Select |
choices (5) |
DateField |
datetime.date |
DateInput |
input_formats(6) |
DateTimeField |
datetime.datetime |
DateTimeInput |
input_formats(7) |
DecimalField |
decimal |
NumberInput |
max_value(8)min_value(9)max_digits(10)decimal_places(11)step_size(12) |
DurationField |
timedelta |
TextInput |
|
EmailField |
str |
EmailInput |
max_length(13)min_length(14)empty_value(15) |
FileField |
UploadedFile |
ClearableFileInput |
max_length(16)allow_empty_file(17) |
FilePathField |
str |
Select |
path (18)recursive(19)match(20)allow_files(21)allow_folders(22) |
FloatField |
float |
NumberInput |
max_value(23)min_value(24)step_size(25) |
GenericIPAddressField |
str |
TextInput |
protocol(26)unpack_ipv4(27)max_length(28) |
ImageField |
UploadedFile |
ClearableFileInput |
|
IntegerField |
int |
NumberInput |
max_value(29)min_value(30)step_size(31) |
JSONField |
dict o list |
Textarea |
encoder(32)decoder(33) |
MultipleChoiceField |
list o str |
SelectMultiple |
choices (34) |
NullBooleanField |
bool o None |
NullBooleanSelect |
|
RegexField |
str |
TextInput |
regex (35) |
SlugField |
str |
TextInput |
allow_unicode(36)empty_value(37) |
TimeField |
datetime.time |
TimeInput |
input_formats(38) |
TypedChoiceField |
Argumento coerce |
Select |
coerce(39)empty_value(40) |
TypedMultipleChoiceField |
Argumento coerce |
SelectMultiple |
coerce(41)empty_value(42) |
URLField |
str |
URLInput |
max_length(43)min_length(44)empty_value(45) |
UUIDField |
UUID |
TextInput |
|
ComboField |
str |
TextInput |
fields (46) |
MultiValueField |
Argumento compress |
TextInput |
fields (47)require_all_fields(48)widget(49)compress(50) |
SplitDateTimeField |
datetime.datetime |
SplitDateTimeWidget |
input_date_formats(51)input_time_formats(52) |
ModelChoiceField |
Instancia de modelo | Select |
queryset (53)empty_label(54)to_field_name(55)blank(56)iterator(57) |
ModelMultipleChoiceField |
QuerySet | SelectMultiple |
queryset (58)to_field_name(59)iterator(60) |
- Tamaño máximo permitido.
- Tamaño mínimo permitido.
- Si es
Truese aplicarástrip()sobre el valor. -
- Valor usado para representar «vacío».
- Por defecto es la cadena vacía.
- Iterable de tuplas de dos elementos.
- Iterable de formatos para convertir un «string» a un objeto
datetime.date. - Iterable de formatos para convertir un «string» a un objeto
datetime.datetime. - Máximo valor permitido.
- Mínimo valor permitido.
- Número máximo de dígitos permitido.
- Número máximo de lugares decimales permitido.
- Limita la entrada válida a un múltiplo de este valor.
- Tamaño máximo permitido.
- Tamaño mínimo permitido.
-
- Valor usado para representar «vacío».
- Por defecto es la cadena vacía.
- Longitud máxima de fichero permitida.
- Si es
Truepermite que el contenido del fichero esté vacío. -
- Ruta absoluta al directorio desde el que listar el contenido.
- La ruta debe existir.
-
- Si es
Truese listará recursivamente todo el contenido de la ruta indicada. - Por defecto es
False.
- Si es
- Patrón de expresión regular para limitar la búsqueda.
-
- Si es
Truepermite el listado de ficheros. - Por defecto es
True.
- Si es
-
- Si es
Truepermite el listado de directorios. - Por defecto es
False.
- Si es
- Máximo valor permitido.
- Mínimo valor permitido.
- Limita la entrada válida a un múltiplo de este valor.
- Limita la entrada al protocolo especificado.
- Desempaqueta direcciones IPv4.
- Tamaño máximo permitido.
- Máximo valor permitido.
- Mínimo valor permitido.
- Limita la entrada válida a un múltiplo de este valor.
- Una subclase de
JSONEncoderpara serializar los tipos de datos no soportados por el serializador JSON. - Una subclase de
JSONDecoderpara deserializar la entrada. - Iterable de tuplas de dos elementos.
- Expresión regular a aplicar.
-
- Si es
Truepermite que se acepten letras Unicode. - Por defecto es
False.
- Si es
-
- Valor usado para representar «vacío».
- Por defecto es la cadena vacía.
- Iterable de formatos para convertir un «string» a un objeto
datetime.time. - Función que toma un argumento y devuelve el valor «coercionado».
-
- Valor usado para representar «vacío».
- Por defecto es la cadena vacía.
- Función que toma un argumento y devuelve el valor «coercionado».
-
- Valor usado para representar «vacío».
- Por defecto es la cadena vacía.
- Tamaño máximo permitido.
- Tamaño mínimo permitido.
-
- Valor usado para representar «vacío».
- Por defecto es la cadena vacía.
- Lista de campos que se deberían usar para validar el valor.
- Tupla de campos cuyos valores se limpian y se combinan en un único valor.
-
- Si es
Truese lanza un error de validación si algún campo está vacío. - Por defecto es
True.
- Si es
- Widget que se usará para los controles.
- Toma una lista de valores válidos y returna una versión «comprimida» de dichos valores.
- Lista de formatos para convertir un «string» a un objeto
datetime.date. - Lista de formatos para convertir un «string» a un objeto
datetime.time. QuerySetde objetos desde donde tomar las opciones del campo.- Texto del «widget» que indica el valor vacío.
- Campo a usar como valor de las opciones del «widget».
- Indica si se creará una opción vacía al usar el «widget»
RadioSelect. - Iterador usado para generar las opciones desde el «queryset».
QuerySetde objetos desde donde tomar las opciones del campo.- Campo a usar como valor de las opciones del «widget».
- Iterador usado para generar las opciones desde el «queryset».
Parámetro requerido.
Para explicar la creación y el uso de formularios de clase vamos a utilizar el mismo ejemplo que en el apartado anterior en el cual creamos un nuevo «post» de un «blog» a partir de los datos introducidos por el usuario.
Lo primero que debemos hacer es definir nuestro formulario en un fichero forms.py dentro de la correspondiente aplicación:
from django import forms
class AddPostForm(forms.Form):#(1)!
title = forms.CharField()#(2)!
content = forms.CharField()#(3)!
-
- Aunque no es una regla fija, sí es de «buen estilo» añadir el sufijo
Formal nombre de una clase de formulario. - Una clase de formulario debe heredar de
django.forms.Form.
- Aunque no es una regla fija, sí es de «buen estilo» añadir el sufijo
- Los campos se definen «manualmente» pero utilizando los tipos de campos para formularios que ofrece Django.
-
- Los campos se definen «manualmente» pero utilizando los tipos de campos para formularios que ofrece Django.
- Aquí deberíamos usar un «TextField» pero no existe como tal (en los campos de formulario). La forma de «solucionarlo» sería modificando el «widget» asociado.
Campos opcionales
Por defecto, todos los campos que incluyamos en un formulario son obligatorios. Si queremos indicar que alguno de ellos es opcional debemos utilizar el parámetro required=False.
Ahora veamos cuál es el código que debemos introducir en la plantilla:
<h1>Add post</h1>
<form method="post" novalidate><!--(1)!-->
{% csrf_token %}<!--(2)!-->
{{ form }}<!--(3)!-->
<input type="submit" value="Enviar"><!--(4)!-->
</form>
-
- Diseñamos el formulario usando petición «post».
- En un modelo SSR es recomendable validar el envío del formulario en el servidor. Para ello desactivamos la validación HTML →
<form method="post" novalidate>
- Django proporciona este mecanismo de seguridad contra CSRF. Genera un token único que debe ser enviado en la petición para que sea válida.
- Con esto basta para que se renderice el contenido del formulario en HTML.
- Es necesario incluir un botón para enviar el formulario.
Renderizando formularios
Existen varias opciones para renderizar un formulario Django en una plantilla:
| Método | Descripción |
|---|---|
{{ form }} |
Tabla HTML automática |
{{ form.as_p }} |
Cada campo en un <p> |
{{ form.as_ul }} |
Cada campo en un <ul><li> |
| Personalizado | Documentación oficial de Django |
Por último veamos cómo implementar la vista que debe procesar el formulario:
from django.shortcuts import render, redirect
from django.utils.text import slugify
from .forms import AddPostForm
from .models import Post
def add_post(request):
if request.method == 'POST':
if (form := AddPostForm(request.POST)).is_valid():#(1)!
post_title = form.cleaned_data['title']#(2)!
post_content = form.cleaned_data['content']#(3)!
post_slug = slugify(post_title)#(4)!
Post.objects.create(#(5)!
title=post_title,
content=post_content,
slug=post_slug,
)
return redirect('posts:post-list')#(6)!
else:
form = AddPostForm()#(7)!
return render(request, 'posts/post/add.html', {'form': form})#(8)!
-
- La petición ha sido «post».
- Instanciamos (construimos) el formulario con los datos que provienen de la propia petición «post».
- El método
is_valid()comprueba si los datos del formulario son válidos.
-
- Los formularios disponen un atributo
cleaned_data(dict) con los datos ya «limpios» y transformados al tipo correspondiente según su definición. - Extraemos el título del «post» → La clave
'title'debe coincidir con el nombre que se le dio al campo enforms.py.
- Los formularios disponen un atributo
-
- Los formularios disponen un atributo
cleaned_data(dict) con los datos ya «limpios» y transformados al tipo correspondiente según su definición. - Extraemos el contenido del «post» → La clave
'content'debe coincidir con el nombre que se le dio al campo enforms.py.
- Los formularios disponen un atributo
- Creamos el «slug» a partir del título del «post».
- Se crea un nuevo «post» a partir del título, contenido y «slug».
- Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
-
- La petición ha sido «get».
- Construimos un formulario vacío ya que debemos mostrarlo así para que se introduzcan los datos.
- Renderizamos la plantilla pasando el formulario como contexto y la devolvemos.
Formularios de modelo¶
Este tipo de formularios se construyen a partir de un modelo Django.
Cuando estamos trabajando con un modelo y queremos pedir datos en un formulario que finalmente constituirán un objeto de dicho modelo, Django nos ofrece los ModelForm.
El formulario se diseña especificando el modelo al que está vinculado y los campos a utilizar.
Correspondencia de campos
Cada (tipo de) campo de modelo tiene su correspondencia con un (tipo de) campo de formulario.
A partir del ejemplo ya visto, vamos a preparar un formulario de modelo para crear un «post» de un «blog»:
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):#(1)!
class Meta:#(2)!
model = Post#(3)!
fields = ('title', 'content')#(4)!
-
- Aunque no es una regla fija, sí es de «buen estilo» añadir el sufijo
Formal nombre de una clase de formulario. - Una clase formulario de modelo debe heredar de
django.forms.ModelForm.
- Aunque no es una regla fija, sí es de «buen estilo» añadir el sufijo
- Django permite añadir metadatos a una clase incorporando otra clase interior llamada
Meta. - En el atributo de clase
modelsindicamos el modelo al que vincular el presente formulario. -
- En el atributo de clase
fieldsindicamos los campos del modelo a incluir en el formulario. - Puede ser tanto una tupla como una lista.
- Si queremos seleccionar todos los campos del modelo, basta con:
fields = '__all__' - También podemos usar el atributo
excludepara excluir ciertos campos del modelo.
- En el atributo de clase
Ahora veremos cómo es el código de la plantilla:
<h1>Add post</h1>
<form method="post" novalidate><!--(1)!-->
{% csrf_token %}<!--(2)!-->
{{ form }}<!--(3)!-->
<input type="submit" value="Enviar"><!--(4)!-->
</form>
-
- Diseñamos el formulario usando petición «post».
- En un modelo SSR es recomendable validar el envío del formulario en el servidor. Para ello desactivamos la validación HTML →
<form method="post" novalidate>
- Django proporciona este mecanismo de seguridad contra CSRF. Genera un token único que debe ser enviado en la petición para que sea válida.
- Con esto basta para que se renderice el contenido del formulario en HTML.
- Es necesario incluir un botón para enviar el formulario.
Renderizando formularios
Existen varias opciones para renderizar un formulario Django en una plantilla:
| Método | Descripción |
|---|---|
{{ form }} |
Tabla HTML automática |
{{ form.as_p }} |
Cada campo en un <p> |
{{ form.as_ul }} |
Cada campo en un <ul><li> |
| Personalizado | Documentación oficial de Django |
Por último veamos cómo implementar la vista que debe procesar el formulario:
from django.shortcuts import redirect, render
from django.utils.text import slugify
from .forms import AddPostForm
def add_post(request):
if request.method == 'POST':
if (form := AddPostForm(request.POST)).is_valid():#(1)!
form.save()#(2)!
return redirect('posts:post-list')#(3)!
else:
form = AddPostForm()#(4)!
return render(request, 'posts/post/add.html', {'form': form})#(5)!
-
- La petición ha sido «post».
- Instanciamos (construimos) el formulario con los datos que provienen de la propia petición «post».
- El método
is_valid()comprueba si los datos del formulario son válidos.
- Una llamada a
form.save()en un formulario de modelo guarda el objeto de modelo en la base de datos y lo devuelve. - Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
-
- La petición ha sido «get».
- Construimos un formulario vacío ya que debemos mostrarlo así para que se introduzcan los datos.
- Renderizamos la plantilla pasando el formulario como contexto y la devolvemos.
from django.shortcuts import redirect, render
from django.utils.text import slugify
from .forms import AddPostForm
def add_post(request):
if (form := AddPostForm(request.POST or None)).is_valid():#(1)!
form.save()#(2)!
return redirect('posts:post-list')#(3)!
return render(request, 'posts/post/add.html', {'form': form})#(4)!
-
- Construimos el formulario.
- Si hay información en
request.POSTlo usamos, en otro caso pasamosNonepara construir un formulario vacío. - El método
is_valid()comprueba si los datos del formulario son válidos.
- Una llamada a
form.save()en un formulario de modelo guarda el objeto de modelo en la base de datos y lo devuelve. - Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
- Renderizamos la plantilla pasando el formulario como contexto y la devolvemos.
Veamos cómo implementar cierta lógica adicional.
En este ejemplo convertimos a «slug» el título del «post» antes de almacenarlo:
from django.shortcuts import redirect, render
from django.utils.text import slugify
from .forms import AddPostForm
def add_post(request):
if request.method == 'POST':
if (form := AddPostForm(request.POST)).is_valid():#(1)!
post = form.save(commit=False)#(2)!
post.slug = slugify(post.title)#(3)!
post.save()#(4)!
return redirect('posts:post-list')#(5)!
else:
form = AddPostForm()#(6)!
return render(request, 'posts/post/add.html', {'form': form})#(7)!
-
- La petición ha sido «post».
- Instanciamos (construimos) el formulario con los datos que provienen de la propia petición «post».
- El método
is_valid()comprueba si los datos del formulario son válidos.
-
- Una llamada a
form.save()en un formulario de modelo guarda el objeto de modelo en la base de datos y lo devuelve. - El problema es que aquí necesitamos generar el «slug» antes de guardar definitivamente. Es por ello que usamos el argumento
commit=Falsepara que no se escriba aún en disco.
- Una llamada a
-
- Creamos el «slug» a partir del título del «post».
- Nótese que ya podemos acceder a la variable
postcomo un objeto de tipoPost.
- Ahora sí que definitivamente guardamos el objeto en la base de datos.
- Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
-
- La petición ha sido «get».
- Construimos un formulario vacío ya que debemos mostrarlo así para que se introduzcan los datos.
- Renderizamos la plantilla pasando el formulario como contexto y la devolvemos.
Acceso a datos
En un formulario de modelo, salvo casos excepcionales, deberíamos guardar el objeto de modelo con save() y no acceder a través de cleaned_data.
Formularios de edición¶
Es habitual que, además de crear formularios para añadir/crear objetos, necesitemos formularios para editar/modificar dichos objetos.
No es un tipo en sí mismo, pero cabe destacarlos por la forma especial en la que se procesan los datos. Se puede aplicar tanto para formularios de clase como para formularios de modelo.
Seguimos con el ejemplo anterior y vamos a implementar una solución para editar título y contenido de un determinado «post».
Lo primero será definir un formulario de modelo para editar «posts»:
from django import forms
from .models import Post
class EditPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
La presentación de este modelo en la plantilla no difiere mucho de lo que ya se ha visto en apartados anteriores:
<h1>Editando post "{{ post.title }}"</h1><!--(1)!-->
<form method="post" novalidate><!--(2)!-->
{% csrf_token %}
{{ form }}
<input type="submit" value="Guardar">
</form>
- Aprovechamos para mostrar el título del «post» en la plantilla.
- En un modelo SSR es recomendable validar el envío del formulario en el servidor. Para ello desactivamos la validación HTML →
<form method="post" novalidate>
Por último escribimos la vista que procesará este formulario:
from django.shortcuts import redirect, render
from django.utils.text import slugify
from .forms import EditPostForm
from .models import Post
def edit_post(request, post_slug: str):#(1)!
post = Post.objects.get(slug=post_slug)#(2)!
if request.method == 'POST':
if (form := EditPostForm(request.POST, instance=post)).is_valid():#(3)!
post = form.save(commit=False)#(4)!
post.slug = slugify(post.title)#(5)!
post.save()#(6)!
return redirect('posts:post-list')#(7)!
else:
form = EditPostForm(instance=post)#(8)!
return render(request, 'posts/post/edit.html', {'post': post, 'form': form})#(9)!
- Necesitamos conocer el «slug» (u otro campo único) del «post» ya que estamos editando dicho objeto.
- Buscamos el «post» en la base de datos a través de su «slug».
-
- La petición ha sido «post».
- Instanciamos (construimos) el formulario con los datos que provienen de la propia petición «post» pero además debemos pasar la instancia actual del objeto que estamos editando mediante el parámetro
instance. - El método
is_valid()comprueba si los datos del formulario son válidos.
-
- Una llamada a
form.save()en un formulario de modelo guarda el objeto de modelo en la base de datos y lo devuelve. - El problema es que aquí necesitamos generar el «slug» antes de guardar definitivamente. Es por ello que usamos el argumento
commit=Falsepara que no se escriba aún en disco.
- Una llamada a
-
- Creamos el «slug» a partir del título del «post».
- Nótese que ya podemos acceder a la variable
postcomo un objeto de tipoPost.
- Ahora sí que definitivamente guardamos el objeto en la base de datos.
- Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
-
- La petición ha sido «get».
- Construimos el formulario con los datos que provienen del «post» que estamos editando.
- Renderizamos la plantilla pasando el «post» y el formulario como contexto y la devolvemos.
Widgets¶
Django intermedio
Un «widget» es la representación Django de un componente HTML para formulario. El «widget» maneja el renderizado del HTML y la extración de datos desde el correspondiente diccionario GET/POST.
Django proporciona una gran cantidad de widgets «built-in». Cuando definimos un campo de formulario, este tiene asignado un «widget» por defecto, pero tenemos la posibilidad de personalizar el «widget» o incluso de asignar otro.
Widgets predefinidos¶
Para acceder a cada «widget» basta con importarlo desde: from django import forms y luego usarlo (por ejemplo) con forms.TextInput ...
| «Widget» | HTML | Destino | Atributos |
|---|---|---|---|
TextInput |
<input type="text" ...> |
Texto corto | |
NumberInput |
<input type="number" ...> |
Número | |
EmailInput |
<input type="email" ...> |
Correo electrónico | |
URLInput |
<input type="url" ...> |
URL | |
ColorInput |
<input type="color" ...> |
Color | |
SearchInput |
<input type="search" ...> |
Búsqueda | |
TelInput |
<input type="tel" ...> |
Teléfono | |
PasswordInput |
<input type="password" ...> |
Contraseña | |
HiddenInput |
<input type="hidden" ...> |
Campo oculto | |
DateInput |
<input type="text" ...> |
Fecha | format |
DateTimeInput |
<input type="text" ...> |
Fecha/Hora | format |
TimeInput |
<input type="text" ...> |
Hora | format |
Textarea |
<textarea>...</textarea> |
Texto largo |
| «Widget» | HTML | Destino | Atributos |
|---|---|---|---|
CheckboxInput |
<input type="checkbox" ...> |
Verificación | check_text |
Select |
<select><option ...>...</select> |
Selección | choices |
NullBooleanSelect |
<select><option ...>...</select> |
Selección de opciones booleanas | |
SelectMultiple |
<select><option ...>...</select> |
Selección de múltiples opciones | |
RadioSelect |
<select><option ...>...</select> |
Selección (radial) | |
CheckboxSelectMultiple |
<select><option ...>...</select> |
Verificación de múltiples opciones |
| «Widget» | HTML | Destino | Atributos |
|---|---|---|---|
FileInput |
<input type="file" ...> |
Subida de ficheros | |
ClearableFileInput |
<input type="file" ...> |
Subida de ficheros (con borrado) |
| «Widget» | HTML | Destino | Atributos |
|---|---|---|---|
MultipleHiddenInput |
<input type="hidden" ...> |
Múltiples campos ocultos | |
SplitDateTimeWidget |
<input type="date" ...> |
Fecha y hora divididos | date_formattime_formatdate_attrstime_attrs |
SplitHiddenDateTimeWidget |
<input type="date" ...> |
Fecha y hora divididos (ocultos) | date_formattime_formatdate_attrstime_attrs |
SelectDateWidget |
<input type="date" ...> |
Fecha (compuesta por mes, día y año) | yearsmonthsempty_label |
Modificando widgets¶
Para modificar el «widget» de un determinado campo de formulario hay que distinguir si estamos trabajando con un formulario de clase o con un formulario de modelo.
Retomando el ejemplo de los «posts» de un «blog», veamos cómo asignar un «widget» Textarea para el campo «contenido» de un «post»:
from django import forms
class AddPostForm(forms.Form):
title = forms.CharField()
content = forms.CharField(widget=forms.Textarea)#(1)!
- Usamos el parámetro
widgeten el constructor del campoCharFieldindicando que queremos usar un «widget» de tipoTextarea.
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
widgets = {'content': forms.Textarea()}#(1)!
- El atributo
widgetsde la claseMetanos permite asignar un diccionario donde las claves sean los nombres de los campos y los valores sean los «widgets» que queremos asignar.
Modificando atributos HTML¶
Para modificar los atributos HTML de un «widget» podemos hacer uso de la propiedad attrs de la que disponen todos los «widgets». El método para llevar esto a cabo depende de si estamos trabajando con un formulario de clase o con un formulario de modelo.
Continuando con el ejemplo de los «posts» de un blog, veamos cómo modificar el identificador del campo título y la clase del campo contenido:
Una primera aproximación sería modificar la definición de los campos:
from django import forms
class AddPostForm(forms.Form):
title = forms.CharField(widget=forms.TextInput(attrs={'id': 'post-title'}))
content = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control'}))
Pero también es posible hacerlo de manera programática:
Una primera aproximación sería modificar la definición en la clase Meta:
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
widgets = {
'title': forms.TextInput(attrs={'id': 'post-title'}),
'content': forms.Textarea(attrs={'class': 'form-control'}),
}
Pero también es posible hacerlo de manera programática:
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
widgets = {
'content': forms.Textarea(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['title'].widget.attrs.update({'id': 'post-title'})
self.fields['content'].widget.attrs.update({'class': 'form-control'})
Nota
Dentro del constructor (o cualquier otro método de instancia), es posible recorrer los campos del formulario mediante self.fields, un iterable que devuelve objetos de tipo Field.
Si queremos modificar atributos de todos los campos visibles del formulario, podemos aplicar lo siguiente:
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):#(1)!
class Meta:
model = Post
fields = ('title', 'content')
def __init__(self, *args, **kwargs):#(2)!
super().__init__(*args, **kwargs)#(3)!
for visible in self.visible_fields():#(4)!
visible.field.widget.attrs['class'] = 'form-control'#(5)!
- También es aplicable a formularios de clase que hereden de
forms.Form. - Debemos implementar el constructor del formulario para realizar cambios cuando se construye cada instancia.
- Llamada al constructor de la clase base
forms.ModelForm. - Recorremos los campos visibles del formulario.
- Asignamos la clase CSS
form-control.
Campos de tipo fecha/hora¶
Uno de los «widgets» que suele ser más complicado de ajustar es el de fecha/hora.
Veamos un ejemplo en el que creamos un formulario de clase para añadir un post que incluye fecha de publicación:
from django import forms
class AddPostForm(forms.Form):
title = forms.CharField()
content = forms.CharField(widget=forms.Textarea)
published_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
Con esta configuración obtendremos un control interactivo para seleccionar la fecha:

Este ajuste también es aplicable tanto a campos de tipo DateTimeField() como a formularios de modelo.
Guardar de forma personalizada¶
Django intermedio
Hay ocasiones en las que nos interesa personalizar el guardado de un formulario para modificar determinados atributos o realizar otras acciones. Esto se consigue sobreescribiendo el método save() del formulario.
Escenario sin claves ajenas¶
Vamos a retomar el ejemplo del formulario de modelo AddPostForm donde pretendíamos convertir a «slug» el título del «post» antes de guardarlo definitivamente en disco.
La idea que hay detrás de esta nueva aproximación es encapsular el código «adicional» (personalizado) en el propio método save() del formulario de modelo:
from django import forms
from django.utils.text import slugify
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
def save(self, *args, **kwargs):#(1)!
post = super().save(commit=False)#(2)!
post.slug = slugify(post.title)#(3)!
post = super().save(*args, **kwargs)#(4)!
return post#(5)!
-
- Sobreescribimos el método
save()de la claseforms.ModelForm. - Esto también sería válido para
forms.Form.
- Sobreescribimos el método
-
- Guardamos el formulario sin escribir en disco.
- Con ello obtendremos un objeto
Posten memoria que podremos manipular.
- Generamos el «slug» del «post» a partir del título.
- Ahora ya podemos almacenar el objeto
posten disco llamando al método de la clase base. - El método
save()de un formulario siempre debe devolver la instancia creada.
Con este diseño de formulario, crear un nuevo «post» a partir de su formulario de modelo es muy sencillo:
from django.shortcuts import redirect, render
from django.utils.text import slugify
from .forms import AddPostForm
def add_post(request):
if request.method == 'POST':
if (form := AddPostForm(request.POST)).is_valid():
post = form.save()#(1)!
return redirect('posts:post-list')
else:
form = AddPostForm()
return render(request, 'posts/post/add.html', {'form': form})
- Toda la «lógica» de generación del «slug» queda encapsulada en el propio formulario.
Modelo
Una aproximación más genérica (y sostenible) sería centralizar los cambios a la hora de guardar el propio modelo.
Escenario con claves ajenas¶
Veamos ahora un ejemplo algo más elaborado, en el que no sólo tenemos un «post» sino que tenemos una clave ajena al usuario que lo escribió.
Por tanto queremos que cuando se cree un nuevo «post» desde el formulario de modelo, también se almacene de forma «automática» el usuario que lo escribió.
Veamos a continuación dos enfoques según lo que necesitemos:
La forma más «directa» de realizar esta tarea sería la siguiente:
from django import forms
from django.utils.text import slugify
from .models import Post
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
def save(self, user, *args, **kwargs):#(1)!
post = super().save(commit=False)#(2)!
post.user = user#(3)!
post = super().save(*args, **kwargs)#(4)!
return post
- Pasamos el usuario como primer parámetro del método de guardado.
- Construimos el «post» en memoria (sin aún escribir en disco).
- Asignamos el usuario a la clave ajena correspondiente en el «post».
- Guardamos definitivamente el objeto en la base de datos.
Ahora podremos simplificar el código de la vista correspondiente:
from django.shortcuts import redirect, render
from .forms import AddPostForm
def add_post(request):
if request.method == 'POST':
if (form := AddPostForm(request.POST)).is_valid():
post = form.save(request.user)#(1)!
return redirect('home')
else:
form = AddPostForm()
return render(request, 'posts/post/add.html', {'form': form})
- El primer parámetro que pasamos al método de guardado es el usuario.
Es posible que necesitemos realizar alguna lógica adicional en el constructor:
from django import forms
from django.utils.text import slugify
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
def __init__(self, user, *args, **kwargs):#(1)!
super().__init__(*args, **kwargs)#(2)!
self.user = user#(3)!
# Manejo posterior de "user"
def save(self, *args, **kwargs):
post = super().save(commit=False)
post.user = self.user#(4)!
post = super().save(*args, **kwargs)
return post
-
- Necesitamos sobreescribir el constructor del formulario.
- El detalle importante aquí es que el primer parámetro del constructor será el usuario
userque escribió el «post».
- Llamada al constructor de la clase base
forms.ModelForm. - Guardamos «temporalmente» el usuario que escribió el «post» para utilizarlo posteriormente.
- Asignamos el usuario a la clave ajena correspondiente en el «post».
Ahora podremos simplificar el código de la vista correspondiente:
from django.shortcuts import redirect, render
from .forms import AddPostForm
def add_post(request):
if request.method == 'POST':
if (form := AddPostForm(request.user, request.POST)).is_valid():#(1)!
post = form.save()#(2)!
return redirect('home')
else:
form = AddPostForm(request.user)#(3)!
return render(request, 'posts/post/add.html', {'form': form})
-
- El primer parámetro que pasamos al constructor del formulario es el usuario.
- El segundo parámetro son los datos propios del formulario.
- Simplemente con guardar el formulario ya se estará ejecutando toda la «lógica» necesaria.
- Al construir el formulario «vacío» también debemos pasar el usuario.
Validación¶
Django avanzado
Django permite añadir validación personalizada a los formularios. La validación de un formulario se puede hacer en varios contextos:
Validación individual
Validación cruzada
Validación individual¶
En este tipo de validación analizamos cada campo por separado. Si el formulario dispone de un campo llamado foo se podrá personalizar su validación implementando el método de instancia clean_foo().
Partiendo de un «blog» veamos un ejemplo de formulario para añadir «post» donde queremos validar que el título del «post» no esté completamente en mayúsculas:
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'content')
def clean_title(self):#(1)!
if (title := self.cleaned_data.get('title')).isupper():#(2)!
raise forms.ValidationError('Title cannot be all uppercase.')#(3)!
return title#(4)!
- Dado que queremos
limpiarvalidar el campotitletendremos que implementar el método de instanciaclean_title(). -
- Extraemos el valor del campo
title. - Comprobamos si está completamente en mayúsculas.
- Extraemos el valor del campo
- Lanzamos una excepción de tipo
ValidationErrorcuando queremos informar de un error de validación. - Siempre debemos devolver el valor del campo.
Validación de email
Un caso de uso interesante se podría dar en un formulario de registro tratando de evitar que dos usuarios utilicen la misma dirección de correo electrónico:
from django import forms
from django.contrib.auth import get_user_model
class SignupForm(forms.ModelForm):
class Meta:
model = get_user_model()
fields = ('username', 'password', 'first_name', 'last_name', 'email')
def clean_email(self):#(1)!
email = self.cleaned_data['email']#(2)!
if self._meta.model.objects.filter(email=email).count() > 0:#(3)!
raise forms.ValidationError('A user with that email already exists.')#(4)!
return email#(5)!
- Dado que queremos
limpiarvalidar el campoemailtendremos que implementar el método de instanciaclean_email(). - Extraemos el valor del campo
email. -
- Hacemos una consulta para ver si existe algún usuario con el mismo email.
- Estamos usando un «atajo» para acceder al modelo de usuario mediante
self._meta.model.
- Lanzamos una excepción de tipo
ValidationErrorcuando queremos informar de un error de validación. - Siempre debemos devolver el valor del campo.
Validación cruzada¶
Cuando la validación que queremos hacer involucra más de un campo, es adecuado implementar un método de instancia clean() para esta tarea.
Continuando con el ejemplo anterior, supongamos ahora que queremos validar que alguna de las palabras del título del «post» aparezcan en su contenido:
from django import forms
from .models import Post
class AddPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
def clean(self):#(1)!
title = self.cleaned_data.get('title')#(2)!
content = self.cleaned_data.get('content')#(3)!
if not any(w in content for w in title.split()):#(4)!
raise forms.ValidationError('Content must contain at least one word from the title.')#(5)!
- Validación «cruzada» por tanto tendremos que implementar el método de instancia
clean(). - Extraemos el título del «post».
- Extraemos el contenido del «post».
- Comprobamos que alguna palabra del título se encuentre en el contenido del «post».
- Lanzamos una excepción de tipo
ValidationErrorcuando queremos informar de un error de validación.
Acceso a errores
Para acceder a estos errores en una plantilla podemos utilizar {{ form.non_field_errors }}.