Saltar a contenido

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:

1⃣ Formularios de plantilla.
2⃣ Formularios de clase.
3⃣ 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:

posts/templates/posts/post/add.html
<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>
  1. 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.
  2. Los nombres que damos a los «widgets» son importantes. En este caso el nombre es post-title y contendrá el título del post que introduzca el usuario.
  3. Los nombres que damos a los «widgets» son importantes. En este caso el nombre es post-content y contendrá el contenido del post que introduzca el usuario.
  4. 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»:

posts/views.py
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)!

  1. Distinguimos el método de la petición HTTP.
  2. En request.POST tenemos un diccionario con todos los datos que provienen de la petición «post». La clave que buscamos debe coincidir con el atributo name del correspondiente «input» del formulario HTML.
  3. En request.POST tenemos un diccionario con todos los datos que provienen de la petición «post». La clave que buscamos debe coincidir con el atributo name del correspondiente «input» del formulario HTML.
  4. «Validación» del formulario → debe existir un título y un contenido para el post.
  5. Creamos el «slug» a partir del título del «post».
  6. Se crea un nuevo «post» a partir del título, contenido y «slug».
  7. Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
  8. En el caso de que falte algún campo de entrada, habrá que informar del error.
  9. 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)
  1. Tamaño máximo permitido.
  2. Tamaño mínimo permitido.
  3. Si es True se aplicará strip() sobre el valor.
  4. Iterable de tuplas de dos elementos.
  5. Iterable de formatos para convertir un «string» a un objeto datetime.date.
  6. Iterable de formatos para convertir un «string» a un objeto datetime.datetime.
  7. Máximo valor permitido.
  8. Mínimo valor permitido.
  9. Número máximo de dígitos permitido.
  10. Número máximo de lugares decimales permitido.
  11. Limita la entrada válida a un múltiplo de este valor.
  12. Tamaño máximo permitido.
  13. Tamaño mínimo permitido.
  14. Longitud máxima de fichero permitida.
  15. Si es True permite que el contenido del fichero esté vacío.
    • Ruta absoluta al directorio desde el que listar el contenido.
    • La ruta debe existir.
    • Si es True se listará recursivamente todo el contenido de la ruta indicada.
    • Por defecto es False.
  16. Patrón de expresión regular para limitar la búsqueda.
    • Si es True permite el listado de ficheros.
    • Por defecto es True.
    • Si es True permite el listado de directorios.
    • Por defecto es False.
  17. Máximo valor permitido.
  18. Mínimo valor permitido.
  19. Limita la entrada válida a un múltiplo de este valor.
  20. Limita la entrada al protocolo especificado.
  21. Desempaqueta direcciones IPv4.
  22. Tamaño máximo permitido.
  23. Máximo valor permitido.
  24. Mínimo valor permitido.
  25. Limita la entrada válida a un múltiplo de este valor.
  26. Una subclase de JSONEncoder para serializar los tipos de datos no soportados por el serializador JSON.
  27. Una subclase de JSONDecoder para deserializar la entrada.
  28. Iterable de tuplas de dos elementos.
  29. Expresión regular a aplicar.
    • Si es True permite que se acepten letras Unicode.
    • Por defecto es False.
  30. Iterable de formatos para convertir un «string» a un objeto datetime.time.
  31. Función que toma un argumento y devuelve el valor «coercionado».
  32. Función que toma un argumento y devuelve el valor «coercionado».
  33. Tamaño máximo permitido.
  34. Tamaño mínimo permitido.
  35. Lista de campos que se deberían usar para validar el valor.
  36. Tupla de campos cuyos valores se limpian y se combinan en un único valor.
    • Si es True se lanza un error de validación si algún campo está vacío.
    • Por defecto es True.
  37. Widget que se usará para los controles.
  38. Toma una lista de valores válidos y returna una versión «comprimida» de dichos valores.
  39. Lista de formatos para convertir un «string» a un objeto datetime.date.
  40. Lista de formatos para convertir un «string» a un objeto datetime.time.
  41. QuerySet de objetos desde donde tomar las opciones del campo.
  42. Texto del «widget» que indica el valor vacío.
  43. Campo a usar como valor de las opciones del «widget».
  44. Indica si se creará una opción vacía al usar el «widget» RadioSelect.
  45. Iterador usado para generar las opciones desde el «queryset».
  46. QuerySet de objetos desde donde tomar las opciones del campo.
  47. Campo a usar como valor de las opciones del «widget».
  48. 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:

posts/forms.py
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 Form al nombre de una clase de formulario.
    • Una clase de formulario debe heredar de django.forms.Form.
  1. 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:

posts/templates/posts/post/add.html
<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>
  1. 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.
  2. Con esto basta para que se renderice el contenido del formulario en HTML.
  3. 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:

posts/views.py
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 en forms.py.
    • 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 en forms.py.
  1. Creamos el «slug» a partir del título del «post».
  2. Se crea un nuevo «post» a partir del título, contenido y «slug».
  3. 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.
  4. 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»:

posts/forms.py
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 Form al nombre de una clase de formulario.
    • Una clase formulario de modelo debe heredar de django.forms.ModelForm.
  1. Django permite añadir metadatos a una clase incorporando otra clase interior llamada Meta.
  2. En el atributo de clase models indicamos el modelo al que vincular el presente formulario.
    • En el atributo de clase fields indicamos 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 exclude para excluir ciertos campos del modelo.

Ahora veremos cómo es el código de la plantilla:

posts/templates/posts/post/add.html
<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>
  1. 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.
  2. Con esto basta para que se renderice el contenido del formulario en HTML.
  3. 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:

posts/views.py
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.
  1. Una llamada a form.save() en un formulario de modelo guarda el objeto de modelo en la base de datos y lo devuelve.
  2. 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.
  3. Renderizamos la plantilla pasando el formulario como contexto y la devolvemos.

posts/views.py
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.POST lo usamos, en otro caso pasamos None para construir un formulario vacío.
    • El método is_valid() comprueba si los datos del formulario son válidos.
  1. Una llamada a form.save() en un formulario de modelo guarda el objeto de modelo en la base de datos y lo devuelve.
  2. Todo ha ido bien → Redirigimos (por ejemplo) a la página con el listado de todos los «posts».
  3. 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:

posts/views.py
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=False para que no se escriba aún en disco.
    • Creamos el «slug» a partir del título del «post».
    • Nótese que ya podemos acceder a la variable post como un objeto de tipo Post.
  1. Ahora sí que definitivamente guardamos el objeto en la base de datos.
  2. 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.
  3. 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»:

posts/forms.py
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:

posts/templates/posts/post/edit.html
<h1>Editando post "{{ post.title }}"</h1><!--(1)!-->

<form method="post" novalidate><!--(2)!-->
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Guardar">
</form>

  1. Aprovechamos para mostrar el título del «post» en la plantilla.
  2. 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:

posts/views.py
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)!

  1. Necesitamos conocer el «slug» (u otro campo único) del «post» ya que estamos editando dicho objeto.
  2. 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=False para que no se escriba aún en disco.
    • Creamos el «slug» a partir del título del «post».
    • Nótese que ya podemos acceder a la variable post como un objeto de tipo Post.
  3. Ahora sí que definitivamente guardamos el objeto en la base de datos.
  4. 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.
  5. 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_format
time_format
date_attrs
time_attrs
SplitHiddenDateTimeWidget <input type="date" ...> Fecha y hora divididos (ocultos) date_format
time_format
date_attrs
time_attrs
SelectDateWidget <input type="date" ...> Fecha (compuesta por mes, día y año) years
months
empty_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»:

posts/forms.py
from django import forms


class AddPostForm(forms.Form):
    title = forms.CharField()
    content = forms.CharField(widget=forms.Textarea)#(1)!

  1. Usamos el parámetro widget en el constructor del campo CharField indicando que queremos usar un «widget» de tipo Textarea.

posts/forms.py
from django import forms

from .models import Post


class AddPostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'content')
        widgets = {'content': forms.Textarea()}#(1)!

  1. El atributo widgets de la clase Meta nos 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:

posts/forms.py
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:

posts/forms.py
from django import forms


class AddPostForm(forms.Form):
    title = forms.CharField()
    content = forms.CharField(widget=forms.Textarea)

    title.widget.attrs.update({'id': 'post-title'})
    content.widget.attrs.update({'class': 'form-control'})

Una primera aproximación sería modificar la definición en la clase Meta:

posts/forms.py
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:

posts/forms.py
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:

posts/forms.py
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)!

  1. También es aplicable a formularios de clase que hereden de forms.Form.
  2. Debemos implementar el constructor del formulario para realizar cambios cuando se construye cada instancia.
  3. Llamada al constructor de la clase base forms.ModelForm.
  4. Recorremos los campos visibles del formulario.
  5. 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:

posts/forms.py
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:

DateInput

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:

posts/forms.py
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 clase forms.ModelForm.
    • Esto también sería válido para forms.Form.
    • Guardamos el formulario sin escribir en disco.
    • Con ello obtendremos un objeto Post en memoria que podremos manipular.
  1. Generamos el «slug» del «post» a partir del título.
  2. Ahora ya podemos almacenar el objeto post en disco llamando al método de la clase base.
  3. 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:

posts/views.py
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})

  1. 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:

posts/forms.py
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

  1. Pasamos el usuario como primer parámetro del método de guardado.
  2. Construimos el «post» en memoria (sin aún escribir en disco).
  3. Asignamos el usuario a la clave ajena correspondiente en el «post».
  4. Guardamos definitivamente el objeto en la base de datos.

Ahora podremos simplificar el código de la vista correspondiente:

posts/views.py
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})

  1. 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:

posts/forms.py
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 user que escribió el «post».
  1. Llamada al constructor de la clase base forms.ModelForm.
  2. Guardamos «temporalmente» el usuario que escribió el «post» para utilizarlo posteriormente.
  3. Asignamos el usuario a la clave ajena correspondiente en el «post».

Ahora podremos simplificar el código de la vista correspondiente:

posts/views.py
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.
  1. Simplemente con guardar el formulario ya se estará ejecutando toda la «lógica» necesaria.
  2. 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:

1⃣ Validación individual
2⃣ 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:

posts/forms.py
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)!

  1. Dado que queremos limpiar validar el campo title tendremos que implementar el método de instancia clean_title().
    • Extraemos el valor del campo title.
    • Comprobamos si está completamente en mayúsculas.
  2. Lanzamos una excepción de tipo ValidationError cuando queremos informar de un error de validación.
  3. 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:

accounts/forms.py
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)!

  1. Dado que queremos limpiar validar el campo email tendremos que implementar el método de instancia clean_email().
  2. 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.
  3. Lanzamos una excepción de tipo ValidationError cuando queremos informar de un error de validación.
  4. 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:

posts/forms.py
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)!

  1. Validación «cruzada» por tanto tendremos que implementar el método de instancia clean().
  2. Extraemos el título del «post».
  3. Extraemos el contenido del «post».
  4. Comprobamos que alguna palabra del título se encuentre en el contenido del «post».
  5. Lanzamos una excepción de tipo ValidationError cuando 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 }}.