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ícateInstalació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.
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.
Introducción A Flask
Introducción Y Entorno
Instalación Y Configuración Flask Con Venv
Introducción Y Entorno
Mysql Con Sqlalchemy En Flask
Modelos Y Migraciones
Tipos De Datos En Modelos
Modelos Y Migraciones
Operaciones Crud Y Consultas
Modelos Y Migraciones
Asociaciones De Modelos
Modelos Y Migraciones
Migraciones Con Flask-migrate
Modelos Y Migraciones
Rutas Endpoints Rest Get
Api Rest
Respuestas Con Esquemas Flask Marshmallow
Api Rest
Rutas Endpoints Rest Post, Put Y Delete
Api Rest
Manejo De Errores Y Códigos De Estado Http
Api Rest
Autenticación Jwt Con Flask-jwt-extended
Api Rest
Controlador Mvc Con Métodos Get En Flask
Mvc
Sintaxis De Plantillas Jinja 2 En Flask
Mvc
Controlador Mvc Con Métodos Post En Flask
Mvc
Inclusión De Archivos Estáticos En Jinja
Mvc
Validación De Formularios Con Wtforms
Mvc
Subir Archivos En Formularios Jinja En Flask
Mvc
Autenticación Con Flask-login
Mvc
Autorización Con Flask-principal
Mvc
Qué Son Los Blueprints Y Cómo Crear Uno
Blueprints
Integrar Openai Api En Flask Api Rest
Aplicación Con Ia
Sqlalchemy Orm En Flask Mysql
Aplicación Con Ia
Resultados De Ia Con Jinja En Flask
Aplicación Con Ia
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.