Blog
En este capítulo vamos a centrarnos un poco en los posibles errores que ocurren cuando desarrollamos una aplicación, no solo hablaremos de ellos si no que también veremos como manejarlos que es lo importante de este capítulo, hasta ahora la estructura de nuestro proyecto es la siguiente:
flask1
│
│ .flaskenv
│ app.db
│ blog.py
│
├───app
│ │ formularios.py
│ │ modelos.py
│ │ rutas.py
│ │ __init__.py
│ │
│ ├───settings
│ │ │ config.py
│ │ │ __init__.py
│ │ │
│ │ └───__pycache__
│ │ config.cpython-37.pyc
│ │ __init__.cpython-37.pyc
│ │
│ ├───templates
│ │ $post.html
│ │ base.html
│ │ editar_perfil.html
│ │ gracias.html
│ │ index.html
│ │ iniciar_sesion.html
│ │ registro.html
│ │ usuarios.html
│
├───migrations/
Errores en Flask
¿Recuerdas que cuando configuramos el archivo .flaskenv en el capitulo 2 activamos el modo desarrollo y el modo debug?, esto lo hicimos debido a que cuando comenzamos a desarrollar una aplicación es un paso importante, ya que nos permite ver la información de los errores de una manera más agradable a que verlos por la consola de ejecución del servidor de flask.
Pero como mencioné antes, este archivo .flaskenv debe borrarse o no debe incluirse al momento de subir nuesta aplicación a un servidor, porque el modo de desarrollo muestra nuestro código si se produce un error interno e la aplicación.
Ahora, vamos a registrar un nuevo usuario en el blog, pero para este caso quiero que ocurra un error. Para ello tratemos de registrar un usuario con el mismo correo electrónico de algún usuario que ya este en nuestra base de datos, esto generará el siguiente error:
Lo que se puede apreciar en el error es que sqlalchemy al ejecutar la consulta a la base de datos se encuentra con que el email tiene establecido el valor “unique”, esto quiere decir que este campo no puede ser igual en otro campo porque es único. Entonces arroja el dicho error porque la base de datos no permite la entrada para este campo único.
Hasta aquí hemos visto un error común que se produce cuando se desarrolla una aplicación, pero todavía no hemos visto como manejar este tipo de errores.
Manejo de errores en Flask
Flask nos permite aprovecharnos de una función interna que el posee para manejar los errores de nuestra aplicación, esta función nos permite establecer nuestras páginas de errores personalizadas que serán mostradas cuando ocurra algún error, y así evitar que el usuario vea una página de error por defecto, que por lo general son bastante feas, si estoy en una web y me arroja un error lo que por lo menos espero es que esta página de error sea bastante bonita, ya que permite ver que los desarrolladores se preocupan por las personas que visitan su sitio.
Para declarar el controlador de errores personalizados de flask lo hacemos con el decorador @errorhandler
vamos a crear el controlador para nuestros errores dentro de la carpeta app y le damos el nombre de errores.py
from flask import render_template from app import app, bdd @app.errorhandler(404) def pagina_no_encontrada(error): return render_template('404.html'), 404 @app.errorhandler(500) def error_interno(error): bdd.session.rollback() return render_template('500.html'), 500
Ya con esto capturamos dichos errores cuando se produzcan y se mostrará la plantilla de error personalizada. Hasta ahora no hemos creado las plantillas, así que vamos a nuestra carpeta template y creamos dos archivos, 404.html y 500.html:
404.html:
{% extends "base.html" %} {% block contenido %} <h1>Página no encontrada</h1> <p><a href="{{ url_for('index') }}">Ir atrás</a></p> {% endblock %}
500.html:
{% extends "base.html" %} {% block contenido %} <h1>Ha ocurrido un error inesperado</h1> <p>El administrador del sitio ha sido notificado, te pedimos disculpas por las molestias ocasionadas</p> <p><a href="{{ url_for('index') }}">Ir atrás</a></p> {% endblock %}
Ahora vamos a importar el archivo errores.py en nuestra aplicación para poder usarlo, vamos a __init__.py y lo importamos:
from flask import Flask from app.settings.config import Ajustes from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager app = Flask(__name__) app.config.from_object(Ajustes) bdd = SQLAlchemy(app) migrate = Migrate(app, bdd) login = LoginManager(app) login.login_view = 'login' login.login_message = 'Por favor inica sesión para acceder a esta página.' from app import rutas, modelos, errores
Lo siguiente por hacer es ir a nuestro archivo .flaskenv y la variable de entorno de desarrollo decirle que estamos en producción y la del modo depuración o debug igualarla a 0 para desactivarla:
FLASK_ENV=production FLASK_DEBUG=0 FLASK_APP=blog.py
Ahora, vamos a reiniciar nuestra aplicación si la tenemos iniciada y la volvemos a iniciar para que cargue los cambios e intentemos volver a agregar el usuario con el mismo email para hacer que ocurra nuevamente el error y así obtendriamos la plantilla de error 500 que personalizamos anteriormente:
Para ver la plantilla de error 404 solo basta con escribir en la url del navegador una ruta que no tenga nuestra aplicación, en mi caso introduciré la siguiente: “/eliminar/usuario” y al introducirla me arroja un error 404 y se muestra nuestra plantilla:
Muy bien, con esto hemos visto como manejar algunos errores en flask pero de manera general, pueden manejarse errores también de forma más especifica pero ya esto es un poco más avanzado, como por ejemplo en lugar de mostrar el error 500 cuando intentamos registrar un usuario con un email que ya se encuentre en la base de datos, devuelva un error diciendo que el correo electrónico ya está siendo usado.
Envío de correo electrónicos para notificar errores
Cuando estamos en desarrollo podemos ver los errores que ocurren gracias al modo debug o por la consola de ejecución, pero una vez que nuestra aplicación es subida a un servidor, nadie va a mirar estos errores, bueno si que alguien los va a mirar, en este caso es el usuario; pero ningún usuario se encuentra en la obligación de notificarnos de dicho error, lo más seguro es que termine cerrando la web.
Ahora vamos a ir a nuestro archivo config.py y vamos a crear una clase con los parámetros de configuración para nuestro servidor de email, en este caso voy a usar parámetros de conexión para un correo gmail:
#... class ConexionMail(object): MAIL_SERVER= 'smtp.gmail.com' MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USERNAME = 'tucorreo@gmail.com' MAIL_PASSWORD = 'contraseñaDelCorreoQueProporcionasteArriba' ADMINS = 'tucorreo@gmail.com' """ Podemos pasar todos los correos de las personas que queremos que les llegue el error dentro de la variable ADMIS, yo utilicé solamente el mismo correo que envía el error, pero esto ya es un poco más avanzado. Así que solo usaremos 1 correo a donde nos notificaremos los errores nosotros mismos, en pocas palabras el correo lo estaríamos enviando al mismo correo """
Una solución un poco sólida es configurar flask para que nos envié un correo electrónico cuando ocurra un error, para este caso lo haremos para los errores que se produzcan al momento de enviar cualquier formulario dentro de nuestro blog, para ello nos situamos en el archivo __init__.py, pero antes te recomiendo ir a tu cuenta de google y cambiar el acceso a las aplicaciones menos seguras, puedes acceder desde aquí si ya te encuentras logueado, activa dicho acceso para poder proceder, nuestro archivo __init__.py debe verse así:
from flask import Flask from app.settings.config import Ajustes, ConexionMail from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager import logging from logging.handlers import SMTPHandler app = Flask(__name__) app.config.from_object(Ajustes) bdd = SQLAlchemy(app) migrate = Migrate(app, bdd) login = LoginManager(app) login.login_view = 'login' login.login_message = 'Por favor inicia sesión para acceder a esta página.' from app import rutas, modelos, errores # Configurando el servidor de email para los errores en formularios if app.debug == False: if ConexionMail.MAIL_SERVER: autenticacion = None if ConexionMail.MAIL_USERNAME or ConexionMail.MAIL_PASSWORD: autenticacion = (ConexionMail.MAIL_USERNAME, ConexionMail.MAIL_PASSWORD) seguridad = None if ConexionMail.MAIL_USE_TLS: seguridad = () enviar_email = SMTPHandler( mailhost = (ConexionMail.MAIL_SERVER, ConexionMail.MAIL_PORT), fromaddr = 'no-reply@' + ConexionMail.MAIL_SERVER, toaddrs = ConexionMail.ADMINS, subject='Fallo encontrado en nuestro Blog', credentials= autenticacion, secure=seguridad ) enviar_email.setLevel(logging.ERROR) app.logger.addHandler(enviar_email)
Primero le decimos a python que si la app no esta en modo debug ejecute el código, entonces al entrar al siguiente bloque, python verifica si existe una configuración para el “MAIL_SERVER”, como esta configurada entramos en la siguiente parte del código, el cual establece una variable autenticacion
vacía, luego valida que exista la configuración del MAIL_USERNAME y MAIL_PASSWORD, en los siguientes pasos se guardan los datos de autenticación en una variable que lleva el mismo nombre, se establece una variable de seguridad vacía y python verifica que si el TLS del correo electrónico está activado no debemos pasarle ningún parámetro de seguridad y pasamos la variable seguridad como una tupla vacía, más adelante veremos el porque.
Ahora en una variable enviar_email hacemos uso del objeto SMTHandler que se encargará de hacer la conexión al correo con los datos que se le suministran a través de las variables definidas del propio objeto.
- mailhost = Host del servidor del correo electrónico.
- fromaddr = De quien es el correo que envía el mensaje.
- toaddrs = Para quien es el correo que se envía.
- credentials = Solicita las credenciales de seguridad más un argumento extra de seguridad llamado
secure
que en nuestro caso lo pasamos como una tupla vacía porque el TLS del servidor está activado.
Por último hacemos uso de la prioridad del email y le decimos que capture los errores de login, es decir que este servidor de correo, solo notificará los errores que ocurran al momento de enviar un formulario, ya que estos son los que usan el decorador @login_required
y por último usamos el app.logger.addHandler
para capturar el log del error y enviarlo por correo, si nos dirigimos a nuestro blog e invocamos de nuevo el error, verás que se enviará el correo y lo estaremos recibiendo.
Esto solo sirve para la informarnos de los errores que se produzcan en los formularios, pero ahora; supongamos que nosotros no sabíamos de esos errores que devuelve la base de datos al tratar de crear un nuevo registro con un campo único que se encuentra ya registrado en la base de datos, y gracias al email que nos lo notificó nos enteramos de dicho error e iremos inmediatamente a tratar de arreglarlos, pues bueno vamos a proceder a hacerlo.
Para esto necesitamos ir a nuestro archivo formularios.py para implementar la solución a este error.
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length from app.modelos import Usuario class EditarPerfil(FlaskForm): username = StringField('Usuario', validators=[DataRequired(message='Este campo es requerido')]) sobre_mi = TextAreaField('Sobre mi', validators=[Length(min=0, max=140)]) submit = SubmitField('Enviar') def __init__(self, usuarioActual, *args, **kwargs): super(EditarPerfil, self).__init__(*args, **kwargs) self.usuarioActual = usuarioActual def validate_username(self, username): if username.data != self.usuarioActual: usuario = Usuario.query.filter_by(username=self.username.data).first() if usuario is not None: raise ValidationError('El nombre de usuario ya existe, por favor intenta con otro.')
Hemos definido dentro de la clase un constructor que acepta el nombre del usuario actual (usuario logueado) como argumento, este nombre de usuario se guarda como una variable de instancia y se verifica en el validate_username(),
es importante resaltar que la función validate_username
debe tener este nombre y no debemos cambiarla para que flask pueda reconocerla; como última cosa por hacer, tenemos que agregar el usuario que se encuentra logueado como argumento para el formulario, vamos a rutas.py y modificamos la función editar_perfil()
:
from app.formularios import EditarPerfil @app.route('/editar_perfil', methods=['GET', 'POST']) @login_required def editar_perfil(): form = EditarPerfil(current_user.username) # Le pasamos como argumento current_user.username if form.validate_on_submit(): current_user.username = form.username.data current_user.sobre_mi = form.sobre_mi.data bdd.session.commit() flash('Tus cambios han sido guardados correctamente') return redirect(url_for('editar_perfil')) elif request.method == 'GET': form.username.data = current_user.username form.sobre_mi.data = current_user.sobre_mi return render_template('editar_perfil.html', titulo='Editar Perfil', form=form)
Reiniciemos el servidor Flask y ahora si vamos al blog, al editar el perfil de un usuario y tratar de cambiarle el nombre de usuario por otro nombre de usuario que ya se encuentre registrado en la base de datos, obtendríamos lo siguiente:
Y es así como manejamos algunos errores en Flask e incluso pudimos ver como notificarnos de errores via email al momento en que un usuario envíe un formulario, lo último que me queda por decirte es que vuelvas a activar el modo de desarrollo y el modo depuración en nuestro archivo .flaskenv:
FLASK_ENV=development FLASK_DEBUG=1 FLASK_APP=blog.py
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: