Validación de formulario de matraz con Flask-WTF

WTForms es una biblioteca popular de Python que valida datos de formularios. Este tutorial le muestra cómo usar WTForms con Flask para crear y verificar formularios en su aplicación.

Introducción

La validación de formularios es uno de los componentes más esenciales de la entrada de datos en las aplicaciones web. Los usuarios pueden cometer errores, algunos usuarios son maliciosos. Con la validación de entrada, protegemos nuestra aplicación de datos incorrectos que afectan la lógica comercial y la entrada maliciosa destinada a dañar nuestros sistemas.

Intentar procesar entradas de usuario no validadas puede causar errores inesperados o no controlados, si no un bloqueo del servidor. En este contexto, validar datos significa verificar la entrada y verificar si cumple con ciertas expectativas o criterios. La validación de datos se puede realizar tanto en la parte delantera como en la trasera.

En este tutorial, aprenderemos cómo validar la entrada del usuario en formularios Flask usando la extensión Flask-WTForms.

Al finalizar este tutorial tendremos el siguiente formulario de registro de usuario con criterios de validación:

Formulario de registro con validación

Usaremos Flask versión 1.1.2 y Flask-WTF con versión 0.14.3.

Configuración

Si bien no es necesario, le recomendamos que cree un entorno virtual para seguir:

1
2
3
4
$ mkdir flask-form-validation
$ cd flask-form-validation
$ python3 -m venv .
$ . bin/activate

En su entorno virtual activado, instalaremos nuestros paquetes escribiendo:

1
$ pip install Flask Flask-WTF

Tenga en cuenta que si desea utilizar la validación de correo electrónico, también deberá instalar el paquete email_validator (la versión actual es 1.1.1):

1
$ pip3 install email_validator

Ahora vamos a crear nuestros archivos necesarios. Comenzaremos creando un app.py básico que, para simplificar, contendrá nuestra aplicación Flask, rutas y formularios:

1
2
3
4
from flask import Flask, render_template

app = Flask(__name__, template_folder='.')
app.config['SECRET_KEY']='LongAndRandomSecretKey'

Creamos un objeto Flask y configuramos template_folder en la carpeta actual. Luego asignamos el objeto Flask ​​a la variable app. Agregamos SECRET_KEY a la configuración de nuestro objeto app.

La SECRET_KEY se usa comúnmente para el cifrado con conexiones de base de datos y sesiones de navegador. WTForms usará SECRET_KEY como sal para crear un token CSRF. Puede leer más sobre CSRF en esta página wiki.

Si su aplicación ya usa la configuración SECRET_KEY para otros fines, querrá crear una nueva para WTForms. En ese caso, puede establecer la configuración WTF_CSRF_SECRET_KEY.

Vamos a crear y agregar un formulario a nuestro app.py actual:

1
2
3
4
5
6
7
8
9
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'))
    submit = SubmitField(label=('Submit'))

# ...

Nuestra clase simple GreetUserForm contiene un StringField. Como su nombre lo indica, este campo espera y devolverá un valor de cadena (siempre puede convertir esa entrada a otros tipos de datos según sea necesario). El nombre del campo es nombre de usuario, y usaremos este nombre para acceder a los datos del elemento de formulario.

Los parámetros de etiqueta son los que se representarán en nuestra página para que los usuarios entiendan qué datos captura un elemento de formulario. También tenemos un botón enviar, que intentará enviar el formulario si todos los campos pasan nuestros criterios de validación.

Ahora que estamos configurados, ¡utilicemos WTForms para validar nuestros datos!

Validación de formulario de matraz con Flask-WTForms

Comencemos por crear una ruta para mostrar y procesar nuestro formulario:

1
2
3
4
5
6
7
8
# ...

@app.route('/', methods=('GET', 'POST'))
def index():
    form = GreetUserForm()
    if form.validate_on_submit():
        return f'''<h1> Welcome {form.username.data} </h1>'''
    return render_template('index.html', form=form)

Nuestra ruta tiene métodos GET y POST. El método GET muestra el formulario, mientras que el método POST procesa los datos del formulario al enviarlo. Establecemos la ruta de la URL en /, o la URL raíz, para que aparezca como la página de inicio de nuestra aplicación web. Representamos la plantilla index.html y pasamos el objeto form como parámetro.

Hagamos una pausa y prestemos mucha atención a esta línea: if form.validate_on_submit():. Esta regla dice 'si el método de solicitud es POST y si los campos del formulario son válidos, entonces continúe. Si la entrada de nuestro formulario pasa nuestros criterios de validación, en la página siguiente se mostrará un mensaje de bienvenida simple con el nombre del usuario. Observe que aquí usamos el nombre de campo (nombre de usuario) para acceder a los datos de entrada.

Para ver el formulario, necesitamos crear la plantilla index.html. Cree el archivo y agréguele el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<form method="POST" action="">
    <div class="form-row">
        <div class="form-group col-md-6">
            {{ form.csrf_token() }}
            <label for=""> {{ form.username.label }}</label>
            {{ form.username }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </div>
</form>

Usamos nuestro objeto form para pasar elementos WTform a Jinja2, el analizador de plantillas para Flask.

Nota: WTForms genera automáticamente csrf_token y cambia cada vez que se representa la página. Esto nos ayuda a proteger nuestro sitio contra ataques CSRF. Por defecto, es un campo oculto. También puede optar por usar {{ form.hidden_field() }} para representar todos los campos ocultos, incluido el token CSRF, pero eso no se recomienda.

Ahora, vayamos a nuestra terminal para iniciar nuestra aplicación Flask escribiendo:

1
$ FLASK_ENV=development flask run

Para mayor comodidad, configuramos la variable de entorno FLASK_ENV en 'desarrollo' durante el desarrollo. Esto permite que la aplicación se vuelva a cargar cada vez que presionamos guardar. Para Windows, es posible que deba usar set FLASK_ENV=desarrollo en su terminal/consola antes de ejecutar su aplicación Flask.

Esto es lo que veremos si navegamos al servidor local:

Ejecutando la aplicación Flask con formulario

Escriba un nombre en el campo de entrada y envíe el formulario. Verás el mensaje de saludo que definimos en nuestra ruta:

Envío exitoso del formulario con nombre de usuario

Funciona como se esperaba. Pero, ¿y si no escribimos nada en el campo de entrada? Todavía validaría el formulario:

Envío exitoso de formulario sin nombre

Evitemos que eso suceda y solo permitamos que los usuarios que escribieron sus nombres vean la página siguiente. Para hacerlo, debemos asegurarnos de que nuestro campo nombre de usuario tenga datos de entrada.

Importaremos uno de los métodos de validación integrados de WTForms: DataRequired() desde wtforms.validators y lo pasaremos a nuestro campo username.

1
2
3
4
5
6
7
8
9
# ...
from wtforms.validators import ValidationError, DataRequired

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
                           validators=[DataRequired()])
    submit = SubmitField(label=('Submit'))

# ...

Note que estamos pasando el parámetro validators como una lista. Esto nos dice que podemos tener múltiples validadores para cada campo.

Ahora que estamos usando DataRequired(), el campo username no se validará si no hay datos de entrada:

Información sobre herramientas que se muestra al usuario cuando los datos del formulario están en blanco

De hecho, si hacemos clic derecho e inspeccionamos el elemento del formulario, veremos que WTForms agregó automáticamente el atributo requerido al campo de entrada:

Inspeccionando el HTML del formulario para ver el atributo \"requerido\" del campo de entrada

Al hacerlo, WTForms agrega una validación de front-end básica a nuestro campo de formulario. No podría enviar ese formulario sin el campo nombre de usuario incluso si intenta publicar el formulario utilizando herramientas como cURL o Postman.

Ahora, digamos que queremos establecer una nueva regla de validación que solo permita nombres que tengan al menos 5 caracteres. Podemos usar el validador Length() con el parámetro min:

1
2
3
4
5
6
7
8
9
# ...
from wtforms.validators import ValidationError, DataRequired, Length

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'), 
        validators=[DataRequired(), Length(min=5)])
    submit = SubmitField(label=('Submit'))

# ...

Si intentamos enviar el formulario con datos de entrada de menos de 5 caracteres, no se cumplirán los criterios de validación y el envío fallará:

fallo de validación de wtforms

Hacer clic en el botón Enviar no hace nada por los datos no válidos, tampoco muestra ningún error al usuario. Necesitamos proporcionar mensajes de error para que el usuario entienda qué está pasando y cómo solucionarlo.

En nuestra plantilla index.html, justo debajo de {{ form.username }}, agregue el siguiente ciclo for de Jinja2 para mostrar los errores:

1
2
3
4
5
 {% for field, errors in form.errors.items() %}
    <small class="form-text text-muted ">
        {{ ', '.join(errors) }}
    </small>
{% endfor %}

Nuestro formulario puede generar errores de validación limpios ahora:

Errores de representación de formulario para un nombre de usuario corto

Por alguna razón, si necesitamos limitar la longitud máxima de nuestros datos de campo, podemos hacerlo pasando el parámetro max al validador Length(). También es posible personalizar el mensaje de error pasando un parámetro opcional message con una cadena de error personalizada.

Actualicemos el campo nombre de usuario en consecuencia:

1
2
3
4
5
6
7
8
9
# ...

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
        validators=[DataRequired(), 
        Length(min=5, max=64, message='Name length must be between %(min)d and %(max)dcharacters') ])
    submit = SubmitField(label=('Submit'))

# ...

Errores de representación de formulario para un nombre de usuario corto con un mensaje personalizado

Más campos de WTForms y validadores con el formulario de registro de usuario

Nuestro formulario actual tiene un solo campo, que es un poco aburrido. WTForms proporciona amplios criterios de validación de formularios y una variedad de campos de formulario, así que aprovechémoslo y creemos algo con un uso práctico.

Crearemos un formulario de registro de usuario y utilizaremos validadores integrados de WTForms.

Usaremos el validador DataRequired() para los campos que queremos asegurarnos de que el usuario llene. Verificaremos la longitud mínima y máxima de los campos con el validador Length(), validaremos los correos electrónicos con Email() validador y verifique si dos campos contienen los mismos datos con el validador EqualTo().

Elimina la clase GreetUserForm y reemplaza el comienzo de tu código con nuestro nuevo formulario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, \
    SubmitField
from wtforms.validators import ValidationError, DataRequired, \
    Email, EqualTo, Length

class CreateUserForm(FlaskForm):
    username = StringField(label=('Username'), 
        validators=[DataRequired(), 
        Length(max=64)])
    email = StringField(label=('Email'), 
        validators=[DataRequired(), 
        Email(), 
        Length(max=120)])
    password = PasswordField(label=('Password'), 
        validators=[DataRequired(), 
        Length(min=8, message='Password should be at least %(min)d characters long')])
    confirm_password = PasswordField(
        label=('Confirm Password'), 
        validators=[DataRequired(message='*Required'),
        EqualTo('password', message='Both password fields must be equal!')])

    receive_emails = BooleanField(label=('Receive merketting emails.'))

    submit = SubmitField(label=('Submit'))

# ...    

Tenemos cuatro campos diferentes en nuestros formularios. El último es un botón de envío normal. Usamos StringField para obtener entradas de cadena de los usuarios, como nombre de usuario y correo electrónico. Por otro lado, PasswordField oculta el texto de la contraseña en el front-end. BooleanField se muestra como una casilla de verificación en el front-end, ya que solo contiene valores verdaderos (marcados) o falsos (no marcados).

Necesitamos modificar la plantilla index.html para representar nuestros nuevos campos de formulario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div class="container">
    <h2>Registration Form</h2>
    {% for field, errors in form.errors.items() %}
    {{ ', '.join(errors) }}
    {% endfor %}
    <form class="form-horizontal" method="POST" action="">
        {{ form.csrf_token() }}
        <div class="form-group">
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.confirm_password.label }}
            {{ form.confirm_password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.receive_emails.label }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </form>
</div>

Nuestros campos de formulario se representan correctamente como puede ver:

El formulario de registro de usuario se renderiza correctamente

Nota: Si su sitio web va a tener varios formularios diferentes, es posible que desee utilizar las macros de Jinja2 en lugar de escribir cada campo de formulario uno por uno. El uso de macros está más allá del alcance de este artículo, pero acelera enormemente los procesos de creación de formularios.

Crear sus propios validadores personalizados

En la mayoría de los sitios web, ciertos caracteres no están permitidos en los nombres de usuario. Puede ser por motivos de seguridad, puede ser para cosméticos. WTForms no tiene esa lógica por defecto pero podemos definirla nosotros mismos.

WTForms nos permite agregar validadores personalizados agregando un método de validación a nuestra clase UserRegistrationForm. Implementemos esa validación personalizada en nuestro formulario agregando el método validate_username() justo debajo del botón enviar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ...

class UserRegistrationForm(FlaskForm):
    # ...
    submit = SubmitField(label=('Submit'))

    def validate_username(self, username):
        excluded_chars = " *?!'^+%&/()=}][{$#"
        for char in self.username.data:
            if char in excluded_chars:
                raise ValidationError(
                    f"Character {char} is not allowed in username.")
                
# ...

Podemos agregar tantos o tan pocos métodos de validación como queramos. WTForms ejecutará métodos de validación automáticamente una vez definidos.

La clase ValidationError nos brinda una forma conveniente de definir nuestro mensaje de validación personalizado. Tenga en cuenta que deberá importarlo desde wtforms.validators antes de usarlo.

Probemos este nuevo método ingresando los datos adecuados en todos los campos excepto en el campo nombre de usuario, que contendrá un carácter excluido - '%'.

No se pudo validar el formulario de usuario debido a nuestra validación personalizada

Como puede ver, nuestro método de validación personalizado funciona perfectamente y nos brinda un error de validación limpio, lo que nos ayuda a comprender qué es lo que está mal con nuestros datos de entrada. Si lo hace, mejora en gran medida la experiencia del usuario.

Puede usar bibliotecas externas, su base de datos o API para combinar con WTForms y validar los datos de entrada entrantes. Cuando desee capturar {{ form.some_field.data }} y escribir o consultar desde la base de datos, use validadores de WTForms para asegurarse de que sea seguro guardarlo.

Nota: Hemos excluido la mayoría de los códigos HTML ya que no están directamente relacionados con nuestro tutorial. El código completo estará disponible en este repositorio de GitHub, por si quieres consultar.

Conclusión

La validación de datos es una de las partes más esenciales de las aplicaciones web de Flask. Flask-WTforms proporciona formas muy poderosas y fáciles de aprender para manejar datos de formularios.

Ahora que conoce los fundamentos de la validación de datos con Flask-WTF, puede continuar y aplicar su propia lógica de validación y/o implementar sus propios métodos tanto para la seguridad como para una mejor experiencia del usuario. io.