Capítulo 4. Formularios web
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Las plantillas con las que trabajaste en el Capítulo 3 son unidireccionales, en el sentido de que permiten que la información fluya del servidor al usuario. Para la mayoría de las aplicaciones, sin embargo, también es necesario que la información fluya en la otra dirección, con el usuario proporcionando datos que el servidor acepta y procesa.
Con HTML es posible crear formularios web, en los que los usuarios pueden introducir información. A continuación, el navegador web envía los datos del formulario al servidor, normalmente en forma de solicitud POST
. El objeto de solicitud Flask, introducido en el Capítulo 2, expone toda la información enviada por el cliente en una solicitud y, en particular para las solicitudes POST
que contienen datos de formularios, proporciona acceso a la información del usuario a través de request.form
.
Aunque el soporte proporcionado en el objeto request de Flask es suficiente para el manejo de formularios web, hay una serie de tareas que pueden llegar a ser tediosas y repetitivas. Dos buenos ejemplos son la generación de código HTML para los formularios y la validación de los datos enviados del formulario.
La extensión Flask-WTF hace que trabajar con formularios web sea una experiencia mucho más agradable. Esta extensión es una envoltura de integración de Flask alrededor del paquete WTForms.
Flask-WTF y sus dependencias pueden instalarse con pip:
(venv) $ pip install flask-wtf
Configuración
A diferencia de la mayoría de extensiones, Flask-WTF no necesita inicializarse a nivel de aplicación, pero espera que la aplicación tenga configurada una clave secreta. Una clave secreta es una cadena con cualquier contenido aleatorio y único que se utiliza como clave de cifrado o de firma para mejorar la seguridad de la aplicación de varias formas. Flask utiliza esta clave para proteger el contenido de la sesión de usuario contra manipulaciones. Debes elegir una clave secreta diferente en cada aplicación que construyas y asegurarte de que esta cadena no es conocida por nadie. El Ejemplo 4-1 muestra cómo configurar una clave secreta en una aplicación Flask.
Ejemplo 4-1. hola.py: Configuración de Flask-WTF
app
=
Flask
(
__name__
)
app
.
config
[
'SECRET_KEY'
]
=
'hard to guess string'
El diccionario app.config
es un lugar de propósito general para almacenar variables de configuración utilizadas por Flask, extensiones o la propia aplicación. Se pueden añadir valores de configuración al objeto app.config
utilizando la sintaxis estándar de los diccionarios. El objeto de configuración también tiene métodos para importar valores de configuración de archivos o del entorno. En el Capítulo 7 se tratará una forma más práctica de gestionar los valores de configuración de una aplicación más grande.
Flask-WTF requiere que se configure una clave secreta en la aplicación porque esta clave forma parte del mecanismo que utiliza la extensión para proteger todos los formularios contra los ataques de falsificación de petición en sitios cruzados (CSRF). Un ataque CSRF se produce cuando un sitio web malicioso envía peticiones al servidor de aplicaciones en el que el usuario está conectado en ese momento. Flask-WTF genera tokens de seguridad para todos los formularios y los almacena en la sesión del usuario, que está protegida con una firma criptográfica generada a partir de la clave secreta.
Nota
Para mayor seguridad, la clave secreta debe almacenarse en una variable de entorno en lugar de incrustarse en el código fuente. Esta técnica se describe en el Capítulo 7.
Clases de formulario
Cuando se utiliza Flask-WTF, cada formulario web se representa en el servidor mediante una clase que hereda de la clase FlaskForm
. La clase define la lista de campos del formulario, cada uno representado por un objeto. Cada objeto de campo puede tener uno o varios validadores adjuntos. Un validador es una función que comprueba si los datos enviados por el usuario son válidos.
El Ejemplo 4-2 muestra un formulario web sencillo que tiene un campo de texto y un botón de envío.
Ejemplo 4-2. hola.py: definición de la clase formulario
from
flask_wtf
import
FlaskForm
from
wtforms
import
StringField
,
SubmitField
from
wtforms.validators
import
DataRequired
class
NameForm
(
FlaskForm
):
name
=
StringField
(
'What is your name?'
,
validators
=
[
DataRequired
()])
submit
=
SubmitField
(
'Submit'
)
Los campos del formulario se definen como variables de clase, y a cada variable de clase se le asigna un objeto asociado al tipo de campo. En este ejemplo, el formulario NameForm
tiene un campo de texto llamado name
y un botón de envío llamado submit
. La clase StringField
representa un elemento HTML <input>
con un atributo type="text"
. La clase SubmitField
representa un elemento HTML <input>
con un atributo type="submit"
. El primer argumento de los constructores de campo es la etiqueta que se utilizará al convertir el formulario a HTML.
El argumento opcional validators
incluido en el constructor StringField
define una lista de verificadores que se aplicarán a los datos enviados por el usuario antes de que sean aceptados. El validador DataRequired()
garantiza que el campo no se envíe vacío.
Nota
La clase base FlaskForm
está definida por la extensión Flask-WTF, por lo que se importa de flask_wtf
. Los campos y validadores, sin embargo, se importan directamente del paquete WTForms.
La lista de campos HTML estándar admitidos por WTForms se muestra en la Tabla 4-1.
Tipo de campo | Descripción |
---|---|
|
Casilla de verificación con los valores |
|
Campo de texto que acepta un valor |
|
Campo de texto que acepta un valor |
|
Campo de texto que acepta un valor |
|
Campo de carga de archivos |
|
Campo de texto oculto |
|
Campo de carga múltiple de archivos |
|
Lista de campos de un tipo determinado |
|
Campo de texto que acepta un valor de coma flotante |
|
Formulario incrustado como campo en un formulario contenedor |
|
Campo de texto que acepta un valor entero |
|
Campo de texto Contraseña |
|
Lista de botones de radio |
|
Lista desplegable de opciones |
|
Lista desplegable de opciones con selección múltiple |
|
Botón de envío del formulario |
|
Campo de texto |
|
Campo de texto de varias líneas |
La lista de validadores incorporados a WTForms se muestra en la Tabla 4-2.
Validador | Descripción |
---|---|
|
Valida que el campo contiene datos tras la conversión de tipo |
|
Valida una dirección de correo electrónico |
|
Compara los valores de dos campos; útil cuando se pide que se introduzca una contraseña dos veces para confirmarla |
|
Valida que el campo contiene datos antes de la conversión de tipo |
|
Valida una dirección de red IPv4 |
|
Valida la longitud de la cadena introducida |
|
Valida una dirección MAC |
|
Valida que el valor introducido está dentro de un rango numérico |
|
Permite la introducción de datos vacíos en el campo, saltándose los validadores adicionales |
|
Valida la entrada según una expresión regular |
|
Valida una URL |
|
Valida un UUID |
|
Valida que la entrada es uno de una lista de valores posibles |
|
Valida que la entrada no sea ninguno de una lista de valores posibles |
Representación HTML de formularios
Los campos de formulario son callables que, cuando se invocan desde una plantilla, se renderizan a sí mismos en HTML. Suponiendo que la función de vista pase una instancia de NameForm
a la plantilla como un argumento llamado form
, la plantilla puede generar un formulario HTML sencillo como sigue:
<form
method=
"POST"
>
{{ form.hidden_tag() }} {{ form.name.label }} {{ form.name() }} {{ form.submit() }}</form>
Observa que, además de los campos name
y submit
, el formulario tiene un elemento form.hidden_tag()
. Este elemento define un campo de formulario adicional que está oculto, utilizado por Flask-WTF para implementar la protección CSRF.
Por supuesto, el resultado de renderizar un formulario web de esta forma es extremadamente desnudo. Cualquier argumento de palabra clave que se añada a las llamadas que renderizan los campos se convierte en atributos HTML para el campo; así, por ejemplo, puedes dar al campo los atributos id
o class
y luego definir estilos CSS para ellos:
<form
method=
"POST"
>
{{ form.hidden_tag() }} {{ form.name.label }} {{ form.name(id='my-text-field') }} {{ form.submit() }}</form>
Pero incluso con atributos HTML, el esfuerzo necesario para renderizar un formulario de esta forma y que quede bien es considerable, por lo que es mejor aprovechar el propio conjunto de estilos de formulario de Bootstrap siempre que sea posible. La extensión Flask-Bootstrap proporciona una función de ayuda de alto nivel que renderiza un formulario Flask-WTF completo utilizando los estilos de formulario predefinidos de Bootstrap, todo con una sola llamada. Utilizando Flask-Bootstrap, el formulario anterior puede renderizarse como sigue:
{% import "bootstrap/wtf.html" as wtf %} {{ wtf.quick_form(form) }}
La directiva import
funciona del mismo modo que los scripts normales de Python y permite importar elementos de plantilla y utilizarlos en muchas plantillas. El archivo importado bootstrap/wtf.html define funciones de ayuda que renderizan formularios Flask-WTF utilizando Bootstrap. La función wtf.quick_form()
toma un objeto formulario Flask-WTF y lo renderiza utilizando los estilos predeterminados de Bootstrap. La plantilla completa de hola.py se muestra en el Ejemplo 4-3.
Ejemplo 4-3. templates/index.html: uso de Flask-WTF y Flask-Bootstrap para mostrar un formulario
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky{% endblock %} {% block page_content %}<div
class=
"page-header"
>
<h1>
Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }} {% endblock %}
El área de contenido de la plantilla tiene ahora dos secciones. La primera sección es una cabecera de página que muestra un saludo. Aquí se utiliza un condicional de plantilla. Los condicionales en Jinja2 tienen el formato {% if condition %}...{% else %}...{% endif %}
. Si la condición se evalúa como True
, lo que aparece entre las directivas if
y else
se añade a la plantilla renderizada. Si la condición se evalúa como False
, entonces lo que hay entre las directivas else
y endif
se renderiza en su lugar. El propósito de esto es mostrar Hello, {{ name }}!
cuando la variable de plantilla name
está definida, o la cadena Hello, Stranger!
cuando no lo está. La segunda sección del contenido muestra la forma NameForm
utilizando la función wtf.quick_form()
.
Manejo de formularios en funciones de vista
En la nueva versión de hola.py, la función de vista index()
tendrá dos tareas. Primero mostrará el formulario y después recibirá los datos introducidos por el usuario. El Ejemplo 4-4 muestra la función vista index()
actualizada.
Ejemplo 4-4. hola.py: manejar un formulario web con métodos de petición GET y POST
@app.route
(
'/'
,
methods
=
[
'GET'
,
'POST'
])
def
index
():
name
=
None
form
=
NameForm
()
if
form
.
validate_on_submit
():
name
=
form
.
name
.
data
form
.
name
.
data
=
''
return
render_template
(
'index.html'
,
form
=
form
,
name
=
name
)
El argumento methods
añadido al decorador app.route
indica a Flask que registre la función de vista como gestora de las peticiones GET
y POST
en el mapa URL. Si no se indica methods
, la función de vista se registra para gestionar únicamente las solicitudes GET
.
Añadir POST
a la lista de métodos es necesario porque los envíos de formularios se gestionan mucho más cómodamente como peticiones POST
. Es posible enviar un formulario como una petición GET
, pero como las peticiones GET
no tienen cuerpo, los datos se añaden a la URL como una cadena de consulta y se hacen visibles en la barra de direcciones del navegador. Por ésta y otras razones, los formularios se envían casi siempre como peticiones POST
.
La variable local name
se utiliza para mantener el nombre recibido del formulario cuando está disponible; cuando no se conoce el nombre, la variable se inicializa a None
. La función vista crea una instancia de la clase NameForm
mostrada anteriormente para representar el formulario. El método validate_on_submit()
del formulario devuelve True
cuando el formulario ha sido enviado y los datos han sido aceptados por todos los validadores de campos. En todos los demás casos, validate_on_submit()
devuelve False
. El valor de retorno de este método sirve efectivamente para determinar si el formulario necesita ser renderizado o procesado.
Cuando un usuario navega por la aplicación por primera vez, el servidor recibirá una petición GET
sin datos de formulario, por lo que validate_on_submit()
devolverá False
. El cuerpo de la sentencia if
se omitirá y la solicitud se gestionará renderizando la plantilla, que obtiene como argumentos el objeto formulario y la variable name
establecida en None
. Los usuarios verán ahora el formulario mostrado en el navegador.
Cuando el usuario envía el formulario, el servidor recibe una petición a POST
con los datos. La llamada a validate_on_submit()
invoca al validador DataRequired()
adjunto al campo nombre. Si el nombre no está vacío, el validador lo acepta y validate_on_submit()
devuelve True
. Ahora el nombre introducido por el usuario es accesible como atributo data
del campo. Dentro del cuerpo de la sentencia if
, este nombre se asigna a la variable local name
y el campo del formulario se borra estableciendo ese atributo data
en una cadena vacía, de modo que el campo quede en blanco cuando el formulario se vuelva a mostrar en la página. La llamada render_template()
de la última línea renderiza la plantilla, pero esta vez el argumento name
contiene el nombre del formulario, por lo que el saludo será personalizado.
Consejo
Si has clonado el repositorio Git de la aplicación en GitHub, puedes ejecutar git checkout 4a
para comprobar esta versión de la aplicación.
La Figura 4-1 muestra el aspecto del formulario en la ventana del navegador cuando un usuario entra inicialmente en el sitio. Cuando el usuario envía un nombre, la aplicación responde con un saludo personalizado. El formulario sigue apareciendo debajo, por lo que el usuario puede enviarlo varias veces con nombres diferentes si lo desea. La Figura 4-2 muestra la aplicación en este estado.
Si el usuario envía el formulario con un nombre vacío, el validador DataRequired()
detecta el error, como se ve en la Figura 4-3. Observa cuánta funcionalidad se proporciona automáticamente. Este es un gran ejemplo de la potencia que pueden dar a tu aplicación extensiones bien diseñadas como Flask-WTF y Flask-Bootstrap.
Redirecciones y sesiones de usuario
La última versión de hola.py tiene un problema de usabilidad. Si introduces tu nombre y lo envías, y luego pulsas el botón de actualizar en tu navegador, es probable que recibas una oscura advertencia que te pide confirmación antes de volver a enviar el formulario. Esto ocurre porque los navegadores repiten la última petición enviada cuando se les pide que actualicen una página. Cuando la última petición enviada es una petición a con datos de formulario, una actualización provocaría un envío duplicado del formulario, que en casi todos los casos no es la acción deseada. Por eso, el navegador pide confirmación al usuario. POST
Muchos usuarios no entienden esta advertencia del navegador. En consecuencia, se considera una buena práctica que las aplicaciones web nunca dejen una petición POST
como la última petición enviada por el navegador.
Esto se consigue respondiendo a las solicitudes de POST
con una redirección en lugar de una respuesta normal. Una redirección es un tipo especial de respuesta que contiene una URL en lugar de una cadena con código HTML. Cuando el navegador recibe una respuesta de redirección, emite una petición GET
para la URL de redirección, y ésa es la página que muestra. La página puede tardar unos milisegundos más en cargarse debido a la segunda petición que hay que enviar al servidor, pero aparte de eso, el usuario no verá ninguna diferencia. Ahora la última petición es una GET
, por lo que el comando actualizar funciona como se esperaba. Este truco se conoce como patrón Post/Redirect/Get.
Pero este enfoque conlleva un segundo problema. Cuando la aplicación gestiona la petición POST
, tiene acceso al nombre introducido por el usuario en form.name.data
, pero en cuanto esa petición finaliza, los datos del formulario se pierden. Como la petición POST
se gestiona con una redirección, la aplicación necesita almacenar el nombre para que la petición redirigida pueda disponer de él y utilizarlo para construir la respuesta real.
Las aplicaciones pueden "recordar" cosas de una solicitud a la siguiente almacenándolas en la sesión de usuario, un almacenamiento privado que está disponible para cada cliente conectado. La sesión de usuario se introdujo en el Capítulo 2 como una de las variables asociadas al contexto de solicitud. Se llama session
y se accede a ella como a un diccionario estándar de Python.
Nota
Por defecto, las sesiones de usuario se almacenan en cookies del lado del cliente que se firman criptográficamente utilizando la clave secreta configurada. Cualquier manipulación del contenido de la cookie invalidaría la firma, invalidando así la sesión.
El Ejemplo 4-5 muestra una nueva versión de la función de vista index()
que implementa redireccionamientos y sesiones de usuario.
Ejemplo 4-5. hola.py: redirecciones y sesiones de usuario
from
flask
import
Flask
,
render_template
,
session
,
redirect
,
url_for
@app.route
(
'/'
,
methods
=
[
'GET'
,
'POST'
])
def
index
():
form
=
NameForm
()
if
form
.
validate_on_submit
():
session
[
'name'
]
=
form
.
name
.
data
return
redirect
(
url_for
(
'index'
))
return
render_template
(
'index.html'
,
form
=
form
,
name
=
session
.
get
(
'name'
))
En la versión anterior de la aplicación, se utilizaba una variable local name
para almacenar el nombre introducido por el usuario en el formulario. Ahora esa variable se coloca en la sesión del usuario como session['name']
para que se recuerde más allá de la solicitud.
Las solicitudes que vengan con datos de formulario válidos terminarán ahora con una llamada a redirect()
, una función de ayuda de Flask que genera la respuesta de redirección HTTP. La función redirect()
toma como argumento la URL a la que redirigir. La URL de redirección utilizada en este caso es la URL raíz, por lo que la respuesta podría haberse escrito de forma más concisa como redirect('/')
, pero en su lugar se utiliza la función generadora de URL de Flask url_for()
, introducida en el Capítulo 3.
El primer y único argumento requerido a url_for()
es el nombre del punto final, el nombre interno que tiene cada ruta. Por defecto, el punto final de una ruta es el nombre de la función de vista asociada a ella. En este ejemplo, la función de vista que gestiona la URL raíz es index()
, por lo que el nombre dado a url_for()
es index
.
El último cambio está en la función render_template()
, que ahora obtiene el argumento name
directamente de la sesión utilizando session.get('name')
. Al igual que con los diccionarios normales, utilizar get()
para solicitar una clave del diccionario evita una excepción para las claves que no se encuentren. El método get()
devuelve un valor por defecto de None
para una clave que falta.
Consejo
Si has clonado el repositorio Git de la aplicación en GitHub, puedes ejecutar git checkout 4b
para comprobar esta versión de la aplicación.
Con esta versión de la aplicación, puedes ver que al actualizar la página en tu navegador siempre se produce el comportamiento esperado.
Mensaje intermitente
A veces es útil dar al usuario una actualización de estado después de completar una solicitud. Puede ser un mensaje de confirmación, una advertencia o un error. Un ejemplo típico es cuando envías un formulario de acceso a un sitio web con un error y el servidor responde mostrando de nuevo el formulario de acceso con un mensaje encima que te informa de que tu nombre de usuario o contraseña no son válidos.
Flask incluye esta funcionalidad como característica principal. El Ejemplo 4-6 muestra cómo se puede utilizar la función flash()
para este fin.
Ejemplo 4-6. hola.py: mensajes intermitentes
from
flask
import
Flask
,
render_template
,
session
,
redirect
,
url_for
,
flash
@app.route
(
'/'
,
methods
=
[
'GET'
,
'POST'
])
def
index
():
form
=
NameForm
()
if
form
.
validate_on_submit
():
old_name
=
session
.
get
(
'name'
)
if
old_name
is
not
None
and
old_name
!=
form
.
name
.
data
:
flash
(
'Looks like you have changed your name!'
)
session
[
'name'
]
=
form
.
name
.
data
return
redirect
(
url_for
(
'index'
))
return
render_template
(
'index.html'
,
form
=
form
,
name
=
session
.
get
(
'name'
))
En este ejemplo, cada vez que se envía un nombre, se compara con el nombre almacenado en la sesión del usuario, que se habrá puesto allí durante un envío anterior del mismo formulario. Si los dos nombres son diferentes, se invoca la función flash()
con un mensaje que se mostrará en la siguiente respuesta enviada al cliente.
Llamar a flash()
no es suficiente para que se muestren los mensajes; las plantillas utilizadas por la aplicación necesitan renderizar estos mensajes. El mejor lugar para renderizar los mensajes parpadeantes es la plantilla base, porque así se habilitarán estos mensajes en todas las páginas. Flask pone a disposición de las plantillas una función get_flashed_messages()
para recuperar los mensajes y renderizarlos, como se muestra en el Ejemplo 4-7.
Ejemplo 4-7. plantillas/base.html: representación de mensajes parpadeantes
{% block content %}<div
class=
"container"
>
{% for message in get_flashed_messages() %}<div
class=
"alert alert-warning"
>
<button
type=
"button"
class=
"close"
data-dismiss=
"alert"
>
×
</button>
{{ message }}</div>
{% endfor %} {% block page_content %}{% endblock %}</div>
{% endblock %}
En este ejemplo, los mensajes se representan utilizando los estilos CSS de alerta de Bootstrap para los mensajes de advertencia (en la Figura 4-4 se muestra uno).
Se utiliza un bucle porque podría haber varios mensajes en cola para su visualización, uno por cada vez que se llamó a flash()
en el ciclo de solicitud anterior. Los mensajes que se recuperen de get_flashed_messages()
no se devolverán la próxima vez que se llame a esta función, por lo que los mensajes parpadeantes sólo aparecen una vez y luego se descartan.
Consejo
Si has clonado el repositorio Git de la aplicación en GitHub, puedes ejecutar git checkout 4c
para comprobar esta versión de la aplicación.
Ser capaz de aceptar datos del usuario a través de formularios web es una característica necesaria para la mayoría de las aplicaciones, y también lo es la capacidad de almacenar esos datos en un almacenamiento permanente. El uso de bases de datos con Flask es el tema del próximo capítulo.
Get Desarrollo Web con Flask, 2ª Edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.