Hace una semana publiqué, en la web de adictos, una guía completa con las bases para construir un sitio web con Flask.

Lo último que hicimos en aquel proyecto guiado fue la introducción de datos a través de formularios. Lo hicimos de forma nativa, aplicando solamente algunos estilos de bootstrap. Vamos a continuar con el mismo repositorio de github como proyecto guiado.

Hoy vamos a aplicar un módulo extra de Flask: WTF.

Esta guía no prentende ser una simple reproducción de ejemplos a modo de documentación. Encontrarás diferentes formas de hacer las cosas y recomendaciones para poder hacer el código con las mejores prácitcas posibles.

Introducción a WTF

WTF (WTForms) es un módulo de Python ampliamente utilizado para la creación y validación de formularios web en aplicaciones Flask. La abreviatura «WTF» en realidad significa «WT Forms» (donde «WT» proviene de «Werkzeug», la librería en la que se basa Flask). Este módulo proporciona (o más bien tiene el objetivo de proporcionar) una forma sencilla y eficiente de definir y manejar formularios en aplicaciones Flask, facilitando la interacción con el usuario y la validación de datos entrantes.

He decidido traer esto como una guía aparte porque no viene de forma nativa con el framework. Por tanto, es evidente que lo primero que tenemos que hacer es instalar la dependencia en nuestro entorno (virtual o no):

pip install flask-wtf

Y no podemos olvidar añadirlo a nuestro requirements.txt para que ya estén ambos:

flask~=3.0.2
wtforms~=3.1.2

Las versiones irán cambiando según pase el tiempo, lo normal es que uses versiones más modernas según van saliendo.

¿En qué consiste WTF?

En esencia, WTF ofrece una manera conveniente de definir estructuras de formularios en Python utilizando clases. Estas clases representan los campos de entrada que se mostrarán en el formulario HTML. WTF también proporciona herramientas para validar los datos ingresados por el usuario y para generar automáticamente HTML para mostrar los formularios en las vistas.

Ventajas de usar WTF:

  1. Simplicidad y claridad: WTF simplifica enormemente la creación de formularios en Flask al permitirte definirlos en Python directamente. Lo veremos, por supuesto, a continuación.
  2. Validación de datos: WTF incluye un sistema de validación integrado que te permite definir reglas de validación para cada campo de formulario. Si, como yo, vienes de angular, es como pasar a potenciarte con formularios reactivos.
  3. Protección contra ataques: WTF también ofrece protección contra ataques comunes, como ataques de falsificación de solicitudes entre sitios (CSRF), lo que aumenta la seguridad de nuestra aplicación.
  4. Personalización: Aunque WTF proporciona funcionalidades predeterminadas para la creación de formularios, también es altamente personalizable. Puedes personalizar fácilmente la apariencia y el comportamiento de tus formularios según tus necesidades específicas. (Nosotros continuaremos utilizando bootstrap para este ejemplo)

Desventajas potenciales:

  1. Curva de aprendizaje inicial: obviamente resulta algo más difícil de entender que los formularios nativos. Ya aprendimos a hacerlos en la primera guía. Ahora, vamos a mejorarlo.
  2. Complejidad en formularios muy personalizados: Si tus formularios son extremadamente personalizados o tienen requisitos muy específicos, puede que te encuentres lidiando con cierta complejidad al trabajar con WTF. Sin embargo, en la mayoría de los casos, WTF es lo suficientemente flexible como para manejar una amplia gama de escenarios.

En resumen, WTF es una herramienta creada con intención de simplificar significativamente el proceso de creación y validación de formularios. Ofrece una combinación de simplicidad, flexibilidad y seguridad, convirtiéndolo así en una elección popular entre los desarrolladores.

Formulario básico

Empecemos preparando la página donde vamos a insertar nuestro formulario y el endpoint que la renderiza.

{% extends 'base.html' %}

{% block title %}Formulario con WTF{% endblock %}

{% block page_header %}
    <h1>DATOS DE USUARIO (formulario WTF)</h1>
{% endblock %}

{% block page_content %}
<h2>EXAMPLE CONTENT</h2>
{%  endblock %}
@app.route('/wtf')
def wtf_form():
    return render_template('wtf_form.html')

Creamos la clase que gestionará el formulario. Ya utilizamos WTF. El archivo lo llamaré wt_form.py:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField

