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.
La lección 16 dejó un ejemplo con una advertencia grande encima: no copies esto a ningún sitio. Era este:
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:
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:
- Dibuja los campos en HTML.
- Valida lo que llega.
- 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:
formulario = Contacta() # vacío: para enseñar
formulario = Contacta(peticion.POST) # con datos: para comprobarunbound is_valid: False | is_bound: FalseUn 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#
formulario = Contacta({'nombre': ' Ana Ruiz ', 'correo': 'ana@ejemplo.es',
'mensaje': 'un mensaje suficientemente largo', 'copia': 'on'})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:
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:
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:
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.
conserva nombre: True
conserva mensaje: TrueEso 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:
<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#
<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__:
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:
{% 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:
{{ formulario.nombre.label_tag }}
{{ formulario.nombre }}
{{ formulario.nombre.errors }}Cada {{ formulario.nombre }} es un objeto con bastante dentro:
label : Correo de respuesta
id_for_label : id_correo
html_name : correo
value() : None
help_text : ''
field.required: TrueFí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.