Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 25 de 45

Formularios

La clase que resuelve de golpe los tres fallos del ejemplo marcado como incorrecto, y el ciclo de ocho líneas que se repite en todas las vistas.

⏱ 15 min de lectura

La lección 16 dejó un ejemplo con una advertencia grande encima: no copies esto a ningún sitio. Era este:

python
Contacto.objects.create(
    nombre=peticion.POST['nombre'],
    correo=peticion.POST['correo'],
)

Y sus tres problemas: revienta con MultiValueDictKeyError si falta un campo, no valida nada porque save() no valida, y no hay protección contra formularios falsificados. Añade un cuarto que no se mencionó: cuando algo falla, hay que volver a pintar el formulario con lo que la persona ya había escrito, y eso a mano es horrible.

Los cuatro los resuelve una sola clase.

Declarar un formulario#

Se parece mucho a un modelo, y esa semejanza es intencionada:

contactos/forms.py
from django import forms


class Contacta(forms.Form):
        nombre = forms.CharField(max_length=100, label='Tu nombre')
    correo = forms.EmailField(label='Correo de respuesta')
    asunto = forms.CharField(max_length=80, required=False)
    mensaje = forms.CharField(widget=forms.Textarea, min_length=10)
    copia = forms.BooleanField(required=False, label='Mándame una copia')

Se parece pero no es lo mismo, y la diferencia importa: un modelo describe una tabla y un formulario describe una pregunta. Este no guarda nada; los datos se van a ir por correo. Cuando lo que se pregunta coincide con lo que se guarda, hay una versión que se escribe sola, y es la lección siguiente.

Fíjate en required=False: en los formularios, lo obligatorio es lo normal y hay que desactivarlo. Es al revés que en los modelos, donde blank=True era lo que había que añadir.

Un formulario hace tres cosas a la vez, y conviene tenerlas separadas en la cabeza porque después se tocan por separado:

  1. Dibuja los campos en HTML.
  2. Valida lo que llega.
  3. Convierte el texto de la petición a tipos de Python.

Vacío o con datos#

Esta distinción es la que más confunde al principio, y es simple:

python
formulario = Contacta()               # vacío: para enseñar
formulario = Contacta(peticion.POST)  # con datos: para comprobar
Salida en pantalla
unbound is_valid: False | is_bound: False

Un formulario vacío nunca es válido. No es que los datos estén mal, es que no hay datos que comprobar. Llamar a is_valid() sobre uno vacío siempre da False, y ese es el motivo del if peticion.method == 'POST' de la vista: solo tiene sentido validar cuando hay algo que validar.

El argumento posicional son los datos. No hay que ponerle nombre, y de hecho Contacta(data=peticion.POST) es lo mismo.

is_valid y cleaned_data#

python
formulario = Contacta({'nombre': '  Ana Ruiz  ', 'correo': 'ana@ejemplo.es',
                       'mensaje': 'un mensaje suficientemente largo', 'copia': 'on'})
Salida en pantalla
válido?: True
cleaned_data: {'nombre': 'Ana Ruiz', 'correo': 'ana@ejemplo.es', 'asunto': '',
               'mensaje': 'un mensaje suficientemente largo', 'copia': True}
tipos: {'nombre': 'str', 'correo': 'str', 'asunto': 'str', 'mensaje': 'str', 'copia': 'bool'}

Dos cosas que han pasado sin pedirlas. Los espacios de ' Ana Ruiz ' han desaparecido, porque CharField viene con strip=True. Y el 'on' de la casilla, que es lo que manda de verdad un navegador, ha llegado como True.

Eso último es la tercera función: en peticion.POST todo es texto, y en cleaned_data cada cosa tiene su tipo. Un IntegerField da un int, un DateField da un date, un DecimalField da un Decimal. Es exactamente el trabajo manual que se hizo a mano en el módulo 2 con los argumentos de la terminal.

La regla que se deriva: después de validar, se usa cleaned_data y no se vuelve a mirar peticion.POST.

Con datos malos:

Salida en pantalla
válido?: False
errores:
    correo  -> Introduzca una dirección de correo electrónico válida.
    mensaje -> Asegúrese de que este valor tenga al menos 10 carácter(es) (tiene 5).

Los mensajes salen en español sin instalar nada, por el LANGUAGE_CODE de la lección 5.

El ciclo en la vista#

Este trozo se va a repetir en todas las vistas que reciban datos, con variaciones mínimas. Merece la pena aprendérselo de memoria:

contactos/views.py
def contacta(peticion):
        if peticion.method == 'POST':
                formulario = Contacta(peticion.POST)
        if formulario.is_valid():
                        datos = formulario.cleaned_data
            enviar_correo(datos['correo'], datos['mensaje'])
            return redirect('contactos:gracias')
    else:
                formulario = Contacta(initial={'asunto': 'Consulta'})
    return render(peticion, 'contactos/contacta.html', {'formulario': formulario})

Los tres caminos, medidos:

Salida en pantalla
GET                ->  200, formulario vacío con el asunto puesto
POST inválido      ->  200, con los errores y lo escrito conservado
POST válido        ->  302 /contactos/gracias/

Hay tres cosas que conviene mirar con detalle.

El return render está fuera del if, y eso es lo que hace que un POST inválido caiga ahí abajo y se vuelva a pintar el mismo formulario, ya con sus errores dentro. No hay que hacer nada más: el objeto ya los tiene.

Salida en pantalla
conserva nombre:  True
conserva mensaje: True

Eso es el cuarto problema resuelto. Quien rellenó el formulario no pierde lo que escribió, porque el formulario se pintó a partir de los datos enviados.

initial pone valores de partida, y solo tiene sentido en el formulario vacío:

Salida en pantalla
<input type="text" name="asunto" value="Consulta" maxlength="80" id="id_asunto">

Y solo se redirige cuando todo ha ido bien, que es el patrón de la lección 2. Un 200 después de guardar deja a la gente recargando y duplicando.

Pintarlo#

contactos/templates/contactos/contacta.html
<form method="post" novalidate>{% csrf_token %}
  {{ formulario.non_field_errors }}
  {{ formulario }}
  <button>Enviar</button>
</form>

Tres cosas de esas cinco líneas hay que explicarlas.

El <form> y el botón no los pinta Django. {{ formulario }} son los campos, nada más. La primera vez todo el mundo se pregunta por qué no sale el botón.

El {% csrf_token %} es obligatorio y su ausencia da 403, como se vio en la lección 24.

Y non_field_errors son los errores que no son de ningún campo concreto, como "la contraseña y su repetición no coinciden". Sin esa línea no se ven en ninguna parte, porque no tienen campo al lado donde ponerse. Es un olvido clásico y el síntoma es un formulario que se niega a enviarse sin decir por qué. Salen en errors bajo la clave __all__:

Salida en pantalla
errors: {'__all__': ['A y B no pueden ser iguales.']}

Cuando hace falta más control#

{{ formulario }} sirve para empezar y se queda corto en cuanto hay un diseño. Se puede bajar todo lo que haga falta:

html
{% for campo in formulario %}
  <div class="campo {% if campo.errors %}con-error{% endif %}">
    {{ campo.label_tag }}
    {{ campo }}
    {% if campo.help_text %}<small>{{ campo.help_text }}</small>{% endif %}
    {{ campo.errors }}
  </div>
{% endfor %}

O campo a campo, cuando el orden en la página no es el del formulario:

html
{{ formulario.nombre.label_tag }}
{{ formulario.nombre }}
{{ formulario.nombre.errors }}

Cada {{ formulario.nombre }} es un objeto con bastante dentro:

Salida en pantalla
label        : Correo de respuesta
id_for_label : id_correo
html_name    : correo
value()      : None
help_text    : ''
field.required: True

Fíjate en field: el objeto del bucle no es el campo que declaraste, es un envoltorio que junta ese campo con el valor y los errores de esta petición concreta. Por eso las propiedades de la declaración se piden a través de campo.field.