Flask

Flask

Tutorial Flask: Validación de formularios con WTForms

WTForms: Aprende cómo instalar y configurar WTForms usando Flask y Flask-WTF. Descriptores de seguridad y eficiencia para manejar formularios.

Aprende Flask GRATIS y certifícate

Instalación y configuración inicial de WTForms

Para utilizar WTForms en una aplicación Flask, es necesario instalar la extensión Flask-WTF, que integra WTForms de forma nativa con Flask. En el entorno virtual de Python, ejecutar:

pip install flask-wtf

Con Flask-WTF instalado, es crucial configurar la aplicación Flask para que pueda manejar formularios de manera segura. Esto implica establecer una clave secreta (SECRET_KEY) que se utiliza para asegurar los datos de los formularios y habilitar la protección contra ataques CSRF (Cross-Site Request Forgery).

En el archivo principal de la aplicación:

from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = 'una_clave_secreta_segura'

Es importante que la clave secreta sea difícil de adivinar y se mantenga confidencial. Puede generarse utilizando herramientas como secrets.token_hex() en Python:

import secrets

app.config['SECRET_KEY'] = secrets.token_hex(16)

Esta configuración inicial permite que Flask-WTF habilite la protección CSRF de forma automática en los formularios. La protección CSRF es esencial para prevenir que actores maliciosos realicen solicitudes no autorizadas en nombre del usuario.

Con la instalación y configuración básicas completadas, la aplicación Flask está lista para utilizar WTForms en la creación y gestión de formularios robustos y seguros.

Creación de clases de formulario y validadores

Para manejar formularios de manera eficiente en Flask con WTForms, es fundamental crear clases de formulario que representen la estructura y lógica de los datos que se desean recopilar. Estas clases se definen heredando de FlaskForm, que es una extensión específica para Flask proporcionada por Flask-WTF.

Primero, se debe importar FlaskForm y los campos y validadores necesarios:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length

A continuación, se define una clase que representa el formulario. Por ejemplo, un formulario de registro podría ser:

class RegistroForm(FlaskForm):
    nombre = StringField('Nombre completo', validators=[DataRequired(), Length(min=2, max=50)])
    email = StringField('Correo electrónico', validators=[DataRequired(), Email()])
    contraseña = PasswordField('Contraseña', validators=[DataRequired(), Length(min=8)])
    enviar = SubmitField('Registrarse')

En este ejemplo, cada atributo de la clase es un campo del formulario. Los validadores asociados a cada campo aseguran que los datos proporcionados cumplen ciertos criterios:

  • DataRequired(): verifica que el campo no esté vacío.
  • Email(): comprueba que el formato del correo electrónico sea válido.
  • Length(min, max): asegura que la longitud del input esté dentro del rango especificado.

Es posible combinar múltiples validadores en un solo campo para aplicar varias restricciones. Por ejemplo, en el campo nombre, se verifica que el usuario haya ingresado un nombre y que su longitud esté entre 2 y 50 caracteres.

Además de los campos de entrada, se incluye un SubmitField para el botón de envío del formulario. Este campo no requiere validadores, pero es esencial para que el formulario pueda ser enviado desde la interfaz de usuario.

Si se necesitan campos personalizados o validaciones más complejas, se pueden definir métodos de validación dentro de la clase del formulario. Estos métodos deben seguir la nomenclatura validate_<nombre_campo>:

from wtforms.validators import ValidationError

class RegistroForm(FlaskForm):
    nombre = StringField('Nombre completo', validators=[DataRequired(), Length(min=2, max=50)])
    email = StringField('Correo electrónico', validators=[DataRequired(), Email()])
    contraseña = PasswordField('Contraseña', validators=[DataRequired(), Length(min=8)])
    enviar = SubmitField('Registrarse')

    def validate_contraseña(self, field):
        if field.data.islower() or field.data.isupper():
            raise ValidationError('La contraseña debe contener letras mayúsculas y minúsculas.')
        if not any(char.isdigit() for char in field.data):
            raise ValidationError('La contraseña debe incluir al menos un número.')

En este caso, se agregan validaciones adicionales al campo contraseña para asegurarse de que cumple con ciertas reglas de seguridad. Si alguna validación falla, se lanza una excepción ValidationError con un mensaje específico.

Es esencial utilizar validadores personalizados para implementar lógica de negocio específica que no está cubierta por los validadores predeterminados de WTForms. Esto garantiza que los datos ingresados por los usuarios sean consistentes y seguros.

Para utilizar el formulario en una ruta de Flask, se instancia la clase y se pasa al contexto de la plantilla:

from flask import render_template, redirect, url_for, flash

@app.route('/registro', methods=['GET', 'POST'])
def registro():
    form = RegistroForm()
    if form.validate_on_submit():
        # Lógica para crear un nuevo usuario
        flash('¡Registro exitoso!', 'success')
        return redirect(url_for('inicio'))
    return render_template('registro.html', form=form)

Aquí, validate_on_submit() combina is_submitted() y validate(), verificando que se haya enviado una solicitud POST y que los datos cumplen con todas las validaciones establecidas.

En la plantilla registro.html, se pueden renderizar los campos del formulario utilizando las etiquetas de WTForms y Jinja2:

<form method="post">
    {{ form.hidden_tag() }}
    <div>
        {{ form.nombre.label }}<br>
        {{ form.nombre(size=32) }}<br>
        {% for error in form.nombre.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}
    </div>
    <div>
        {{ form.email.label }}<br>
        {{ form.email(size=32) }}<br>
        {% for error in form.email.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}
    </div>
    <div>
        {{ form.contraseña.label }}<br>
        {{ form.contraseña(size=32) }}<br>
        {% for error in form.contraseña.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}
    </div>
    <div>
        {{ form.enviar() }}
    </div>
</form>

La función hidden_tag() se utiliza para incluir campos ocultos necesarios para la protección CSRF. Los errores de validación se muestran junto a cada campo, permitiendo al usuario corregirlos inmediatamente.

Renderización de campos y manejar y mostrar errores

Una vez definidos los formularios con WTForms, es esencial renderizar sus campos en las plantillas Jinja2 y manejar adecuadamente los errores de validación para proporcionar feedback al usuario.

Renderización de campos en plantillas

Para mostrar un formulario en una plantilla, se pasa la instancia del formulario al contexto de la plantilla desde la vista:

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # Lógica de autenticación
        return redirect(url_for('dashboard'))
    return render_template('login.html', form=form)

En la plantilla login.html, se puede renderizar el formulario utilizando la sintaxis de Jinja2:

<form method="post">
    {{ form.hidden_tag() }}
    <div class="form-group">
        {{ form.username.label }}
        {{ form.username(class="form-control") }}
    </div>
    <div class="form-group">
        {{ form.password.label }}
        {{ form.password(class="form-control") }}
    </div>
    <div class="form-group">
        {{ form.submit(class="btn btn-primary") }}
    </div>
</form>

Aquí, cada campo del formulario se representa como form.nombre_campo, permitiendo acceder tanto al campo como a su etiqueta. Es recomendable utilizar las clases de Bootstrap 5 para mejorar la apariencia de los formularios.

Manejo y muestra de errores de validación

Cuando un usuario envía un formulario con datos inválidos, es importante mostrar los errores de validación junto a los campos correspondientes. Cada campo tiene un atributo errors que es una lista de mensajes de error.

Para mostrar los errores en la plantilla, se puede iterar sobre form.campo.errors:

<div class="form-group">
    {{ form.username.label }}
    {{ form.username(class="form-control") }}
    {% if form.username.errors %}
        <div class="alert alert-danger">
            {% for error in form.username.errors %}
                <span>{{ error }}</span>
            {% endfor %}
        </div>
    {% endif %}
</div>

Este bloque verifica si hay errores en el campo username y, si los hay, los muestra dentro de un div con clases de Bootstrap para resaltarlos visualmente.

Personalización de mensajes de error

Los validadores de WTForms permiten especificar mensajes de error personalizados. Al instanciar un validador, se puede pasar el parámetro message:

from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Nombre de usuario', validators=[DataRequired(message='El nombre de usuario es obligatorio.')])
    password = PasswordField('Contraseña', validators=[DataRequired(message='La contraseña es obligatoria.')])
    submit = SubmitField('Iniciar sesión')

De esta forma, si el campo username está vacío, se mostrará el mensaje específico definido, proporcionando una experiencia de usuario más clara.

Visualización de errores globales

Además de los errores asociados a campos individuales, es posible que se generen errores globales que no están vinculados a un campo específico, como errores de autenticación. Estos pueden agregarse a form.errors en la vista:

if form.validate_on_submit():
    user = User.query.filter_by(username=form.username.data).first()
    if user is None or not user.check_password(form.password.data):
        form.username.errors.append('Nombre de usuario o contraseña incorrectos.')
    else:
        # Inicio de sesión exitoso
        login_user(user)
        return redirect(url_for('dashboard'))

En la plantilla, se pueden mostrar estos errores generales:

{% if form.errors %}
    <div class="alert alert-danger">
        {% for field_errors in form.errors.values() %}
            {% for error in field_errors %}
                <p>{{ error }}</p>
            {% endfor %}
        {% endfor %}
    </div>
{% endif %}

Esto permite al usuario comprender que ocurrió un error que no está relacionado directamente con un campo específico.

Uso de macros para formularios

Para evitar repetir código al renderizar campos y errores, se recomienda utilizar macros de Jinja2. Se puede crear un archivo macros.html con una macro para renderizar campos:

{% macro render_field(field) %}
<div class="form-group">
    {{ field.label }}
    {{ field(class="form-control") }}
    {% if field.errors %}
        <div class="alert alert-danger">
            {% for error in field.errors %}
                <span>{{ error }}</span>
            {% endfor %}
        </div>
    {% endif %}
</div>
{% endmacro %}

En la plantilla, se incluye el archivo de macros y se utiliza la macro:

{% from "macros.html" import render_field %}

<form method="post">
    {{ form.hidden_tag() }}
    {{ render_field(form.username) }}
    {{ render_field(form.password) }}
    {{ form.submit(class="btn btn-primary") }}
</form>

El uso de macros mejora la legibilidad y mantenibilidad del código, facilitando la modificación de la apariencia de los campos en un único lugar.

Protección contra ataques CSRF

Es fundamental incluir {{ form.hidden_tag() }} dentro del formulario para asegurar la protección contra ataques CSRF. Este método agrega un campo oculto con un token de seguridad que WTForms verifica al procesar el formulario.

Depuración de formularios

Si surgen problemas al manejar formularios, se puede imprimir form.errors en la consola o en la plantilla para identificar los campos con errores y los mensajes asociados. Esto facilita la depuración y solución de problemas comunes.

Limpieza del formulario

La limpieza del formulario en WTForms es un proceso fundamental para asegurar que los datos ingresados por el usuario sean consistentes y aptos para su procesamiento posterior. Además de las validaciones estándar, es posible implementar métodos de limpieza para normalizar y transformar los datos antes de que sean utilizados en la aplicación.

Uso de filtros en campos

WTForms permite aplicar filtros a los campos del formulario mediante el parámetro filters en la definición del campo. Los filtros son funciones que se ejecutan antes de la validación y se utilizan para modificar el valor ingresado por el usuario.

Por ejemplo, para eliminar espacios en blanco al inicio y al final de una cadena:

class BuscarForm(FlaskForm):
    consulta = StringField('Buscar', filters=[lambda x: x.strip() if x else None])
    enviar = SubmitField('Buscar')

En este caso, el filtro aplicado al campo consulta utiliza una función lambda que elimina los espacios en blanco utilizando strip(). Esto asegura que el valor procesado esté limpio antes de la validación.

Métodos validate_ para campos individuales

Para realizar una limpieza personalizada en campos específicos, se pueden definir métodos con el prefijo validate_ seguido del nombre del campo. Estos métodos permiten acceder y modificar el valor del campo antes de que se realice la validación completa del formulario.

Por ejemplo, para convertir el contenido de un campo a minúsculas:

class UsuarioForm(FlaskForm):
    username = StringField('Nombre de usuario', validators=[DataRequired(), Length(min=4, max=25)])
    enviar = SubmitField('Enviar')
    
    def validate_username(self, field):
        field.data = field.data.lower()

En este ejemplo, el método validate_username convierte el valor de username a minúsculas, garantizando una consistencia en los datos almacenados.

Método validate del formulario

Además de los métodos de validación por campo, se puede sobreescribir el método validate del formulario para aplicar reglas de limpieza y validación que involucran múltiples campos. Este método se llama después de validar todos los campos individualmente.

Por ejemplo, para asegurar que dos campos de contraseña coinciden:

class RegistroForm(FlaskForm):
    contraseña = PasswordField('Contraseña', validators=[DataRequired()])
    confirmar_contraseña = PasswordField('Confirmar Contraseña', validators=[DataRequired()])
    enviar = SubmitField('Registrarse')
    
    def validate(self):
        if not super().validate():
            return False
        if self.contraseña.data != self.confirmar_contraseña.data:
            self.confirmar_contraseña.errors.append('Las contraseñas no coinciden.')
            return False
        return True

Este enfoque permite manejar validaciones que dependen de múltiples campos y agregar errores personalizados al formulario.

Métodos pre_validate y post_validate

WTForms proporciona los métodos pre_validate y post_validate que se pueden utilizar para realizar tareas específicas antes y después de la validación del formulario.

El método pre_validate se ejecuta antes de la validación de los campos individuales:

class ContactoForm(FlaskForm):
    email = StringField('Correo electrónico', validators=[DataRequired(), Email()])
    mensaje = TextAreaField('Mensaje', validators=[DataRequired()])
    enviar = SubmitField('Enviar')
    
    def pre_validate(self, form):
        if 'spam' in self.mensaje.data.lower():
            self.mensaje.errors.append('El mensaje contiene contenido no permitido.')
            raise ValidationError('Contenido no permitido.')