class ExampleForm(FlaskForm):
    username = StringField('')
    password = PasswordField('')
    submit = SubmitField('')

Ahora, podemos hacer uso de nuestro formulario en el contenido de nuestro HTML:

{% block page_content %}
    <form action="" method="post">
        <p>
            {{ form.username.label }} <br>
            {{ form.username(size=10) }}
        </p>
        <p>
            {{ form.password.label }} <br>
            {{ form.password(size=10) }}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{%  endblock %}

Para que funcione adecuadamente, debemos importar nuestro formulario ExampleForm desde la clase python que lo renderiza y enviárselo como parámetro:

from wtf_form import ExampleForm

@app.route('/wtf')
def wtf_form():
    example_form = ExampleForm()
    return render_template('wtf_form.html', form=example_form)

Entendamos bien este código: estamos creando una instancia llamada example_form de la clase ExampleForm. Y estamos renderizando la plantilla pasándole nuestro example_form como valor del atributo form de la plantilla.

Venga, tómate un minutito para responder a una pregunta: ¿está todo listo para funcionar?

La respuesta es casi, pero no.

Levantamos nuestro servidor (flask run para los despistados). Navegamos hasta el endpoint /wtf y… ¿qué vemos? Un maravilloso internal server error.

Veamos el log:

Lo que necesitamos es una clave secreta para usar CSRF.

Esto es una de las cosas que os comentaba al principio. Por defecto, este módulo nos protege contra ciertos posibles usos malintencionados por parte de los usuarios. Este es uno de esos casos.

¿Cómo añadimos nuestra clave super secreta?

Tenemos que decirle a la instancia de flask que incluya esta variable. Añadimos lo siguiente donde creamos la instancia “app” de flask:

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

¡Listo! Funciona.

Se ve un poco feo (aún):

Cuando digo que funciona, me refiero a que las cosas se ven. Obviamente no tiene lógica.

De hecho, si hacemos click en el botón, nos devolverá un error porque lo hemos declarado para que envíe un post y no admitimos, todavía, una petición de este tipo en nuestro endpoint.

Actualizando un poco el código de nuestro endpoint:

@app.route('/wtf', methods=['GET', 'POST'])
def wtf_form():
    if request.method == 'POST':
        print(request.form.get('username'))
        print(request.form.get('password'))

    example_form = ExampleForm()
    return render_template('wtf_form.html', form=example_form)

Si escribimos “usuario” y “contraseña” en sus respectivos campos, veremos cómo la consola imprime sus valores al presionar el botón submit.

Como puedes ver, estamos accediendo a los valores del formulario con “request.form.get(’nombre_campo’)”.

Si no introducimos ningún dato, nuestra consola imprimirá valores vacíos:

Seguramente algunos valores de tu formulario serán obligatorios. Si quieres conseguirlo, tendrás que aplicar validaciones. Lo vemos más adelante.

Tipos de campo

En este caso sencillo estamos utilizando 3 tipos básicos: string, password y submit. Pero, por supuesto, existen más. Algunos ejemplos: BooleanField, DateField, DecimalField, IntegerField…

Puedes ver la lista completa en la documentación de WTF.

https://wtforms.readthedocs.io/en/2.3.x/fields/#basic-fields

Además, existen tipos de campo extras heredados de HTML5:

https://wtforms.readthedocs.io/en/2.3.x/fields/#module-wtforms.fields.html5

Para cada tipo de campo, podemos implementar múltiples validaciones, como veremos a continuación.

Validaciones

Una de las ventajas principales que obtenemos de trabajar con WTForms es la automatización de las validaciones. Sin implementamos formularios vanilla como vimos en la primera guía, ¿te imaginas lo que supondría llenar nuestro código de if – else para validar que los campos obligatorios están presentes y que los valores introducidos son correctos según las normas de validacón?

Vamos a actualizar nuestro input de username para añadir exigir que el campo sea obligatorio:

username = StringField('Nombre de usuario', validators=[DataRequired()])

Ahora, si no introducimos datos, el formulario no se envía:

Otros ejemplos de validadores que nos serán muy útiles:

  • Email: valida que el contenido del input sea un email
  • Ipaddress: valida que el formato corresponda con IPv4
  • NumberRange: rango específico de números
  • RegExp: expresión regular que podemos customizar a nuestro gusto para validar patrón de entrada
  • AnyOf/NoneOf: valida que el input incluye/no incluye alguno de los argumentos de la lista.

La lista completa de validadores la puedes encontrar en su documentación oficial:

https://wtforms.readthedocs.io/en/2.3.x/validators/

Lectura de datos

En el ejemplo actual estamos leyendo los datos que recibimos del formulario así:

if request.method == 'POST':
    request.form.get('username')

No es lo más ideal ya que, para que funcione bien, tendremos que wrapear siempre con el if method == post.

Otra forma que, tal vez, sería mas recomendada podría ser algo así:

@app.route('/wtf', methods=['GET', 'POST'])
def wtf_form():
    example_form = ExampleForm()
    if example_form.validate_on_submit():
        print(example_form.username.data)
        print(example_form.password.data)

    return render_template('wtf_form.html', form=example_form)

De esta forma, estamos verificando que ha sido submiteado y validado (en lugar de comprobar sólo que estamos recibiendo un post). Para que esto funcione bien, el propio form tiene un metodo isValid(), que podemos usar (ya lo estamos utilizando implícitamente al hacerlo así).

Además, recordemos la forma anterior en la que leíamos el contenido de cada campo del formulario:

request.form.get('username')

De esta manera, somos nosotros (los programadores) los que tenemos que recordar qué nombre exacto le hemos dado a cada campo del formulario. De la forma que lo hacemos en el último ejemplo:

example_form.username.data

Es mejor para que nuestro IDE nos autocomplete o recomiende código en base a lo que introducimos porque, al fin y al cabo “username” y “password” son variables de la clase ExampleForm().

Por eso te recomiendo que utilices esta forma de hacerlo.

Es probable que, si estás haciendo pruebas, veas que no funciona, ya que no está mostrando en pantalla el valor de los inputs. Para saber qué errores pueden estar ocurriendo en la validación, vamos a añadir un else a nuestro código:

@app.route('/wtf', methods=['GET', 'POST'])
def wtf_form():
    example_form = ExampleForm()
    if example_form.validate_on_submit():
        print("Validated")
        print(example_form.username.data)
        print(example_form.password.data)
    else:
        print("not validated")
        for field, errors in example_form.errors.items():
            for error in errors:
                print(f"{field}: {error}")

    return render_template('wtf_form.html', form=example_form)

Con este código, tras insertar valores en ambos inputs y enviar los datos, obtenemos lo siguiente:

El token CSRF (que hemos configurado en nuestra app.config al inicio del archivo) no está siendo enviado correctamente al formulario. Esto lo hacemos modificando el HTML para incluir el token asociado al formulario:

<form action="" method="post">
        {{ form.csrf_token }}
        <p>
            {{ form.username.label }} <br>
            {{ form.username(size=20) }}
        </p>
        <p>
            {{ form.password.label }} <br>
            {{ form.password(size=20) }}
        </p>
        <p>{{ form.submit() }}</p>
    </form>

Ahora sí obtenemos el resultado esperado:

Redirección

El código que hemos empleado funciona pero tiene un pequeño inconveniente que a la gente no le suele gustar mucho.

Por norma general, los navegadores guardan la última petición realizada y es la que se realiza si refrescas la página.

Por tanto, si enviamos datos y recargamos, la última petición enviada será el post. El navegador nos preguntará “¿deseas reenviar el formulario?”. Esto es porque entiende que reenviar un post con información puede no ser adecuado, y se asegura de que el usuario sepa lo que está haciendo.

Para resolver este sistema basta con añadir una redirección con url_for a la misma página en la que estamos:

@app.route('/wtf', methods=['GET', 'POST'])
def wtf_form():
    example_form = ExampleForm()
    if example_form.validate_on_submit():
        print("Validated")
        print(example_form.username.data)
        print(example_form.password.data)
        return redirect(url_for('wtf_form'))
    else:
        print("not validated")
        for field, errors in example_form.errors.items():
            for error in errors:
                print(f"{field}: {error}")

    return render_template('wtf_form.html', form=example_form)

De esta forma, cuando el usuario envía datos, volvemos a hacer un get a la página con el formulario vacío, permitiendo así que, al recargar la página, enviamos una petición get sin cuerpo en lugar de un post con los mismos datos que habíamos introducido repetidos.

Seguramente, en el mundo real, tras hacer la redirección nos interesa que los datos sigan reflejándose en la pantlla. Esto lo conseguimos con el almacenamiento de datos en la sesión del usuario. Lo dejaremos pendiente para una próxima guía.