jueves, 10 de septiembre de 2020

Flask 18. Usar Flask Blueprint para estructurar las aplicaciones. Parte 1ª




Uso de Flask Blueprint para estructurar aplicaciones. Parte 1ª.


En este capítulo veremos como optimizar la estructura de nuestras aplicaciones. Esto será especialmente útil cuando el proyecto alcanza un cierto tamaño y además es gestionado por diferentes personas. O también cuando queramos reutilizar parte del código en otros proyectos.

Para mejorar la estructura del programa vamos a utilizar planos o módulos (blueprint

Hasta ahora todas las aplicaciones que hemos creado seguían la misma sencilla estructura, un archivo principal, unas pocas vistas, algunas plantillas etc.


+ nuestra_aplicación
|_ inicio.py
|_ static/
|_ templates/


Y todo su código, ya fuera más sencillo o más complejo seguía este mismo esquema. ¿Pero que pasa cuando la aplicación crece de tamaño? Pues si cojes, por ejemplo el código del capítulo 15, verás que el archivo inicio.py es mucho mas largo y complejo, que además hay varias plantillas dentro de templates y varios archivos.py junto al principal, conteniendo partes del proyecto. Vamos, que si no has seguido el proyecto desde el principio te parecerá un jaleo.

¿Y si esto lo juntamos con que el proyecto no sólo lo estas haciendo tú, sino que lo están elaborando varias personas? Pues que las probabilidades de que al ser el archivo principal tan grande, alguien de los que están trabajando en el modifique algo y estropee el trabajo del resto, son muy grandes.

Por ello lo ideal, es que te imagines el trabajo de cada uno de los participantes en el proyecto como unos módulos o compartimentos estancos en los que cada uno trabaja en su parte, sin afectar al resto y que al final se unen y esto es lo que hace que la aplicación este completa. Es como si la aplicación principal la dividiéramos en mini-aplicaciones. Eso es lo que van a hacer los Blueprints.

Para entender como se crean y como funcionan vamos a empezar con una aplicación muy sencilla en la que vamos a usar un único blueprint para ver como se crea y funciona, para luego coger el código del capitulo 15 y trasformarlo usando Blueprints.


Creamos una estructura básica típica de un directorio de Flask:

directorio_de_trabajo
|__ principal.py
|
|__ /templates
|__ /static

En principal.py escribimos el el código básico de una aplicación de Flask:

from flask import Flask

app=Flask(__name__)

@app.route('/')
def test():
    return "<h1>Prueba</h1>"

if __name__=="__main__":
    app.run(debug=True)

Bien, ahora crearemos una segunda vista (segunda.py), al mismo nivel que principal.py, que va a ser nuestro Blueprint de ejemplo. Si te fijas su construcción es muy parecida a como hemos creado la aplicación principal de Flask. Voy a escribir el código y comentamos el contenido.

segunda.py

from flask import Blueprint, render_template

# recomendacion: poner el mismo nombre a la variable que el archivo o directorio que contenga
# el Blueprint
segunda = Blueprint("segunda", __name__, static_folder="static", template_folder="templates")
# static _folder y template_folder son opcionales

@segunda.route('/')
@segunda.route('/home/')
def home():
    return render_template('home.html')

Comenzamos importando el módulo Blueprint de Flask (al igual que en la aplicación principal hicimos con el modulo Flask)

Creamos una variable, que se suele llamar igual que el archivo o directorio que la contiene, en este caso, segunda. Esta variable será un objeto de Blueprint (al igual que en la aplicación principal "app" es un objeto de Flask -> app=Flask(__name__) )

El primer parámetro del Blueprint es su nombre. Se suele recomendar ponerle el mismo nombre que la variable y por tanto del archivo o directorio que lo contiene. 

El segundo parámetro del Blueprint es la variable de importación que es una variable especial de python llamada __name__ (igual que en el proyecto principal)

El tercer parámetro static_folder y el cuarto template_folder son opcionales. Hacen referencia a si este Blueprint va a usar los directorios por defecto para plantillas y contenido estático de la aplicación principal (si los dejamos en blanco) o si por el contrario va a usar los suyos propios. Como ya dijimos el Blueprint es como una mini-aplicación y por tanto puede tener su propio lugar para guardar sus plantillas y su contenido estático independientemente de donde lo hagan las vistas del programa principal.

Por último usando el decorador, creamos dos vistas "home" e "/". Fíjate que aquí no usamos app.route sino que utilizamos el nombre del objeto de Blueprint, en este caso concreto "segunda.route".

Por último creamos la función que se aplicará a esas dos vistas, que será renderizar el archivo "home.html" que es una plantilla que nos da la bienvenida mostrando un pingüino de linux. (los archivos puedes encontrarlos en el código de la aplicación al final del capítulo)

Una vez que hemos creado el Blueprint hay que ir al programa principal y siempre es el mismo procedimiento. Importarlo y registrarlo en la aplicación principal.

Importarlo. Ten presente como es la estructura de la aplicación:

directorio_de_trabajo
|__ principal.py
|__ segunda.py
|__ templates
|      |__ base.html
|      |__ home.html
|__ static
Pues bien, para importar el objeto blueprint que hemos creado, y que se llama segunda, dentro de nuestro programa principal, le decimos al programa: " Importa desde el módulo segunda el objeto segunda ":

from segunda import segunda

Y una vez registrada la aplicación principal "app" registramos nuestras mini-aplicaciones o blueprints con:

app.register_blueprint(segunda)

Quedando el código de principal.py de la siguiente forma:

from flask import Flask
# importamos el blueprint
from segunda import segunda

app=Flask(__name__)
# Lo registramos una vez creada la aplicación principal
app.register_blueprint(segunda)

@app.route('/')
def test():
    return "<h1>Prueba</h1>"

if __name__=="__main__":
    app.run(debug=True)


Pues ya tenemos creado el blueprint.


Date cuenta de que en nuestra segunda vista, hay dos rutas que llevan a que se renderice la plantilla "home.html". Una es ...route('/home/) y la otra es ...route('/'). Pero es que en la aplicación principal, también hay una vista ...route('/'), con el mismo nombre. ¿Que pasará cuando la invoquemos?

Vamos a comenzar con lo más obvio, invocando la vista home:


Hasta aquí todo normal. ¿Pero que pasa si llamamos a la vista ('/')? ¿Se rendizará la palabra "prueba" que es lo que dice la vista de principal.py o por el contrario saldrá de nuevo este bonito pingüino como esta codificado para la vista ('/') en segunda.py?

Si ejecutas el programa, nada más cargar el navegador en la vista principal obtendrás la respuesta:



Esto se produce al entrar en conflicto las dos aplicaciones ya que ambas tienen el mismo nombre de vista ...route('/'). Para evitar esto y hacer que cada aplicación pueda tener vistas independientes vamos a usar el parámetro url_prefix al definir el blueprint. Simplemente añade esto al código:

from flask import Flask
# importamos el blueprint
from segunda import segunda

app=Flask(__name__)
# Lo registramos una vez creada la aplicación principal
app.register_blueprint(segunda, url_prefix="/admin")

@app.route('/')
def test():
    return "<h1>Test</h1>"

if __name__=="__main__":
    app.run(debug=True)


Con esto conseguiremos que nuestro blueprint empiece a funcionar cuando se acceda a la vista con prefijo /admin. Más concretamente a 127.0.0.1:5000/admin. En este caso @segunda.route("/") hace que se renderice el archivo home.html cuando vayamos a la vista inicial del blueprint que es la que ya comentamos, 127.0.0.1:5000/admin.  Y lo mismo ocurre si acedemos a la url 127.0.0.1:5000/admin/home


Vista para 127.0.0.1:5000/admin e 127.0.0.1:5000/admin/home





Sin embargo si ahora tecleas 127.0.0.1:5000/ si que veras el texto "prueba"





Hay otros argumentos opcionales que puedes proporcionar para modificar el comportamiento del Blueprint. Repasemos los que ya conocemos y veamos algunos nuevos:

static_folder: la carpeta donde se pueden encontrar los archivos estáticos del Blueprint

static_url_path: la URL desde la cual servir archivos estáticos.

template_folder: la carpeta que contiene las plantillas del Blueprint

url_prefix: la ruta para anteponer a todas las URL del Blueprint

subdomain: el subdominio con el que las rutas de este Blueprint coincidirán de forma predeterminada

url_defaults: un diccionario de valores predeterminados que recibirán las vistas de este Blueprint

root_path: la ruta del directorio raíz del Blueprint, cuyo valor predeterminado se obtiene del nombre de importación del Blueprint

Ten en cuenta que todas las rutas, excepto root_path, son relativas al directorio del Blueprint.

Puedes encontar más información aquí.


Estructura de un proyecto con Blueprint


Ahora ha llegado el momento de ver realmente el potencial de los blueprint y como se puede utilizar para ordenar la aplicación y poder reutilizar el código. 

Imaginemos, que no tenemos solo un segundo archivo con una sola vista sino que tenemos 100 archivos.py distintos cada uno con muchas vistas distintas. Además uno de ellos controla una parte de una aplicación de una empresa. Se puede corresponder con, por ejemplo, el segundo con la parte de administración, el tercero con la parte que gestiona el alta de clientes, el cuarto el que gestiona el control de las existencias de almacén, el quinto el que gestiona los productos que se venden online y así hasta que te alcance la imaginación.

Hagamos que cada uno de ellos se comporte como una aplicación propia, de manera que podamos trabajar con ella sin afectar al resto. 

Comencemos creando algunas carpetas y trasladando archivos. 

En el ejemplo anterior copia la siguiente estructura creando los directorios app y admin y moviendo los archivos que se indican. Crea los archivos __init__.py que son archivos vacíos que utilizamos en python para que se consideren los directorios como paquetes y podamos importar los objetos blueprint que necesitamos. 

/app será el directorio padre que contendrá las diferentes partes de nuestra empresa (admin, ventas, gestión de inventarios, clientes etc). En este ejemplo práctico solo he puesto una, admin, pero podrían ser cientos, cada uno con sus propias rutas, plantillas y contenido estático independiente.

Como ves esta estructura de trabajo es más práctica e intuitiva para aplicaciones de gran tamaño.

directorio_de_trabajo
|__ principal.py
|__ /app
      |__ __init__.py
      |__ /admin
	     |__ __init__.py
	     |__ segunda.py
	     |__ /templates
	     |__ /static

En templates acuérdate de mover el archivo base.html y home.html que están dentro.

Si has hecho todo correctamente, para conseguir que funcione únicamente tendremos que cambiar la ruta desde la cual importamos el objeto blueprint, en este caso el código de primera.py que quedará así:


from flask import Flask
# importamos el blueprint
from app.admin.segunda import segunda

app=Flask(__name__)
# Lo registramos una vez creada la aplicación principal
app.register_blueprint(segunda, url_prefix="/admin")

@app.route('/')
def test():
    return "<h1>Test</h1>"

if __name__=="__main__":
    app.run(debug=True)


Ejecuta principal.py y comprueba que puedes acceder igualmente a cada una de las rutas que vimos antes de organizar y mover los archivos.

127.0.0.1:5000/

127.0.0.1:5000/admin/

127.0.0.1:5000/admin/home


Para que todo esto quede más claro, en el próximo capitulo vamos a ver como trasformar todo el código del capitulo 15, la aplicación "login de usuarios", utilizando blueprints.


Código del capítulo.


Próximo Episodio: Flask 19. Usar Flask Blueprint para estructurar las aplicaciones. Parte 2ª


domingo, 6 de septiembre de 2020

Flask 17. Manejo de Cookies en Flask.

 


Anteriormente. Flask 16. Mostrando mensajes en Flask.


Manejo de Cookies en Flask.


Cuando estamos creando una aplicación o proyecto en Flask se nos puede dar el caso de que tengamos que guardar provisionalmente alguna información en el lado del cliente, es decir que necesitemos gestionar algún dato que nos proporciona el usuario pero desde su lado, sin subirlo al servidor. 

¿Quien no conoce el clásico caso de los carritos de la compra de las páginas web de venta de artículos online?. En ellos seleccionas los productos que quieres comprar y los incluyes en el carrito, para más tarde completar el proceso de compra. Incluso si cierras el navegador y vuelves a esa página más tarde, tu carrito de la compra seguirá ahí conteniendo los productos que ya habías seleccionado. 

Esto se consigue con las cookies. Tal como nos pone la wikipedia: 

"Una cookie es un término que hace referencia a una pequeña información enviada por un sitio web y almacenada en el navegador del usuario, de manera que el sitio web puede consultar la actividad previa del navegador." 

En resumen es una pequeña información que guardamos en el navegador del usuario información que no sea sensible y que después desde el lado del servidor podemos consultar.

Para gestionar cookies en Flask tenemos que afrontarlo en dos pasos. En primer lugar tenemos que establecer el contenido de la Cookie y crearla y en un segundo paso recuperar del servidor el valor de esa cookie.

Guardar una Cookie


Para guardar una cookie tenemos que tener presente, que la información que queremos guardar debe viajar y guardarse en el cliente, por lo tanto debemos devolverla en una respuesta, por lo que vamos a utilizar el objeto respuesta para devolver esa información.

Para crear el objeto respuesta utilizaremos el método make_reponse().

 respuesta = make_response('Hemos guardado la cookie')

A este método make_response() le podemos pasar:

  • Una página
respuesta = make_response(redirect('/'))

Depués de guardar la cookie nos devuelve, en este caso, a la página inicial. 

  • Un contenido en texto
respuesta = make_response('Hemos guardado la cookie')

Guarda la cookie y nos muestra un texto en pantalla. 

  • Json.


Por ser lo más sencillo vamos a pasarle un texto ('Hemos guardado la Cookie'). 

Ahora sobre respuesta aplicaremos la cookie  con el método set_cookie()

respuesta.set_cookie(Key="nombre de la cookie", value="valor a guardar", secure = True)

Pongo el argumento "secure = True" ya que sino, FireFox y otros navegadores aunque no lo muestren directamente,  en opciones del desarrollador nos da la siguiente alerta:

"Cookie “galleta” will be soon rejected because it has the “SameSite” attribute set to “None” or an invalid value, without the “secure” attribute. To know more about the “SameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite"

Esto también lo podríamos haber hecho usando las opciones de configuración de Flask con:

SESSION_COOKIE_SECURE = True


Bien, ya solo nos quedará devolver el objeto respuesta como return de la petición.

La aplicación de flask podría quedar de esta forma:

from flask import Flask, make_response
app = Flask(__name__)


@app.route('/')
def home():
    return 'Esta es la pagina inicial'


@app.route('/set_cookie/')
def save_cookie():
    respuesta = make_response('Hemos guardado la cookie')
    respuesta.set_cookie('mi_cookie', 'esto se guarda en la cookie', secure = True)
    return respuesta

if __name__ == '__main__':
    app.run(debug=True)

y si la ejecutamos:


Para ver la cookie podemos ir a herramientas del desarrollador web y dentro de la consola veremos nuestra cookie.




Otra opción es chequear la pestaña de almacenamiento dentro de las herramientas del desarrollador, y allí podrás ver la cookie:






Leer una Cookie.


Para recuperar una cookie del navegador, que no es más que una cadena de caracteres, utilizaremos el objeto request.cookies.get de la siguiente forma:

leer_cookie = request.cookies.get('nombre de la cookie')

Nuestro archivo quedaría de la siguiente forma:

from flask import Flask, make_response, request
app = Flask(__name__)


@app.route('/')
def home():
    return 'Esta es la pagina inicial'


@app.route('/set_cookie/')
def save_cookie():
    respuesta = make_response('Hemos guardado la cookie')
    respuesta.set_cookie('mi_cookie', 'esto se guarda en la cookie')
    return respuesta


@app.route('/get_cookie/')
def read_cookie():
    leer_cookie = request.cookies.get('mi_cookie')
    return f'Contenido de la cookie -> {leer_cookie}'


if __name__ == '__main__':
    app.run(debug=True)

y al ejecutarse:


Las Cookies pueden expirar: Los argumentos max_age y expires.


Por defecto, las cookies expiran cuando el usuario cierra la sesión, esto es en cuanto se cierra el navegador. Para conservar una cookie podemos utilizar los atributos expires o max_age.

Cuando se usan ambos max_age tiene preferencia sobre expires. 

En Lugar de expirar cuando se cierra el navegador, las cookies permanentes expiran en una fecha especifica (expires) o tras un periodo de tiempo especifico en segundos (max_age).

Nota: Cuando se establece una fecha de expiración, la fecha y hora que se establece es relativa al cliente en el que se establece la cookie, no del servidor.

Por ejemplo para poner una cookie que se conserve durante 5 minutos podemos usar la siguiente vista:

Cookie con una duración de 5 minutos.

@app.route('/set_cookie5m/')
def save_cookie5m():
    # make_response = La respuesta será 
    respuesta=make_response('Hemos guardado la cookie')
    # en respuesta ira la cookie 'nombre' 'la cookie en si'
    # La duración es max_age es 5 minutos lo que en segundos es 60 s * 5
    respuesta.set_cookie('galleta','Estoy guardado en el navegador', secure=True, max_age=60*5)
    return respuesta

si queremos borrar la cookie ponemos max_age = 0.

Un ejemplo de como establecer una cookie valida durante 90 días con el atributo expires. 

expires => Debe ser un objeto de datetime o la fecha puesta en formato Unix

Cookie con una duración de 90 días, usando expires y un objeto datetime.

@app.route('/set_cookie90d/')
def save_cookie90d():
    '''Pone una cookie en vigor durante 90 dias'''
    # make_response = La respuesta será 
    respuesta=make_response('Hemos guardado la cookie')
    # en respuesta ira la cookie 'nombre' 'la cookie en si'
    # expire = es un objeto de datetime().
    final = datetime.datetime.now()
    final = final + datetime.timedelta(days=90)
    respuesta.set_cookie('galleta','Estoy guardado en el navegador', secure=True, expires=final)
    return respuesta


Las Cookies están limitadas al directorio. El atributo path.


Si establecemos una cookie con el atributo path, esta queda asociada a esa determinada ruta. Una cookie con un atributo de ruta determinado no se puede enviar a una ruta con la que no esta relacionada, incluso si ambas rutas conviven en el mismo dominio.

Cuando se omite el atributo path durante la creación de la ruta, esta se aplica a todo el dominio "/".

Por ejemplo el código:

respuesta.set_cookie('galleta','Estoy guardado en el navegador', secure=True, 
    path="/get_cookie/")

hace que la cookie quede asociada a esa ruta y no se pueda leer desde ninguna otra.

También las cookies están asignadas a un determinado dominio salvo que se especifique otra cosa con el atributo domain.


¡No toques mi cookie! El atributo HttpOnly.


El atributo HttpOnly al establecer una cookie garantiza que un código escrito en JavaScript no pueda acceder a las cookies. Esta es la forma mas importante de protección contra ataques XSS

Para marcar una cookie como HttpOnly en flask usaremos el siguiente  atributo:

respuesta.set_cookie(key="<nombre>", value="<valor>", httponly=True)


Una cookie marcada como httponly no puede ser accedida desde JavaScript: si utilizamos el inspector de consola del navegador, en documento.cookie aparecerá una cadena de texto vacía.

¿Cuando usar HttpOnly? Pues siempre que puedas. Las cookies deben ser httponly, a menos que tengas un buen motivo para que ser tenga que acceder a ellas usando Javascript en tiempo de ejecución.