Validación de formularios y campos

La validación de formularios ocurre cuando se limpian los datos. Si deseas personalizar este proceso, hay varios lugares donde puedes hacer cambios, cada uno con un propósito diferente. Tres tipos de métodos de limpieza se ejecutan durante el procesamiento del formulario. Estos se ejecutan normalmente cuando llamas al método is_valid() en un formulario. Hay otras cosas que también pueden desencadenar la limpieza y la validación (accediendo a la propiedad errors o llamando directamente a full_clean(), pero normalmente no serán necesarias.

En general, cualquier método de limpieza puede levantar ValidationError si hay un problema con los datos que está procesando, pasando la información relevante al constructor ValidationError. Consulta abajo para la práctica recomendada en el levantamiento de ValidationError. Si no se levanta ninguna ValidationError, el método debe devolver los datos limpios (normalizados) como un objeto Python.

La mayoría de las validaciones pueden realizarse utilizando validators - ayudantes que se pueden reutilizar. Los validadores son funciones (o llamables) que toman una sola argumento y levantan ValidationError en la entrada inválida. Los validadores se ejecutan después de que se han llamado los métodos to_python y validate del campo.

La validación de un formulario se divide en varios pasos, que pueden personalizarse o sobrescribirse:

  • El método to_python() en un Field es el primer paso en cada validación. Convierte el valor a un tipo de datos correcto y levanta ValidationError si no es posible. Este método acepta el valor bruto del widget y devuelve el valor convertido. Por ejemplo, un FloatField convierte los datos en un Python float o levanta una ValidationError.

  • El método validate() en un Field maneja la validación específica de campo que no es adecuada para un validador. Toma un valor que ha sido convertido a un tipo de datos correcto y levanta ValidationError en cualquier error. Este método no devuelve nada y no debería alterar el valor. Debes sobrescribirlo para manejar la lógica de validación que no puedes o no quieres poner en un validador.

  • El método run_validators() en un Field ejecuta todos los validadores del campo y agrega todas las errores a una sola ValidationError. No deberías necesitar sobrescribir este método.

  • El clean() método en una subclase de Field es responsable de ejecutar to_python(), validate(), y run_validators() en el orden correcto y propagar sus errores. Si, en cualquier momento, cualquiera de los métodos levanta ValidationError, la validación se detiene y ese error se eleva. Este método devuelve los datos limpios, que luego se insertan en el diccionario cleaned_data de la forma.

  • El método clean_<fieldname>() se llama en una subclase de formulario – donde <fieldname> se reemplaza con el nombre del atributo del campo de la forma. Este método realiza cualquier limpieza específica de ese atributo particular, sin relación con el tipo de campo que es. Este método no recibe ningún parámetro. Deberás buscar el valor del campo en self.cleaned_data y recordar que será un objeto Python en este punto, no la cadena original enviada en el formulario (estará en cleaned_data porque el método de campo general clean(), anteriormente mencionado, ya ha limpiado los datos una vez).

    Por ejemplo, si querías validar que el contenido de un CharField llamado serialnumber era único, clean_serialnumber() sería el lugar adecuado para hacerlo. No necesitas un campo específico (es un CharField), pero quieres una pieza de validación y limpieza/normalización del formulario específica.

    El valor de retorno de este método reemplaza el valor existente en cleaned_data, por lo que debe ser el valor del campo desde cleaned_data (incluso si este método no lo cambió) o un nuevo valor limpio.

  • El método clean() de la subclase de formulario puede realizar validaciones que requieren acceso a múltiples campos de la forma. Este es donde podrías poner comprobaciones como «si se proporciona el campo A, el campo B debe contener una dirección de correo electrónico válida». Este método puede devolver un diccionario completamente diferente si lo desea, que se utilizará como cleaned_data.

    Dado que los métodos de validación de campos han sido ejecutados en el momento en que se llama clean(), también tienes acceso al atributo errors del formulario, que contiene todos los errores levantados por la limpieza de campos individuales.

    Ten en cuenta que cualquier error levantado por tu sobrescritura del método clean() no estará asociado con ningún campo en particular. Van a un «campo» especial (llamado __all__), que puedes acceder mediante el método non_field_errors() si lo necesitas. Si deseas agregar errores a un campo específico del formulario, debes llamar al método add_error().

    También ten en cuenta que hay consideraciones especiales cuando sobrescribes el método clean() de una subclase de ModelForm. (consulte la documentación de ModelForm <overriding-modelform-clean-method> para obtener más información)

Estos métodos se ejecutan en el orden dado arriba, uno por uno. Es decir, para cada campo en la forma (en el orden en que están declarados en la definición de la forma), se ejecuta el método Field.clean() (o su sobrescritura) y luego clean_<fieldname>(). Finalmente, una vez que esos dos métodos han sido ejecutados para cada campo, se ejecuta el método Form.clean() , o su sobrescritura, ya sea que los métodos anteriores hayan levantado errores.

Se proporcionan ejemplos de cada uno de estos métodos a continuación.

Los métodos mencionados pueden levantar una ValidationError. Para cualquier campo, si el método Field.clean() levanta una ValidationError, no se llama ningún método de limpieza específico del campo. Sin embargo, los métodos de limpieza para todos los campos restantes aún se ejecutan.

Levantar ValidationError

Para hacer que los mensajes de error sean flexibles y fáciles de sobreescribir, considera las siguientes directrices:

  • Proporciona un código de error descriptivo al constructor:

    # Good
    ValidationError(_("Invalid value"), code="invalid")
    
    # Bad
    ValidationError(_("Invalid value"))
    
  • No fuerces variables en el mensaje; utiliza marcadores y el argumento params del constructor:

    # Good
    ValidationError(
        _("Invalid value: %(value)s"),
        params={"value": "42"},
    )
    
    # Bad
    ValidationError(_("Invalid value: %s") % value)
    
  • Utiliza claves de mapeo en lugar de formato posicional. Esto permite poner las variables en cualquier orden o omitirlas por completo cuando se reescribe el mensaje:

    # Good
    ValidationError(
        _("Invalid value: %(value)s"),
        params={"value": "42"},
    )
    
    # Bad
    ValidationError(
        _("Invalid value: %s"),
        params=("42",),
    )
    
  • Envuelve el mensaje con gettext para habilitar la traducción:

    # Good
    ValidationError(_("Invalid value"))
    
    # Bad
    ValidationError("Invalid value")
    

Poniendo todo junto:

raise ValidationError(
    _("Invalid value: %(value)s"),
    code="invalid",
    params={"value": "42"},
)

Seguir estas directrices es particularmente necesario si escribes formularios, campos de formulario y campos de modelo reutilizables.

Si bien no se recomienda, si estás en la cadena de validación final (es decir, tu método clean() del formulario) y sabes que nunca necesitarás sobreescribir tu mensaje de error puedes optar por lo menos verbose:

ValidationError(_("Invalid value: %s") % value)

Los textos traducidos son:

Alzar múltiples errores

Si detectas múltiples errores durante un método de limpieza y deseas señalar todos ellos al remitente del formulario, es posible pasar una lista de errores al constructor de ValidationError.

Como arriba se indica, se recomienda pasar una lista de instancias de ValidationError con codes y params, pero también funcionará una lista de cadenas de texto:

# Good
raise ValidationError(
    [
        ValidationError(_("Error 1"), code="error1"),
        ValidationError(_("Error 2"), code="error2"),
    ]
)

# Bad
raise ValidationError(
    [
        _("Error 1"),
        _("Error 2"),
    ]
)

Uso de la validación en práctica

Las secciones anteriores explicaron cómo funciona la validación en general para los formularios. Dado que a veces puede ser más fácil poner las cosas en su lugar viendo cada característica en uso, aquí hay una serie de pequeños ejemplos que utilizan cada uno de las características anteriores.

Uso de validadores

Los campos de formularios (y modelos) de Django admiten el uso de funciones y clases de utilidad conocidas como validadores. Un validador es un objeto callable o función que toma un valor y devuelve nada si el valor es válido o lanza una ValidationError si no lo es. Estos se pueden pasar al constructor del campo, a través del argumento validators del campo, o definirse en la clase de campo con el atributo default_validators.

Los validadores se pueden utilizar para validar valores dentro del campo, veamos un ejemplo con el campo SlugField de Django:

from django.core import validators
from django.forms import CharField


class SlugField(CharField):
    default_validators = [validators.validate_slug]

Como puedes ver, SlugField es un CharField con un validador personalizado que valida que el texto enviado cumple con algunas reglas de caracteres. Esto también se puede hacer en la definición del campo así:

slug = forms.SlugField()

Los textos traducidos son:

slug = forms.CharField(validators=[validators.validate_slug])

Casos comunes como validar contra un correo electrónico o una expresión regular se pueden manejar utilizando clases de validadores existentes disponibles en Django. Por ejemplo, validators.validate_slug es una instancia de la clase RegexValidator construida con el primer argumento siendo el patrón: ^[-a-zA-Z0-9_]+\Z. Consulte la sección sobre escribir validadores para ver una lista de lo que ya está disponible y un ejemplo de cómo escribir un validador.

Limpieza por defecto del campo de formulario

Vamos a crear primero un campo de formulario personalizado que valide que su entrada sea una cadena conteniendo direcciones de correo electrónico separadas por comas. La clase completa se ve así:

from django import forms
from django.core.validators import validate_email


class MultiEmailField(forms.Field):
    def to_python(self, value):
        """Normalize data to a list of strings."""
        # Return an empty list if no input was given.
        if not value:
            return []
        return value.split(",")

    def validate(self, value):
        """Check if value consists only of valid emails."""
        # Use the parent's handling of required fields, etc.
        super().validate(value)
        for email in value:
            validate_email(email)

Cada formulario que utilice este campo tendrá ejecutados estos métodos antes de poder hacer nada más con los datos del campo. Esta es la limpieza específica de este tipo de campo, independientemente de cómo se utilice posteriormente.

Vamos a crear un ContactForm para demostrar cómo utilizarías este campo:

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField()
    sender = forms.EmailField()
    recipients = MultiEmailField()
    cc_myself = forms.BooleanField(required=False)

Utiliza MultiEmailField como cualquier otro campo de formulario. Cuando se llama al método is_valid() del formulario, el método clean() del campo MultiEmailField se ejecutará como parte del proceso de limpieza y llamará a los métodos personalizados to_python() y validate().

Limpieza de un atributo específico del campo

Continuando con el ejemplo anterior, supongamos que en nuestro ContactForm, queremos asegurarnos de que el campo recipients siempre contenga la dirección "fred@example.com". Esta es una validación específica de nuestro formulario, por lo que no queremos ponerla en la clase general MultiEmailField. En su lugar, escribimos un método de limpieza que opera sobre el campo recipients, como se muestra a continuación:

from django import forms
from django.core.exceptions import ValidationError


class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean_recipients(self):
        data = self.cleaned_data["recipients"]
        if "fred@example.com" not in data:
            raise ValidationError("You have forgotten about Fred!")

        # Always return a value to use as the new cleaned data, even if
        # this method didn't change it.
        return data

Limpieza y validación de campos que dependen entre sí

Supongamos que agregamos otra restricción a nuestro formulario de contacto: si el campo cc_myself es True, el subject debe contener la palabra "ayuda". Estamos realizando validaciones en más de un campo al mismo tiempo, por lo que el método limpiar() del formulario es un buen lugar para hacer esto. Nota que estamos hablando sobre el método limpiar() del formulario aquí, mientras que anteriormente estábamos escribiendo un método limpiar() en un campo. Es importante mantener la diferencia entre campos y formularios claros cuando se determina dónde validar las cosas. Los campos son puntos de datos individuales, los formularios son una colección de campos.

Por el momento que se llama al método limpiar() del formulario, todos los métodos limpios de campo individual habrán sido ejecutados (las dos secciones anteriores), por lo que self.cleaned_data estará poblado con cualquier dato que haya sobrevivido hasta ahora. Por tanto también necesitas recordar permitir el hecho de que los campos que deseas validar pueden no haber sobrevivido a las comprobaciones iniciales de campo individual.

Hay dos formas de informar cualquier error en este paso. Probablemente la forma más común es mostrar el error en la parte superior del formulario. Para crear tal error, puedes levantar una ValidationError desde el método limpiar(). Por ejemplo:

from django import forms
from django.core.exceptions import ValidationError


class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super().clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject:
            # Only do something if both fields are valid so far.
            if "help" not in subject:
                raise ValidationError(
                    "Did not send for 'help' in the subject despite CC'ing yourself."
                )

En este código, si se levanta la excepción de validación, el formulario mostrará un mensaje de error en la parte superior del formulario (normalmente) que describe el problema. Tales errores son no-errores de campo, que se muestran en el template con {{ form.non_field_errors }}.

La llamada a super().limpiar() en el ejemplo de código garantiza que cualquier lógica de validación en clases padre se mantenga. Si tu formulario hereda otro que no devuelve un diccionario cleaned_data en su método limpiar(), entonces no asigna cleaned_data al resultado de la llamada a super() y utiliza self.cleaned_data en su lugar:

def clean(self):
    super().clean()
    cc_myself = self.cleaned_data.get("cc_myself")
    ...

La segunda aproximación para informar errores de validación podría implicar asignando el mensaje de error a uno de los campos. En este caso, vamos a asignar un mensaje de error tanto al campo «asunto» como a las filas «cc_myself» del formulario de visualización. Ten cuidado cuando hagas esto en la práctica, ya que puede llevar a una salida confusa del formulario. Estamos mostrando lo posible aquí y dejándolo a usted y sus diseñadores para trabajar en qué funciona efectivamente en su situación particular. Nuestro nuevo código (sustituyendo el ejemplo anterior) se ve así:

from django import forms


class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super().clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject and "help" not in subject:
            msg = "Must put 'help' in subject when cc'ing yourself."
            self.add_error("cc_myself", msg)
            self.add_error("subject", msg)

El segundo argumento de add_error() puede ser una cadena, o preferiblemente una instancia de ValidationError. Consulta levantar-error-de-validación para obtener más detalles. Nota que add_error() elimina automáticamente el campo de cleaned_data.