Blog
En este capítulo nos dedicaremos a agregar páginas para los perfiles de los usuarios, una página de perfil de usuario es donde se muestra información acerca de dicho usuario, tal como su información personal esto es algo muy similar a lo que solemos ver en las redes sociales, en este capitulo haremos dicha vista para que el usuario pueda ingresar su información y esta se muestre en su perfil, logrando así hacer que la vista de los perfiles de usuarios sea dinámica.
Página de perfil de nuestro usuario
Para crear dicha página debemos dirigirnos a nuestro archivo de rutas.py y agregar la vista que va a renderizar la página para los perfiles de los usuarios, agregamos esta ruta justo debajo de la ruta registro:
@app.route('/usuario/<username>') @login_required def perfil_usuario(username): usuario = Usuario.query.filter_by(username=username).first_or_404() posts = [ {'autor': usuario, 'cuerpo': 'Test post #1'}, {'autor': usuario, 'cuerpo': 'Test post #2'} ] return render_template('usuarios.html', usuario=usuario, posts=posts)
Analizando estas lineas de código que acabos de escribir lo que hemos hecho es agregar algo nuevo al decorador @app.route en este caso un “<username>” donde username es una variable que se obtiene a través del método GET y ya bien sabemos que el método GET es usado para obtener valores o parámetros desde la url, esto quiere decir que si un usuario introduce en su navegador /usuario/juan pues el nombre “juan” lo recibe la ruta lo toma con el método GET y lo envía a la función perfil_usuario para procesar la variable.
Una vez llegado a este punto, vemos que hacemos una consulta a la base de datos pero esta vez, en lugar de usar la función .first() para devolver el primer resultado que encuentre, esta vez usaremos una muy parecida pero que evalúa la condición para cuando no exista y es la función .first_or_404() lo que hace esto es obtener el primer resultado que encuentre (como la función .first()) pero si no encuentra un resultado devolverá una página de error 404.
También es importante aclarar que esta función será solo accesible para los usuarios registrado como era de esperarse, es por eso que hemos agregado después del decorador el decorador @login_required.
Ahora en la carpeta templates vamos a crear una plantilla llamada usuarios.html
{% extends "base.html" %} {% block contenido %} <h1>Usuario: {{ usuario.username }}</h1> <hr> {% for post in posts %} <p> {{ post.autor.username }} Twitteó: <b>{{ post.cuerpo }}</b> </p> {% endfor %} {% endblock %}
Hasta ahora todo luce muy bien, pero es necesario también ir a nuestra plantilla base y agregar el enlace a nuestra vista para que los usuarios tenga acceso a su perfil y pues claro teniendo en cuenta que debe estar logueado para poder ver dicho enlace. Entonces nos dirigimos a nuestra plantilla base.html y hagamos que luzca de esta manera:
<html> <head> {% if titulo %} <title>{{ titulo }} - Blog</title> {% else %} <title>microblog</title> {% endif %} </head> <body> <div> Blog: <a href="{{ url_for('index') }}">Inicio</a> {% if current_user.is_anonymous %} <a href="{{ url_for('login') }}">Iniciar sesión</a> {% else %} <a href="{{ url_for('perfil_usuario', username=current_user.username) }}">Perfil</a> <a href="{{ url_for('logout') }}">Cerrar sesión</a> {% endif %} </div> <hr> {% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} {% block contenido %} {% endblock %} </body> </html>
Analicemos un poco lo que hemos introducido en la linea 16 de este código, le hemos dicho a través del método url_for() que redireccione al usuario a la función perfil_usuario la cual es la función que se encarga de procesar la vista para los perfiles de los usuarios del blog y luego separado por coma le pasamos un argumento más el cual es el usuario que se encuentra actualmente logueado y lo hacemos con username=current_user.username ya sabemos lo que hace la función current_user.username que es devolver el nombre del usuario logueado y debemos guardarlo dentro de una variable llamada username ya que así la definimos anteriormente en el decorador de la función perfil_usuario.
Todo esto se hace con url_for para hacerlo dinámico ya que esta vista recibe una variable a través del método GET y bien sabemos que url_for lo que hace es transformar los argumentos que le pasemos en una cadena de texto en formato de url y gracias a esto hacemos que el botón sea dinámico, ya que nos llevará al perfil del usuario que se encuentre logueado.
Iniciando sesión y probando la vista para los perfiles de los usuarios
Vamos a iniciar sesión con el usuario que creamos anteriormente, al iniciar sesión vemos que todo se procesa correctamente en dicha vista y debería verse de la siguiente manera:
Trabajando con los avatares
Vamos a utilizar un servicio muy usado hoy en día por muchos blog, incluyendo este. El servicio de Gravatar sirve para crearnos un usuario que se asocia con nuestro correo electrónico y escogemos una imagen ya sea de las predefinidas por Gravatar o subir nuestra propia imagen, el punto es que la imagen que seleccionemos en nuestra cuenta de Gravatar es la que se mostrará en los sitios web que usen este servicio, entonces vamos a integrar gravatar a nuestro blog.
Para eso nos dirigimos a nuestro archivo de modelos.py e importamos la librería hashlib para hacer uso de la función md5 y en la clase de Usuarios justo debajo de la función verif_clave agregamos lo siguiente:
from hashlib import md5 class Usuario(UserMixin, bdd.Model): # Creamos la siguiente función debajo de la función verif_clave def imagen_perfil(self, tamaño): codigo_hash = md5(self.email.lower().encode('utf-8')).hexdigest() return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(codigo_hash, tamaño)
Muy bien, procesemos nosotros mismos este código. En primer lugar le decimos que el método de la clase recibe un tamaño, este tamaño es para decirle la longitud que queremos que gravatar nos devuelva dichar imagen de perfil, entonces entrando al método, la variable codigo_hash guarda en su interior las funciones de la función md5 y el método de clase .hexdigest().
Si vamos por parte, a la función md5 le pasamos como argumentos el correo electrónico y lo transformamos todo el correo en letras pequeñas ya que Gravatar lo requiere y luego le pasamos que la codificación sea del tipo utf-8, esto nos devuelve un objeto del tipo hash pero no podemos usar dicho objeto para obtener la imagen, debemos usar solo el código hash, dicho código lo obtenemos haciendo uso del método de la clase llamado .hexdigest() que lo que hace es devolvernos solamente el código hash que se encuentra dentro del objeto hash que nos devolvió anteriormente la función md5.
Por último le decimos a la función que nos devuelva la url de Gravatar pero al final, le vamos a pasar el código hash y el tamaño en que le dijimos que queríamos la imagen.
El siguiente paso para probar lo que hemos hecho es agregar a nuestra plantilla usuarios.html donde se mostrarán estos avatares, entonces la plantilla debe lucir de la siguiente manera:
{% extends "base.html" %} {% block contenido %} <h1>Usuario: {{ usuario.username }}</h1> <hr> {% for post in posts %} <p> {{ post.autor.username }} Twitteó: <b>{{ post.cuerpo }}</b> </p> {% endfor %} <table> <tr valign="top"> <td><img src="{{ usuario.imagen_perfil(128) }}"></td> <td><h1>Usuario: {{ usuario.username }}</h1></td> </tr> </table> <hr> {% for post in posts %} <p> {{ post.autor.username }} Twitteó: <b>{{ post.cuerpo}}</b> </p> {% endfor %} {% endblock %}
Solo nos falta un paso más, ya que si recordamos nosotros tenemos publicaciones que ha hecho el usuario y estas se ven en la vista del perfil del usuario, entonces solo nos falta hacer unas pequeños cambios en la plantilla usuarios.html para también agregarle una imagen a estos posts y haremos que la plantilla luzca así:
{% extends "base.html" %} {% block contenido %} <table> <tr valign="top"> <td><img src="{{ usuario.imagen_perfil(128) }}"></td> <!-- recordar que usuario es la consulta que hicimos con el la clase Usuario y por eso podemos usar su método imagen_perfil() --> <td> <h1>Usuario: {{ usuario.username }}</h1> </td> </tr> </table> <hr> {% for post in posts %} <table> <tr valign="top"> <td><img src="{{ post.autor.imagen_perfil(36) }}"></td> <td>{{ post.autor.username }} Twitteó:<br>{{ post.cuerpo }}</td> </tr> </table> {% endfor %} {% endblock %}
Excelente, si ya tienes cuenta en Gravatar puedes probarlo con tu propia cuenta, prueba a registrarte en tu propio blog y durante el registro, registra el mismo correo electrónico con el cual tengas una cuenta en Gravatar y al iniciar sesión ya deberías de poder ver como foto de perfil la misma que tengas en Gravatar.
Mi resultado fue el siguiente:
Subplantillas, ¿para que sirven?
Las subplantillas es una manera de generar pequeñas lineas de código dentro de una plantilla, esto es ideal para cuando queremos mostrar solamente algunos resultados en un determinado lugar de nuestra web, para este ejemplo lo haremos con los post que se muestra en la vista del perfil del usuario, lo únicos dos que generamos que en este caso son el Test post#1 y el Test post#2.
Otro de sus usos es que si por ejemplo tenemos dos plantillas como podría ser una plantilla inicio.html y la plantilla perfil.html y en ambas hacemos uso del mismo código para mostrar una imagen solamente, pero si más adelante decido agregarle cambios para que ahora muestre por ejemplo la imagen, más información acerca de esa imagen; entonces tendríamos que hacer cambios tanto en la plantilla inicio como en la plantilla perfil y no es lo ideal. Por eso al usar las subplantillas si nos ocurre este caso solo tendríamos que hacerle cambios a la subplantilla
Vamos a utilizar un prefijo para nuestras subplantillas, lo cual nos ayudara a saber quienes son plantillas y quienes son las subplantillas, entonces usaremos el símbolo “$” entonces los archivos .html que contengan ese prefijo en su nombre nos ayudará a identificar que es una subplantilla y no una plantilla.
Vamos a nuestra carpeta templates y crearemos una subplantilla con el nombre $post.html y debe lucir de la siguiente forma:
<table> <tr valign="top"> <td><img src="{{ post.autor.imagen_perfil(36) }}"></td> <td>{{ post.autor.username }} Twitteó:<br>{{ post.cuerpo }}</td> </tr> </table>
Esta subplantilla contiene los post que se muestran en la plantilla usuarios.html entonces vamos a ir a dicha plantilla y vamos a decirle que incluya la subplantilla que acabamos de crear, el código debe lucir así:
{% extends "base.html" %} {% block contenido %} <table> <tr valign="top"> <td><img src="{{ usuario.imagen_perfil(128) }}"></td> <!-- recordar que usuario es la consulta que hicimos con la clase Usuario a la base de datos y es por eso podemos usar su método image_perfil() --> <td> <h1>Usuario: {{ usuario.username }}</h1> </td> </tr> </table> <hr> {% for post in posts %} {% include '$post.html' %} {% endfor %} {% endblock %}
Como ves, hemos hecho uso de {% include ‘$post.html’ %} para llamar a dicha subplantilla y que se renderice dentro del bucle for para que muestre todos los post correspondientes al usuario. Si vamos a nuestra vista de los perfiles de usuario, podremos ver que obtenemos el mismo resultado.
Haciendo aún más dinámica la vista de los perfiles de usuario
Los sitios de hoy en día cuentan con mucho contenido dinámico dentro de sus aplicaciones, en el caso de los perfiles es donde suelen centrarse más, la razón del porque; es que a las personas de hoy en día en su gran mayoría para no generalizar les gusta agregar información dentro de sus perfiles, entonces esto además de ser muy utilizado nos sirve a nosotros para seguir aprendiendo acerca de flask, así que hagamos que nuestra vista sea un poco más dinámica.
Entonces vamos a ir al archivo modelos.py y agreguemos en la clase Usuario un par de campos más:
class Usuario(UserMixin, bdd.Model): id = bdd.Column(bdd.Integer, primary_key=True) username = bdd.Column(bdd.String(64), index=True, unique=True) email = bdd.Column(bdd.String(120), index=True, unique=True) hash_clave = bdd.Column(bdd.String(128)) sobre_mi = bdd.Column(bdd.String(140)) ultima_sesion = bdd.Column(bdd.DateTime, default=datetime.utcnow) pubs = bdd.relationship('Pubs', backref='autor', lazy='dynamic')
Como hemos agregado dos campos más en nuestra base de datos, debemos realizar una migración haciendo uso de flask-migrate tal cual como lo vimos en el capitulo Flask: Bases de datos entonces hagamos las respectivas migraciones para la tabla Usuarios para que se agreguen estos dos campos, abrimos la consola para realizar el migrate
(env) λ flask db migrate -m "modelo usuarios" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added column 'usuario.sobre_mi' INFO [alembic.autogenerate.compare] Detected added column 'usuario.ultima_sesion' Generating C:\Users\gamersnautas\Desktop\miBlog\migrations\versions\2c60a75433ff_modelo_usuarios.py ... done
Ahora realizamos el upgrade
(env) λ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade 95e6fdca6032 -> 2c60a75433ff, modelo usuarios
Muy bien aunque por consola todo sea vea bien, siempre es bueno que revisemos la base de datos para asegurarnos de que todo ha ido bien, una vez que hagamos esto procedemos con el siguiente paso que es ir a nuestra plantilla usuarios.html y modificarla para que se vea así:
{% extends "base.html" %} {% block contenido %} <table> <tr valign="top"> <td><img src="{{ usuario.imagen_perfil(128) }}"></td> <!-- recordar que usuario es la consulta que hicimos con la clase Usuario a la base de datos y es por eso podemos usar su método image_perfil() --> <td> <h1>Usuario: {{ usuario.username }}</h1> {% if usuario.sobre_mi %}<p>{{ usuario.sobre_mi }}</p>{% endif %} {% if usuario.ultima_sesion %}<p>Ultima vez activo: {{ usuario.ultima_sesion }}</p>{% endif %} </td> </tr> </table> <hr> {% for post in posts %} {% include '$post.html' %} {% endfor %} {% endblock %}
Podemos ver que hemos introducido las variables dentro del condición if, esto para indicarle que se muestren unicamente si existen y lo logramos de esa manera, ya que si no lo hacemos al tratar de cargar la vista nos generará un error y no queremos que eso se muestre.
Continuando con con este capítulo, vamos ahora a utilizar los dos campos que hemos agregado para que muestren la información que queremos.
Editor de perfil
Comencemos con el campo más fácil, vamos a darle a los usuarios una forma de que puedan editar su perfil al estilo twitter o facebook, entonces vamos a dirigirnos a nuestro archivo formularios.py y vamos a crear un nuevo formulario y a importar un nuevo campo y un nuevo validador:
from wtforms import TextAreaField from wtforms.validators import Length class EditarPerfil(FlaskForm): username = StringField('Usuario', validators=[DataRequired()]) sobre_mi = TextAreaField('Sobre mi', validators=[Length(min=0, max=140)]) submit = SubmitField('Enviar')
En este caso estoy usando un nuevo campo llamado TextAreaField, este es el objeto que genera un textarea en el código html, el cual es el usado para introducir mensajes largos, pero en este caso le hemos dicho que como máximo puede aceptar 140 caracteres que es la cantidad que corresponde para dicho campo en la base de datos.
Vamos a nuestra carpeta templates y vamos a crear una plantilla llamada editar_perfil.html:
{% extends "base.html" %} {% block contenido %} <h1>Editar Perfil</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.username.label }}<br> {{ form.username(size=32) }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.sobre_mi.label }}<br> {{ form.sobre_mi(cols=50, rows=4) }}<br> {% for error in form.sobre_mi.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %}
Por ultimo agreguemos el siguiente enlace para obtener acceso a editar el perfil, y lo haremos en nuestra plantilla usuarios.html debajo del enlace ultima sesion:
{% if usuario == current_user %} <p><a href="{{ url_for('editar_perfil') }}">Editar perfil</a></p> {% endif %}
Ahora vamos a nuestro archivo de rutas.py y vamos a importar dicha vista de editar perfil y hasta ahora debe lucir así:
from flask import render_template from app import app, bdd from app.formularios import FormInicio, FormRegistro, EditarPerfil from flask import render_template, flash, redirect, url_for, request from flask_login import current_user, login_user, logout_user, login_required from app.modelos import Usuario from werkzeug.urls import url_parse @app.route('/') @app.route('/index') @login_required def index(): return render_template('index.html', titulo='Inicio') @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index')) form = FormInicio() if form.validate_on_submit(): usuario = Usuario.query.filter_by(username=form.nombre.data).first() if usuario: if usuario.verif_clave(form.contraseña.data): login_user(usuario, remember=form.recordar.data) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': next_page = url_for('index') return redirect(next_page) else: flash('Usuario o contraseña inválido') return render_template('iniciar_sesion.html', titulo='Iniciar Sesion', form=form) @app.route('/logout') def logout(): logout_user() return redirect(url_for('index')) @app.route('/registro', methods=['GET','POST']) def registro(): if current_user.is_authenticated: return redirect(url_for('index')) form = FormRegistro() if form.validate_on_submit(): usuario = Usuario(username=form.username.data, email=form.email.data) usuario.def_clave(form.contraseña.data) bdd.session.add(usuario) bdd.session.commit() return redirect(url_for('login')) return render_template('registro.html', titulo='Registro', form=form) @app.route('/usuario/<username>') @login_required def perfil_usuario(username): usuario = Usuario.query.filter_by(username=username).first_or_404() posts = [ {'autor': usuario, 'cuerpo': 'Test post #1'}, {'autor': usuario, 'cuerpo': 'Test post #2'} ] return render_template('usuarios.html', usuario=usuario, posts=posts) @app.route('/editar_perfil', methods=['GET', 'POST']) @login_required def editar_perfil(): form = EditarPerfil() 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)
Ya podemos ir a la vista y verificar que aparece el enlace para editar el perfil. Podemos probar a editar el perfil, yo lo he editado y ahora ha quedado así:
Capturar ultima actividad o sesión
Vamos a dirigirnos a nuestro archivo de rutas.py y vamos a importar la librería datetime para capturar el tiempo y importamos la base de datos bdd, además crearemos una nueva función llamada ultima_sesion a la que le agregaremos un nuevo decorador y debería de lucir así:
from flask import render_template from app import app, bdd from app.formularios import FormInicio, FormRegistro, EditarPerfil from flask import render_template, flash, redirect, url_for, request from flask_login import current_user, login_user, logout_user, login_required from app.modelos import Usuario from werkzeug.urls import url_parse from datetime import datetime @app.route('/') @app.route('/index') @login_required def index(): return render_template('index.html', titulo='Inicio') @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index')) form = FormInicio() if form.validate_on_submit(): usuario = Usuario.query.filter_by(username=form.nombre.data).first() if usuario: if usuario.verif_clave(form.contraseña.data): login_user(usuario, remember=form.recordar.data) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': next_page = url_for('index') return redirect(next_page) else: flash('Usuario o contraseña inválido') return render_template('iniciar_sesion.html', titulo='Iniciar Sesion', form=form) @app.route('/logout') def logout(): logout_user() return redirect(url_for('index')) @app.route('/registro', methods=['GET','POST']) def registro(): if current_user.is_authenticated: return redirect(url_for('index')) form = FormRegistro() if form.validate_on_submit(): usuario = Usuario(username=form.username.data, email=form.email.data) usuario.def_clave(form.contraseña.data) bdd.session.add(usuario) bdd.session.commit() return redirect(url_for('login')) return render_template('registro.html', titulo='Registro', form=form) @app.route('/usuario/<username>') @login_required def perfil_usuario(username): usuario = Usuario.query.filter_by(username=username).first_or_404() posts = [ {'autor': usuario, 'cuerpo': 'Test post #1'}, {'autor': usuario, 'cuerpo': 'Test post #2'} ] return render_template('usuarios.html', usuario=usuario, posts=posts) @app.route('/editar_perfil', methods=['GET', 'POST']) @login_required def editar_perfil(): form = EditarPerfil() 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) @app.before_request def ultima_sesion(): if current_user.is_authenticated: current_user.ultima_sesion = datetime.utcnow() bdd.session.commit()
Vamos a explicar la función del decorador @app.before_request más adelante, por ahora vamos a nuestra vista de los perfiles de usuarios y deberíamos ver una hora cercana a la de nuestro servidor, esta se actualiza cada vez que hacemos una petición al servidor, es importante resaltar que debemos usar el formato de hora utc y no el local, ya que los servidores en internet soportan solamente algunos formatos, pero este es soportado por casi todos, nunca use hora local ya que esto es un grabe error debido a que entonces los datos dependerán de su hora local y generaría errores cuando un usuario que se encuentre en otra ubicación y este desee acceder a dichos datos.
Si vamos nuevamente a la vista de perfil de los usuarios, nos quedaría algo como esto:
El decorador @app.before_request
Lo que hace el decorador @app.before_request es registrar el decorador de la función que se ejecuta antes de renderizar la vista, por ejemplo si un usuario hace una solicitud a la vista /login entonces el decorador @app.before_request lo que hará será registrar el decorador de dicha vista que en este caso sería el @app.route(‘login’).
La útilidad que esto tiene es que podemos realizar acciones antes de que se renderice la vista y en este caso lo que queremos es capturar el último movimiento que hizo el usuario en su cuenta, en pocas palabras lo que hemos hecho con @app.before_request es introducir el tiempo en que se realizo dicha solicitud en la cuenta del usuario la cual se guardará en la base de datos, para luego mostrarla en la página del perfil de los usuarios.
Si te preguntas porque no usamos el bdd.session.add() es porque estamos haciendo uso de la función current_user que devuelve un dato del tipo Usuario.username (nombre de usuario para la consulta en la base de datos) porque se supone que el usuario está logueado y al estar logueado se mantiene abierta la conexión a la base de datos del usuario, y podemos hacer uso de current_user.cualquiercampodelabdd = “Lo que queramos introducir” y luego hacer un commit y se agregará el dato al registro de dicho usuario, pero para este caso solo agregaremos información en el campo ultima_sesion.
Y como te das cuenta tiene bastante utilidad este decorador, que con un poco más de práctica te aseguro que le hallarás otra forma de darle uso.
Muy bien y así finalizamos este capítulo y nos vemos en el próximo.
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: