Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 27 de 45

Validación

El orden exacto en que Django comprueba las cosas, los tres sitios donde puedes meter el tuyo, y el return que se olvida y destruye datos en silencio.

⏱ 16 min de lectura

Hasta ahora la validación ha funcionado sola: longitudes, formatos, campos obligatorios, unique. Eso cubre lo que se puede deducir de la declaración, y en cuanto la regla es del negocio ya no se deduce de nada.

El teléfono tiene que ser de nueve dígitos. La fecha de baja no puede ser anterior a la de alta. Las dos contraseñas tienen que coincidir. Si el grupo es "Trabajo", el correo es obligatorio.

Ahí hay que escribir código, y Django ofrece tres sitios donde ponerlo. Elegir mal no es grave, pero da errores que aparecen en el campo equivocado o que no aparecen nunca.

El orden#

Esto es lo que hay que tener claro antes que nada, porque explica casi todos los desconciertos. Con un formulario instrumentado para que cuente lo que hace:

Salida en pantalla
validador('abc')
clean_uno('abc')
clean_dos('def')
clean() ve ['dos', 'uno']

cleaned_data: {'uno': 'ABC', 'dos': 'def'}

Django recorre campo por campo, y con cada uno hace tres cosas en este orden: lo convierte al tipo que toca, pasa sus validadores, y llama a su clean_campo si existe. Cuando ha terminado con todos, llama una vez a clean.

Fíjate en el 'ABC': el clean_uno devolvió el valor en mayúsculas y eso es lo que quedó. Lo que devuelve un clean_campo sustituye al valor, así que es el sitio de las normalizaciones.

Y ahora lo importante, un campo que falla:

Salida en pantalla
validador('xyz')
clean_dos('def')
clean() ve ['dos']

errores: {'uno': ['No puede empezar por x.']}

Han pasado dos cosas. El clean_uno no se ha llamado, porque su validador ya había fallado. Y cuando le toca el turno a clean, el campo uno no está en cleaned_data.

Con un campo vacío, ni siquiera se llega al validador:

Salida en pantalla
clean_dos('def')
clean() ve ['dos']
errores: {'uno': ['Este campo es obligatorio.']}

De ahí sale la regla que evita el error más frecuente de esta lección:

Los tres sitios#

Validadores#

Una función que recibe el valor y lanza una excepción si no le gusta:

python
from django.core.exceptions import ValidationError


def solo_dias_laborables(fecha):
        if fecha.weekday() >= 5:
                raise ValidationError('Tiene que ser un día laborable.')
python
fecha = forms.DateField(validators=[solo_dias_laborables])

Un validador no devuelve nada: o calla o lanza. Y solo ve su propio valor, que es su limitación y su virtud, porque eso lo hace reutilizable.

Django trae unos cuantos hechos:

python
from django.core.validators import RegexValidator, MinValueValidator, validate_email

solo_digitos = RegexValidator(r'^[0-9]{9}$', 'Nueve dígitos, sin espacios.')
Salida en pantalla
'600112233'     -> True
'600 11 22 33'  -> False  ['Nueve dígitos, sin espacios.']

Y hay una ventaja de peso: un validador se puede poner en el campo del modelo, y entonces vale para todos los formularios y para el panel de administración a la vez. Es el sitio correcto para las reglas de formato.

clean_campo#

Un método con el nombre del campo, para lo que solo afecta a ese campo:

python
def clean_nombre(self):
        valor = self.cleaned_data['nombre']
    if Contacto.objects.filter(nombre__iexact=valor).exists():
                raise ValidationError('Ya hay un contacto con ese nombre.')
    return valor

Dos diferencias con un validador. Tiene acceso a self, así que puede consultar la base de datos o mirar el objeto que se está editando. Y devuelve el valor, que es lo que se guarda.

Por eso es el sitio de las normalizaciones:

python
def clean_telefono(self):
        return self.cleaned_data['telefono'].replace(' ', '').replace('-', '')

Dentro de un clean_campo se puede usar self.cleaned_data['nombre'] con corchetes sin miedo: si el campo hubiera fallado, este método no se habría llamado.

clean#

Para lo que necesita más de un campo. Es el único que los ve todos:

python
def clean(self):
        datos = super().clean()
    if datos.get('grupo') == 'trabajo' and not datos.get('correo'):
                self.add_error('correo', 'El correo es obligatorio en el grupo de trabajo.')
    return datos

Tres cosas de ese trozo, y las tres se olvidan:

El super().clean() del principio, que es lo que rellena cleaned_data.

El return datos del final. Se puede omitir en versiones recientes de Django, y aun así conviene ponerlo siempre, porque la costumbre de no ponerlo se contagia a clean_campo, donde sí es obligatorio y silencioso.

El error va a un campo concreto con add_error, no lanzado.

raise o add_error#

Las dos formas de decir que algo está mal, y no hacen lo mismo:

python
raise ValidationError('Uno y dos no pueden ser iguales.')   # error general
self.add_error('b', 'No puede ser igual que A.')            # error de un campo
Salida en pantalla
raise      -> {'__all__': ['Uno y dos no pueden ser iguales.']}
add_error  -> {'b': ['No puede ser igual que A.']}

Un raise dentro de clean produce un error general, y esos solo se ven si la plantilla tiene el {{ formulario.non_field_errors }} de la lección 25. Un error general que nadie pinta es un formulario que se niega a enviarse sin explicar por qué.

add_error lo pone al lado del campo, que es donde la gente lo busca. Y hace una cosa más:

Salida en pantalla
sigue b en cleaned_data?: False

Saca el campo de cleaned_data. Es coherente: un valor con error no es un valor limpio, y así el resto del método no lo usa por descuido.

La regla: si el error se puede atribuir a un campo, add_error con ese campo. El raise se reserva para lo que de verdad no es de ninguno, como "esta combinación de opciones no existe".

Errores con código y parámetros#

La forma completa de un error:

python
raise ValidationError(
    'Hay que tener al menos %(min)s años (tienes %(tuyos)s).',
    code='menor',
    params={'min': 18, 'tuyos': valor},
)
Salida en pantalla
{'edad': ['Hay que tener al menos 18 años (tienes 15).']}
as_json: {"edad": [{"message": "Hay que tener al menos 18 años ...", "code": "menor"}]}

Los params se interpolan con %(nombre)s, y no con una f-string. Hay un motivo: así el mensaje se puede traducir manteniendo los huecos, que es lo que permite que los errores de Django salgan en español.

El code es un identificador que no se enseña y sirve para dos cosas. Para sustituir el mensaje desde el error_messages del Meta, como se hizo en la lección 26 con unique. Y para que quien consuma la respuesta en JSON pueda distinguir errores sin leer el texto, que importa en el módulo 5.

Y para varios errores en el mismo campo, una lista:

python
raise ValidationError([
    ValidationError('Muy corta.', code='corta'),
    ValidationError('Tiene que llevar algún número.', code='sin_numero'),
])
Salida en pantalla
{'clave': ['Muy corta.', 'Tiene que llevar algún número.']}

Eso es mejor que parar en el primero. Quien rellena el formulario ve todo lo que tiene que arreglar de una vez, en lugar de descubrirlo de uno en uno.