El método post_validate se ejecuta después de la validación de los campos:

    def post_validate(self, form, validation_stopped):
        if not self.email.data.endswith('@empresa.com'):
            self.email.errors.append('Debe utilizar un correo de @empresa.com')
            raise ValidationError('Correo no autorizado.')

Estos métodos ofrecen mayor control sobre la validación y limpieza del formulario, permitiendo implementar lógica compleja según los requerimientos de la aplicación.

Transformación y normalización de datos

Es común requerir que los datos ingresados por el usuario sean transformados o normalizados para cumplir con ciertos estándares. Por ejemplo, para formatear un número de teléfono:

import re

class TelefonoForm(FlaskForm):
    telefono = StringField('Número de teléfono', validators=[DataRequired()])
    enviar = SubmitField('Guardar')

    def validate_telefono(self, field):
        numero = re.sub(r'\D', '', field.data)
        if len(numero) != 9:
            raise ValidationError('El número debe tener 9 dígitos.')
        field.data = f'{numero[:3]} {numero[3:6]} {numero[6:]}'

En este ejemplo, se eliminan todos los caracteres que no son dígitos y se verifica que el número tiene 9 dígitos. Luego, se formatea para agregar espacios, mejorando la legibilidad del número almacenado.

Gestión de valores nulos y vacíos

Es importante manejar correctamente los casos en que el usuario no proporciona un valor para un campo opcional. Los filtros y métodos de validación deben contemplar la posibilidad de que el valor sea None o una cadena vacía.

Por ejemplo:

from wtforms.validators import Optional

class PerfilForm(FlaskForm):
    sitio_web = StringField('Sitio web', validators=[Optional(), URL()])
    enviar = SubmitField('Actualizar')

    def validate_sitio_web(self, field):
        if field.data:
            field.data = field.data.strip()

Aquí, el validador Optional() permite que el campo sitio_web sea opcional. En el método de validación, se comprueba si field.data tiene un valor antes de aplicar strip(), evitando errores al llamar métodos sobre None.

Uso de InputRequired vs DataRequired

Es importante distinguir entre los validadores **InputRequired** y **DataRequired**. DataRequired falla la validación si el campo tiene un valor "falso" en términos de Python (como una cadena vacía), mientras que InputRequired falla si no se proporciona ningún valor en absoluto.

Utilizar el validador adecuado es parte de la limpieza del formulario, asegurando que los campos obligatorios contienen datos significativos.

Consideraciones de seguridad

Al limpiar datos ingresados por el usuario, es vital considerar cuestiones de seguridad, como prevenir la inyección de código o la ejecución de scripts maliciosos. Siempre se debe validar y sanear los datos antes de utilizarlos en operaciones sensibles, como consultas a la base de datos.

Por ejemplo, al trabajar con HTML en campos de texto:

from markupsafe import escape

class ComentarioForm(FlaskForm):
    comentario = TextAreaField('Comentario', validators=[DataRequired()])
    enviar = SubmitField('Publicar')

    def validate_comentario(self, field):
        field.data = escape(field.data)

La función escape de Markupsafe asegura que cualquier código HTML ingresado por el usuario se convierta en entidades de texto, evitando ataques de Cross-Site Scripting (XSS).

Integración con el modelo de datos

La limpieza del formulario también implica garantizar que los datos sean compatibles con el modelo de datos de la aplicación. Esto puede incluir convertir tipos, ajustar formatos y comprobar restricciones de unicidad.

Por ejemplo, antes de guardar un nuevo usuario:

class RegistroForm(FlaskForm):
    email = StringField('Correo electrónico', validators=[DataRequired(), Email()])
    # otros campos
    enviar = SubmitField('Registrarse')

    def validate_email(self, field):
        if Usuario.query.filter_by(email=field.data.lower()).first():
            raise ValidationError('El correo electrónico ya está registrado.')
        field.data = field.data.lower()

Aquí se verifica que el correo electrónico no esté ya en uso y se convierte a minúsculas para mantener la integridad de los datos.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

20 % DE DESCUENTO

Plan mensual

19.00 /mes

15.20 € /mes

Precio normal mensual: 19 €
58 % DE DESCUENTO

Plan anual

10.00 /mes

8.00 € /mes

Ahorras 132 € al año
Precio normal anual: 120 €
Aprende Flask GRATIS online

Todas las lecciones de Flask

Accede a todas las lecciones de Flask y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Flask y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Instalar Flask-WTF y configurar su entorno.
  • Definir claves secretas y activar protección CSRF.
  • Crear clases de formulario y aplicar validadores.
  • Renderizar campos y manejar errores de validación.
  • Aplicar limpieza y filtrado de datos en formularios.
  • Implementar validaciones personalizadas de campos.
  • Utilizar macros en Jinja2 para optimizar formularios.