Blog
Flask-WTForms
Los formularios son uno de los elementos mas básicos de las aplicaciones web, ya que estos permiten la interacción de los usuarios con la aplicación. Flask, por si solo, no puede manejar formularios, pero, con la extensión Flask-WTF podemos implementar el paquete WTForms en nuestra aplicación junto con tokens CSRF, carga de archivos y ReCaptchas. Este paquete facilita mucho la definición de formularios y el manejo de sus envíos.
Las extensiones de Flask son paquetes de Python regulares que pueden ser instalados con pip. Con nuestro entorno virtual activo, procedemos a ejecutar este comando desde la terminal para instalar Flask-WTF:
(env) $ pip install flask-wtf
Ajustes
Hasta ahora, nuestra aplicación es sencilla, y por eso no era necesario preocuparnos por sus ajustes. Pero, para cualquier aplicación, excepto las mas simples, es necesario que Flask (y las extensiones de Flask) tengan ciertas libertades en cuanto como hacen las cosas, y es necesario tomar algunas decisiones, las cuales se envían al sistema como una lista de ajustes.
Hay varios formatos para que la aplicación especifique sus ajustes. La solución mas básica, es definirlos en app.config, que utiliza un estilo de diccionario para trabajar con las variables. Pero no queremos los ajustes de nuestra aplicación en el mismo lugar donde la creamos, siguiendo esta linea de separación de conceptos que tenemos en nuestra aplicación (parte lógica separada de interfaz), por lo tanto, utilizaremos una estructura un poco mas elaborada que permite que los ajustes estén separados de la base de nuestra aplicación en un archivo aparte.
Un formato que es bastante utilizado por su gran capacidad, es guardar los ajustes en variables de una clase. Para mantener las cosas organizadas, crearemos la clase de configuración en un modulo de Python separado. Vamos a crear una nueva carpeta llamada settings dentro de la carpeta app (app/settings
), dentro de esta carpeta settings crearemos dos archivos, uno que se llamará __init__.py (El archivo solo debe ser creado y no se le agregará nada, esto es para que python reconozca la carpeta settings como un módulo) y otro llamado config.py, una vez hecho esto; en el archivo config.py agregamos lo siguiente:
import os class Ajustes(object): SECRET_KEY = os.environ.get('SECRET_KEY') or 'contraseña'
Bastante sencillo, ¿cierto? Los ajustes son definidos como las variables de la clase Ajustes. Mientras la aplicación necesite mas ajustes, estos pueden ser agregados dentro de la clase, y luego, si se necesita mas de un grupo de ajustes, se pueden crear subclases de la misma.
La variable “SECRET_KEY” definida como ajuste único es una parte importante de las aplicaciones de Flask. Flask y algunas de sus extensiones utilizan el valor de la variable como llave criptográfica, útil para generar firmas y tokens. La extension Flask-WTF la usa para proteger los formularios de ataques de hackers como CSRF (acrónimo de Cross-Site Request Forgery que significa Falsificación de solicitudes entre sitios). Como su nombre lo dice, SECRET_KEY (en español, llave secreta) debe ser secreta, ya que la fuerza de los tokens y firmas generadas dependen de que nadie fuera del equipo de desarrollo la sepa.
El valor de la clave secreta es definido como una expresión con dos términos, unidos por un or. El primer termino busca el valor en una variable de entorno, llamada también SECRET_KEY. El segundo termino es un string normal. Este patrón es muy utilizado en la definición de ajustes. La idea es que el valor de la variable de entorno tenga mayor precedencia, pero si no está definida, entonces el valor del string es utilizado en su lugar. Cuando se está desarrollando una aplicación, los requerimientos de seguridad son bajos, así que este ajuste puede ser ignorado, dejando así que el string sea el valor utilizado. Pero, cuando la aplicación ya es desplegada en un servidor de producción, es necesario definir el valor de la variable de entorno de forma que no sea sencillo de adivinar.
Ya que tenemos nuestro archivo de ajustes, es necesario decirle a Flask que lo use. Esto se hace luego de que la aplicación es creada, utilizando el metodo “app.ajustes.from_object()”:, así que modificamos nuestro archivo __init__.py que se encuentra dentro de la carpeta app (El que inicia nuestra app):
from flask import Flask from app.settings.config import Ajustes app = Flask(__name__) app.config.from_object(Ajustes) from app import rutas
Formulario de Inicio de Sesión
La extension de Flask-WTF utiliza las clases de Python para representar los formularios. Una clase de formulario define simplemente los campos del formulario como variables de la clase.
Teniendo eso en mente, vamos a crear un archivo llamado formularios.py dentro de la carpeta app, donde estarán nuestros formularios. Para comenzar, creemos un formulario de inicio de sesión, el cual le solicitará al usuario su nombre de usuario y su contraseña. El formulario tendrá un botón para recordar las credenciales y un botón de inicio de sesión:
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired class FormInicio(FlaskForm): nombre = StringField('Usuario', validators=[DataRequired(message='Se requiere que completes este campo')]) contraseña = PasswordField('Contraseña', validators=[DataRequired(message='Se requiere que completes este campo')]) recordar = BooleanField('Recordar Usuario') enviar = SubmitField('Iniciar Sesión')
Las cuatro clases que representan los campos que están implementados en este formulario, son importados directamente desde WTForms, ya que Flask-WTF no provee versiones personalizadas. Para cada campo, se crea un objeto como una variable de clase en la clase FormInicio. Cada campo recibe una descripción o etiqueta como primer argumento.
El argumento validators es opcional. Es utilizado para incluir comportamientos de validación en los campos. El validador DataRequired verifica que el campo no esté vacío a la hora de enviarlo. Existen muchos validadores en Flask, de los cuales utilizaremos varios en otros formularios mas adelante.
Plantillas de los Formularios
Luego de hacer la parte operacional o la parte lógica, proseguimos a trabajar con la interfaz. Para poder utilizar estos formularios, necesitamos que estén en su plantilla HTML especifica, de forma que el explorador pueda renderizarla. La buena noticia es que los campos que definimos en FormInicio saben renderizarse a sí mismos como un HTML, asi que esta tarea es sencilla. Nos dirigimos ahora a la carpeta de las plantillas, templates, y creamos un archivo llamado iniciar_sesion.html y le agregamos lo siguiente:
{% extends "base.html" %} {% block contenido %} <h1>Iniciar Sesión</h1> <form action="" method="post" novalidate> {{ form.hidden_tag() }} <p> {{ form.nombre.label }}<br> {{ form.nombre(size=30)}}<br> </p> <p> {{ form.contraseña.label }}<br> {{ form.contraseña(size=30) }}<br> </p> <p>{{ form.recordar() }} {{ form.recordar.label }}</p> <p>{{ form.enviar() }}</p> </form> {% endblock %}
Vemos cosas conocidas como las etiquetas de bloque o de extension de base.html de partes anteriores, retomando la herencia de plantillas nuevamente. Cabe destacar que esta práctica será recurrente en todas las plantillas, para que la apariencia de la interfaz sea constante en toda la app.
Esta plantilla en especifico, espera recibir un formulario como argumento de la función view (FormInicio), el cual es referido como form.
La etiqueta HTML <form> es utilizada de contenedor para el formulario. El atributo action es utilizado para decirle al explorador que direccion URL debe utilizar al momento de enviar el formulario lleno. Cuando action se deja vacio, el formulario es enviado a la dirección que el explorador tenga abierta en ese momento, la cual es la dirección del formulario en la página. El atributo method especifica que método de solicitud HTTP debe ser utilizado al enviar el formulario al servidor. El metodo utilizado por defecto es GET, pero en la mayoría de los casos, el método POST es utilizado para que el usuario tenga una mejor experiencia, ya que las solicitudes de este tipo envían los datos del formulario en el cuerpo de la solicitud, mientras que GET envía los datos de la solicitud a través de la URL del explorador, sobrecargando la barra de direcciones. El atributo novalidate es utilizado para decirle al explorador que no aplique validación a los campos de este formulario, dejando esta tarea a Flask. Utilizar novalidate es opcional, pero por esta vez es necesario activarlo para permitir la validación desde el servidor, que será estudiada mas adelante.
La expresión form.hidden_tag() de la plantilla, genera un campo oculto que incluye un token que es utilizado para proteger el formulario de ataques a través de CSRF. Lo unico que se necesita para que el formulario esté protegido, es incluir este campo y que la variable SECRET_KEY esté definida en la configuración de Flask. Si estos dos temas son atendidos, Flask-WTF hará el resto por nosotros.
Hemos escrito formularios web HTML ya, quizás sea extraño que no hay campos HTML en esta plantilla Esto es porque los campos del formulario saben como renderizarse a sí mismos como HTML. Lo unico necesario fue incluir las etiquetas {{ form.<campo>.label }} donde queríamos los nombres de los campos, y las etiquetas {{ form.<nombre>() }} donde debía estár el campo que recibe la información. Los argumentos HTML adicionales que puedan ser necesitados, se pueden pasar como argumentos. Los campos de nombre de usuario y contraseña en la plantilla, toman el tamaño (size) como argumento que será agregado al elemento <input> de HTML como un atributo. De esta manera es como se agregan clases CSS o IDs a los formularios.
Funciones View de Formularios
Lo único que falta antes de poder ver los formularios en el explorador es una función view nueva en la aplicación que se encarge del renderizado de la plantilla del formulario. Vamos a crear una nueva función view dirigida a la dirección /iniciars, que se encarge de crear el formulario y enviarlo a la plantilla para que sea renderizada. Esta función estará en el archivo rutas.py también, junto con la función anterior:
from app import app from flask import render_template from app.formularios import FormInicio @app.route('/') @app.route('/index') def index(): usuario = {'nombre':'fede'} pubs = [ { 'autor':{'usuario':'Juan'}, 'pub':'Bonito dia en Barcelona' }, { 'autor':{'usuario':'Maria'}, 'pub':'Hoy tuve una buena tarde en el cine' } ] return render_template('index.html', titulo="Inicio", usuario=usuario, pubs=pubs) @app.route('/login') def login(): form = FormInicio() return render_template('iniciar_sesion.html', titulo='Iniciar Sesión', form=form)
Tenemos el mismo archivo con ligeros cambios. Al inicio importamos FormInicio del archivo formularios.py que estuvimos trabajando hace un momento. Luego, definimos la función view como iniciarSesion, generando a partir de FormInicio, el formulario que es pasado como argumento a la función render_template, junto con el nombre de la plantilla y el titulo de la página. Como habrán notado ya, la expresión @app.route(‘/inciar_sesion’) define la dirección URL que estará relacionada con la función view.
Para que sea mas simple, agregaremos un enlace de inicio de sesión a base.html:
<html> <head> {% if titulo %} <title>{{ titulo }} - Blog</title> {% else %} <title>Blog</title> {% endif %} </head> <body> <div>Blog: <a href="/index">Inicio</a> <a href="/login">Iniciar Sesión</a> </div> <hr> {% block contenido %}{% endblock %} </body> </html>
Si iniciamos el servidor, ya deberíamos poder ver el formulario en la página de la aplicación de la siguiente forma:
Recibir Datos de un Formulario
Si presionamos el boton de “Iniciar Sesión”, el explorador mostrará un mensaje que dice “Method Not Allowed” que significa “Método no permitido”. Esto es porque la función de inicio de sesión de la sección anterior, solo hace una parte del trabajo. Puede mostrar el formulario en la pagina, pero no tiene la parte lógica para procesar los datos enviados por los usuarios hasta ahora. Esta es otra area donde Flask-WTF hace el trabajo sencillo. A continuación veamos una versión actualizada de la función view que acepta y valida datos enviados por usuarios desde el archivo rutas.py
from wtforms import StringField from flask_wtf import FlaskForm from app import app from flask import Flask, render_template, request, redirect, url_for, flash from app.formularios import FormInicio @app.route('/') @app.route('/index') def index(): usuario = {'nombre':'fede'} pubs = [ { 'autor':{'usuario':'Juan'}, 'pub':'Bonito dia en Barcelona' }, { 'autor':{'usuario':'Maria'}, 'pub':'Hoy tuve una buena tarde en el cine' } ] return render_template('index.html', titulo="Inicio", usuario=usuario, pubs=pubs) @app.route('/login',methods=['GET', 'POST']) def login(): form = FormInicio() if(form.validate_on_submit()): flash('Inicio de sesión solicitado por el usuario {}, recordar={}'.format(form.nombre.data, form.recordar.data)) return redirect(url_for('gracias')) return render_template('iniciar_sesion.html', form=form) @app.route('/gracias') def gracias(): return render_template('gracias.html')
Tenemos nuevas contribuciones al inicio, en el decorador y dentro de la función login. En el decorador tenemos un argumento nuevo, methods, el cual le dice a Flask que metodos acepta la función view, en este caso, GET y POST, sobrescribiendo el ajuste que viene por defecto (recibir solo solicitudes GET). El protocolo HTTP define que las solicitudes GET son aquellas que devuelven información al cliente (el explorador en este caso). Todas las solicitudes en la aplicación hasta ahora son de este tipo. Las solicitudes POST son tipicamente utilizadas cuando el explorador envia datos del formulario al servidor (en realidad las solicitudes GET pueden ser utilizadas para este fin, pero no es una práctica recomendada). El error mostrado por el explorador aparece porque intentó enviar una solicitud POST y la aplicación no está configurada para aceptarla. Al ajustar methods, estamos diciendole a Flask que métodos deben ser aceptados.
El método form.validate_on_submit() hace todo el trabajo de procesamiento del formulario. Cuando el explorador envía la soliciud GET para recibir la pagina web con el formulario, este método devolvera False, para que en ese caso la función vaya directamente al final y renderice la plantilla.
Cuando el explorador envía la solicitud POST como resultado de que el usuario presione el boton de inicio de sesión, form.validate_on_submit() reune todos los datos, ejecuta las validaciones que tengan los campos y, si todo está bien, devuelve True, indicando que los datos son válidos y pueden ser procesados por la aplicación. Pero, si alguno de los datos no pasa la validación, la función devolverá False, causando asi que el formulario sea devuelto al usuario, de la misma manera cuando se hace la solicitud GET. Mas adelante agregaremos un mensaje que indique que la validación falló.
Cuando form.validate_on_submit() devuelve True, la función view de inicio de sesión llama dos funciones nuevas, importadas de Flask. La función flash() es una manera útil de mostrar un mensaje al usuario. Muchas aplicaciones utilizan esta técnica para informar al usuario si alguna acción fue exitosa o no. En este caso, este mecanismo es utilizado como solución temporal, ya que aun no está lista la infraestructura necesaria para que los usuarios inicien sesión. Lo mejor que se puede hacer hasta ahora, es mostrar un mensaje que diga que la aplicación recibió las credenciales.
La segunda función nueva implementada en la función view de inicio de sesión es redirect(). Esta función le dice al explorador que vaya automáticamente a una pagina distinta, dada como argumento. Esta función se utiliza para redireccionar al usuario a la pagina de inicio de la aplicación.
El archivo gracias.html es muy sencillo y va dentro de la carpeta de templates.
{% extends "base.html" %} {% block contenido %} <h1>Gracias por iniciar</h1> {% endblock %}
Cuando se llama a la función flash(), Flask almacena el mensaje, pero, estos mensajes no aparecen mágicamente en las paginas web. Las plantillas de la aplicación deben renderizar estos mensajes “flasheados” de forma que funcione con la interfaz del sitio. Vamos a agregar estos mensaje en la plantilla base, para que todas las plantillas hereden esta funcionalidad:
<html> <head> <meta charset="UTF-8"> {% if titulo %} <title>{{ titulo }} - Blog</title> {% else %} <title>Blog</title> {% endif %} </head> <body> <div>Blog: <a href="/index">Inicio</a> <a href="/login">Iniciar Sesion</a> </div> <hr> {% with mensajes = get_flashed_messages() %} {% if mensajes %} <ul> {% for mensaje in mensajes %} <li>{{ mensaje }}</li> {% endfor %} </ul> {% endif %} {% endwith %} {% block contenido %}{% endblock %} </body> </html>
Vamos a nuestra app especificamente a la ruta /login
y vamos a hacer la función de como si nos estuviésemos logueando, escribe cualquier cosa en dicho formulario y luego envialo y gracias a la ruta llamada /gracias
, la cual es hacia donde redireccionamos cuando se ha realizado el formulario de manera exitosa se mostrará una imagen como la siguiente:
Tenemos una expresión with, la cual asigna el resultado de llamar a get_flashed_messages() a una variable llamada mensajes, todo en el contexto de la plantilla. La función get_flashed_messages() viene de Flask, y devuelve una lista de todos los mensajes que han sido previamente registrados con flash(). El condicional que sigue, revisa que mensajes no esté vacía, y en ese caso, se crea una lista no ordenada con <ul> que va a renderizar cada mensaje como un elemento de la lista, <li>. Este estilo de renderizado no es muy bonito, pero el estilizado de la aplicación lo trataremos mas adelante.
Una propiedad interesante de estos mensajes flasheados es que, una vez que son solicitados a través de la función get_flashed_messages, son eliminados de la lista de mensajes, para que aparezcan solo una vez que la función flash() es llamada.
Vamos a probar nuevamente la aplicación (seguro ya la probaron varias veces antes de llegar hasta aqui) y veamos como funciona todo en conjunto. Asegurate de enviar datos a traves del formulario con el botón de inicio de sesión, para ver como funciona todo. Es recomendable probar a enviar el formulario vacío para ver como funciona el validador DataRequired.
Mejorar la Validación de los Campos
Los validadores que están adjuntos a los campos del formulario previenen que datos inválidos sean aceptados en la aplicación. La aplicación maneja las entradas de datos invalidas mostrando nuevamente el formulario, para que el usuario pueda hacer las correcciones necesarias.
Si intentas enviar datos inválidos, seguro podrás notar que, mientras que los mecanismos de validación estén funcionando bien, no hay mensaje alguno de que los datos son inválidos, el usuario solo vuelve a recibir el formulario. La siguiente tarea es mejorar la experiencia del usuario agregando mensajes de errores a cada campo que reciba datos inválidos.
De hecho, los validadores de los formularios generan mensajes que describen los errores automáticamente, así que lo único que falta son detalles lógicos en la plantilla para que sean renderizados.
Vamos con la parte lógica para mostrar los mensajes de validación de nombre de usuario y contraseña en sus campos, dentro de la plantilla iniciar_sesion.html:
{% extends "base.html" %} {% block contenido %} <h1>Iniciar Sesión</h1> <form action="" method="POST" novalidate> {{ form.hidden_tag() }} <p> {{ form.nombre.label }}<br> {{ form.nombre(size=30)}}<br> {% for error in form.nombre.errors %} <span style="color:red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.contraseña.label }}<br> {{ form.contraseña(size=30) }}<br> {% for error in form.contraseña.errors %} <span style="color:red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.recordar() }} {{ form.recordar.label }}</p> <p>{{ form.enviar() }}</p> </form> {% endblock %}
Lo único nuevo que tenemos son los ciclos for que muestran los errores que hayan en cada parte del formulario. Como regla general, cualquier campo que tenga validadores, tendrá sus errores agregados en form.<campo>.errors. Esto será una lista, ya que los campos pueden tener multiples validadores y mas de uno puede estar dando mensajes de error.
Si intentas enviar el formulario con alguno de los campos vacíos, recibirás un mensaje diciendo “Se requiere que completes este campo”, este es el mensaje personalizado que configuramos en el archivo formularios.py al momento de crear el formulario para el inicio de sesión, es el mensaje que va dentro del validador llamado DataRequired().
Si no se configura este mensaje, el validador DataRequired()
mostrará su mensaje por defecto el cual es “This field is required”.
Consejo:
Un problema al escribir enlaces directamente en plantillas y archivos fuente es que si algún día decide reorganizar los enlaces de su web, tendrá que buscar y reemplazar estos enlaces en toda su aplicación. Para tener un mejor control sobre estos enlaces, Flask proporciona una función llamada url_for (), que genera URL utilizando su mapeo interno de URL para ver funciones. Por ejemplo, url_for (‘index’) devuelve ‘/ index’. El argumento para url_for () es el nombre del punto final, que es el nombre de la función de vista.
Entonces, de ahora en adelante, usaremos url_for () cada vez que necesite generar una URL de aplicación. La barra de navegación en la plantilla del archivo base.html queda de la siguiente forma:
<html> <head> <meta charset="UTF-8"> {% if titulo %} <title>{{ titulo }} - Blog</title> {% else %} <title>Blog</title> {% endif %} </head> <body> <div>Blog: <a href="{{ url_for('index') }}">Inicio</a> <a href="{{ url_for('login') }}">Iniciar Sesion</a> </div> <hr> {% with mensajes = get_flashed_messages() %} {% if mensajes %} <ul> {% for mensaje in mensajes %} <li>{{ mensaje }}</li> {% endfor %} </ul> {% endif %} {% endwith %} {% block contenido %}{% endblock %} </body> </html>
Hasta ahora, la estructura de nuestro proyecto es la siguiente:
flask1 | │ .flaskenv │ blog.py │ ├───app │ │ formularios.py │ │ rutas.py │ │ __init__.py │ │ │ ├───settings │ │ │ config.py │ │ │ __init__.py │ │ │ ├───templates │ │ base.html │ │ gracias.html │ │ index.html │ │ iniciar_sesion.html
Esto ha sido todo por esta lección, continuaremos en la próxima.
➡ Continúa aprendiendo con nuestro Curso de Flask – Python:
Excelente información y la forma de presentarlos, ha sido muy didacticas.
De nuevo, muchas gracias