Blog
Desde que comenzamos a desarrollar esta pequeña app al estilo de Twitter hemos agregado a nuestra app desde un primer ¡Hola mundo! hasta llegar a configurar bases de datos, login y registro de usuarios y por último la función de seguidores, lo que haremos en este capítulo será una paginación para lograr que los post que que realicen no se muestren en una sola página del blog, ya que a medida que se vayan publicando más posts entre usuarios, pues tendríamos una página muy larga con todos esos post, para evitar esto debemos introducir la función de paginación, la cual nos permitirá mostrar cierta cantidad de post por página.
Vamos a comenzar creando un nuevo formulario, para ello nos dirigimos a nuestro archivo formularios.py y agregamos el siguiente modelo para el formulario de los posts:
class Publicaciones(FlaskForm): post = TextAreaField('Escribele algo al mundo', validators=[ DataRequired(), Length(min=1, max=140)]) submit = SubmitField('Twittear')
Una vez creado este código, nos falta integrarlo en una de nuestras plantillas, así que vamos a ir a la plantilla que teníamos totalmente olvidada hasta ahora, vamos a la plantilla index.html para integrarlo y debería de lucir de la siguiente manera:
{% extends "base.html" %} {% block contenido %} <h1>Hola, {{ current_user.username }}!</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% for post in posts %} <p> {{ post.autor.username }} Twitteó: <b>{{ post.cuerpo }}</b> </p> {% endfor %} {% endblock %}
Listo, hasta ahora tenemos el formulario y lo hemos integrado a la plantilla que se encargará de mostrarlo, pero falta importar también dicho formulario en la ruta /index
de nuestro archivo rutas.py, ahora esta ruta debe verse así:
from app.formularios import Publicaciones from app.modelos import Pubs @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = Publicaciones() if form.validate_on_submit(): post = Pubs(cuerpo=form.post.data, autor=current_user) bdd.session.add(post) bdd.session.commit() flash('Publicación enviada correctamente.') return redirect(url_for('index')) posts = [ { 'autor': {'username': 'Juam'}, 'cuerpo': '¡Excelente película vi hoy en el cine!' }, { 'autor': {'username': 'Samuel'}, 'cuerpo': 'Es un día excelente para ir a nadar.' } ] return render_template("index.html", titulo='Página de inicio', form=form, posts=posts)
Todo correcto pero si vamos a la página index te das cuenta que se muestran unos posts y son esos que hemos agregado en la memoria en la vista /index
para que se muestren en la página, pero ya es hora de mostrar los posts que vengan desde nuestra base de datos, esos posts que hagan los usuarios que siga el usuario que esta viendo la página /index
y además de eso ver los suyos también. Todo esto se logra a través de una consulta y es lo que haremos a continuación; en el mismo archivo de rutas.py vamos a eliminar esos posts que se guardan en la memoria y agregar los posts que vengan de la base de datos, ahora vamos al archivo rutas.py y la ruta /index
debe lucir de esta manera:
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = Publicaciones() if form.validate_on_submit(): post = Pubs(cuerpo=form.post.data, autor=current_user) bdd.session.add(post) bdd.session.commit() flash('Publicación enviada correctamente.') return redirect(url_for('index')) posts = current_user.pubs_seguidores().all() return render_template("index.html", titulo='Página de inicio', form=form, posts=posts)
Excelente, ahora si un usuario sigue a otro además de ver los propios post que el publique también vera los post de todos aquellos usuarios que siga.
Creando un explorador para ver publicaciones de otros usuarios
Anteriormente les comenté que la web no tenía una forma de buscar a los usuarios, y la única manera de encontrarlos era introduciendo manualmente su nombre de usuario en la url, pero esto no es muy práctico ya que para encontrar a alguien tenemos que saber cual es su usuario.
Entonces vamos a solucionar esto agregando un pequeño explorador de usuarios, vamos a nuestro archivo de rutas.py y crearemos la siguiente ruta:
@app.route('/explorar') @login_required def explorar(): posts = Pubs.query.order_by(Pubs.timestamp.desc()).all() return render_template('index.html', titulo='Explorar', posts=posts)
Ahora en la plantilla index.html debemos agregar un condicional en el formulario que usan los usuarios para realizar sus publicaciones, esto para prevenir el error de que el formulario no esta definido, ya que si te das cuenta la ruta /explorar
también renderiza a la plantilla index.html, entonces deberíamos de tener algo como esto:
{% extends "base.html" %} {% block contenido %} <h1>Hola, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endif %} {% for post in posts %} <p> {{ post.autor.username }} Twitteó: <b>{{ post.cuerpo }}</b> </p> {% endfor %} {% endblock %}
Ahora lo que debemos hacer es ir a nuestra plantilla base.html y agregar un enlace a esta nueva función de explorar que hemos integrado y el resultado debe ser así:
<html> <head> {% if titulo %} <title>{{ titulo }} - Blog</title> {% else %} <title>Blog</title> {% endif %} </head> <body> <div> Blog: <a href="{{ url_for('index') }}">Inicio</a> {% if current_user.is_anonymous %} <a href="{{ url_for('login') }}">Login</a> {% else %} <a href="{{ url_for('perfil_usuario', username=current_user.username) }}">Perfil</a> <a href="{{ url_for('explorar') }}">Explorar</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>
¿Recuerdas la subplantilla $post.html que creamos hace unas lecciones?, pues ahora vamos a darle un retoque de la siguiente forma:
<table> <tr valign="top"> <td><img src="{{ post.autor.imagen_perfil(36) }}"></td> <td> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> {{ post.autor.username }} </a> Twitteó:<br>{{ post.cuerpo }} </td> </tr> </table>
Ahora puedo usar esta subplantilla para mostrar las publicaciones del blog en la página de inicio y explorar (recordar que la página explorar y la página de inicio usan la misma plantilla index.html), entonces la subplantilla $post.html la integramos en index.html como anteriormente se los mostré:
{% extends "base.html" %} {% block contenido %} <h1>Hola, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endif %} {% for post in posts %} <p> {% include '$post.html' %} </p> {% endfor %} {% endblock %}
Llegado a este punto, ya es momento de probar los cambios que hemos hecho, iniciaré sesión con el usuario que he venido usando durante todo este curso y seguiré otro usuario que cree anteriormente al cual también le hice un post, cuando vaya a la ruta /index
(página de inicio) podré ver el post que tiene el usuario con el que inicie sesión más el post de la persona que estoy siguiendo:
Por ahora, nuestra aplicación es más robusta. Pero se que de seguro te estarás preguntando ¿por qué no hemos hecho nada de la paginación?, la respuesta es muy sencilla, para poder hacer la paginación necesitabamos primeramente hacer que los posts se vieran en el blog y es lo que hemos hecho, pero a continuación si comenzaremos con la paginación.
Paginación para los posts del blog
Antes de comenzar, vamos a aclarar el porque es importante mantener una web que muestre información desde una base de datos, no solo de un blog si no también por ejemplo una tienda, web para reservar hoteles e incluso las típicas web que te permiten buscar trabajo.
Pero para nuestro ejemplo, como comenté anteriormente si un usuario llegase a seguir a 2000 personas y entre esas 2000 personas el 90% de esa cantidad tenga un total de 600 posts por persona y el otro 10% un total de 400 post por persona. A estos post se le suman los post de este mismo usuario suponiendo que el usuario tiene 250 post lo cual hace un total de 1.160.250 posts que se deben mostrar en la página de inicio de este usuario lo cual haría que la página aumente su tamaño de manera vertical y el usuario scrollearia la página sin ver prácticamente un final (aunque si lo tenga). Por otra parte ¿te imaginas hacerle una consulta tan grande a nuestra base de datos?, esto es posible pero lo que pasaría es que la página probablemente tardaría horas en responder porque la base de datos se tarda en devolver un resultado porque debe procesar todos estos datos. El caso de ejemplo es explicarte que esto no es práctico y aquí es donde entra en juego la paginación, para tratar de hacer las consultas por parte por así decirlo.
Entonces para cubrir este gran problema que tenemos usaremos el objeto.paginate()
de SQLAlchemy, una consulta de paginación se hace de la siguiente manera, debo acotar que esto es un ejemplo simplemente para explicar los argumentos, por ahora no debemos agregar nada a nuestro código:
usuario.pubs_seguidores().paginate(1, 20, False).items # Esto es un ejemplo simplemente para explicar los argumentos
Información:
Dentro del objeto.paginate
tenemos los siguientes argumentos:
- Primer argumento = Acá se encuentra el número
1
lo que hace es decirle que queremos la paginación desde la página 1. - Segundo argumento = Complementa al primer argumento y es para decirle a la base de datos cuantos datos queremos mostrar por página, que para este caso serían
20
. - Tercer argumento = Es un booleano que si se encuentra en
False
, lo que hará es devolver una lista vacía si se le pasan páginas fueras de rango y si se está en true devolverá un error 404.
Ahora si que sí, es momento de comenzar a habilitar la paginación para nuestra app, para ello vamos a ir a nuestro archivo de config.py y le agregamos lo siguiente a la clase de Ajustes:
class Ajustes(object): # ... POSTS_PER_PAGE = 3
Hacemos uso del método POST_PER_PAGE
que es propio del objeto .paginate()
ahora, la manera en como se van a incorporar las páginas en la url de nuestra app es de la suiguiente manera:
http://127.0.0.1:5000/index/Pagina = 1
http://127.0.0.1:5000/index/Pagina = 2
y así sucesivamente, es importante destacar que la cantidad de páginas dependerá de la cantidad de datos que hayan almacenados en nuestra base de datos.
A continuación vamos agregar dicha función a nuestra app, para ello nos dirigimos al archivo de rutas.py y modificamos tanto la ruta /index
como la de /explorar
y deben verse de la siguiente manera:
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = Publicaciones() if form.validate_on_submit(): post = Pubs(cuerpo=form.post.data, autor=current_user) bdd.session.add(post) bdd.session.commit() flash('Publicación enviada correctamente.') return redirect(url_for('index')) pagina = request.args.get('pagina', 1, type=int) posts = Pubs.query.order_by(Pubs.timestamp.desc()).paginate(pagina, app.config['POSTS_PER_PAGE'], False) return render_template("index.html", titulo='Explorar', posts=posts.items) @app.route('/explorar') @login_required def explorar(): posts = Pubs.query.order_by(Pubs.timestamp.desc()).all() return render_template('index.html', titulo='Explorar', posts=posts)
Teniendo en cuenta este código, ya tenemos habilitada la paginación dentro de nuestra app. Es importante tener en cuenta que el argumento app.config[‘POST_PER_PAGE’] toma los valores que le dimos a este parámetro en el archivo configuraciones.py en la clase Config
Por otro lado se encuentra el objeto .paginate()
que cumple las funciones que ya definimos anteriormente, para mostrar los posts en la plantilla concatenamos la consulta que guardamos en la variable posts
y la concatenamos con el método .items
para obtener cada uno de los posts.
Mostrando la páginación en nuestra plantilla
Como último código que escribimos, ya tenemos la paginación en nuestra app, pero hay unos atributos que contiene la clase .paginate()
además del .items
tenemos los siguientes:
has_next
: Verdadero si hay al menos una página más después de la actualhas_prev
: True si hay al menos una página más antes de la actualnext_num
: número de la página siguienteprev_num
: número de la página anterior
Leyendo para que sirve cada uno de estos, te das cuenta la utilidad que tienen, imagino que ya te estas imaginando como implementarlos en la plantilla, y la verdad; es que esta es su verdadera función que cumple con las características para que los usuarios puedan avanzar de página e incluso seleccionar a que página en concreto desean ir.
Procedamos ahora a agregar todo esto en nuestras plantillas, pero antes debemos habilitarlos también en nuestro archivo de rutas.py, ahora nuestro /index
y /explorar
deben verse así:
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = Publicaciones() if form.validate_on_submit(): post = Pubs(cuerpo=form.post.data, autor=current_user) bdd.session.add(post) bdd.session.commit() flash('Publicación enviada correctamente.') return redirect(url_for('index')) pagina = request.args.get('pagina', 1, type=int) posts = current_user.pubs_seguidores().paginate( pagina, app.config['POSTS_PER_PAGE'], False) pagina_sig = url_for('index', pagina=posts.next_num) \ if posts.has_next else None pagina_ant = url_for('index', pagina=posts.prev_num) \ if posts.has_prev else None return render_template('index.html', titulo='Página de inicio', form=form, posts=posts.items, pagina_sig=pagina_sig, pagina_ant=pagina_ant) @app.route('/explorar') @login_required def explorar(): pagina = request.args.get('pagina', 1, type=int) posts = Pubs.query.order_by(Pubs.timestamp.desc()).paginate(pagina, app.config['POSTS_PER_PAGE'], False) pagina_sig = url_for('explorar', pagina=posts.next_num) \ if posts.has_next else None pagina_ant = url_for('explorar', pagina=posts.prev_num) \ if posts.has_prev else None return render_template("index.html", titulo='Explorar', posts=posts.items, pagina_sig=pagina_sig, pagina_ant=pagina_ant)
En este código la clave esta cuando llamamos a la función url_for()
que lo que hace es generar el enlace a la página siguiente y la página anterior luego realizamos una validación if donde le decimos que si existe dicha página siguiente la muestre o de lo contrario devuelva un None
y lo mismo para la página anterior, esto es lo único nuevo que le agregamos a este código y ya con esto podemos ir a nuestras plantillas para habilitar la páginación.
Vamos a la plantilla index.html (recordar que esta plantilla es la misma para la ruta /index
y la ruta /explorar
y agregaremos las funciones y ahora deberíamos de tener esto:
{% extends "base.html" %} {% block contenido %} <h1>Hola, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endif %} {% for post in posts %} <p> {% include '$post.html' %} </p> {% endfor %} {% if pagina_ant %} <a href="{{ pagina_ant }}">Página anterior</a> {% endif %} {% if pagina_sig %} <a href="{{ pagina_sig }}">Página siguiente</a> {% endif %} {% endblock %}
Paginación en la página de perfil del usuario
Por último paso antes de probar todo esto, nos falta agregar la páginación a la ruta del perfil del usuario, para ello vamos a ir a nuestro archivo de rutas.py y en la ruta perfil_usuario debería verse ahora de esta forma:
@app.route('/usuario/<username>') @login_required def perfil_usuario(username): usuario = Usuario.query.filter_by(username=username).first_or_404() pagina = request.args.get('pagina', 1, type=int) posts = usuario.pubs.order_by(Pubs.timestamp.desc()).paginate(pagina, app.config['POSTS_PER_PAGE'], False) pagina_sig = url_for('perfil_usuario', username=usuario.username, pagina=posts.next_num) \ if posts.has_next else None pagina_ant = url_for('perfil_usuario', username=usuario.username, pagina=posts.prev_num) \ if posts.has_prev else None return render_template('usuarios.html', usuario=usuario, posts=posts.items, pagina_sig=pagina_sig, pagina_ant=pagina_ant)
Finalmente agregamos los enlaces de paginación en nuestra plantilla de usuarios.html:
{% 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 %} {% if usuario == current_user %} <p><a href="{{ url_for('editar_perfil') }}">Editar perfil</a></p> {% elif not current_user.siguiendo(usuario) %} <p><a href="{{ url_for('seguir', username=usuario.username) }}">Seguir</a></p> {% else %} <p><a href="{{ url_for('dejar_seguir', username=usuario.username) }}">Dejar de seguir</a></p> {% endif %} </td> </tr> </table> <hr> {% for post in posts %} {% include '$post.html' %} {% endfor %} {% if pagina_ant %} <a href="{{ pagina_ant }}">Página anterior</a> {% endif %} {% if pagina_sig %} <a href="{{ pagina_sig }}">Página siguiente</a> {% endif %} {% endblock %}
Ya tenemos la paginación en la página de perfil del usuario, ahora en adelante cuando hayan más de 20 post se creará una página nueva para mostrar los demás resultado, probemos ahora los cambios que hicimos, vamos a inyectar este código para generar muchas publicaciones a un usuario en nuestra base de datos (es importante que ya ese usuario este previamente registrado), abrimos la consola de python con el comando flask shell pero asegurate de pasar el nombre de usuario del usuario en el parámetro username:
from datetime import datetime i = 0 while(i<=100): i +=1 u = Usuario.query.filter_by(username='UsuarioAlQueQuierasAgregarlePosts').first() post = Pubs(cuerpo='Mensaje de prueba {}'.format(i), timestamp=datetime(2020, 3, 9, 13, 15), id_usuario = u.id) bdd.session.add(post) bdd.session.commit()
Con este código hemos creado 100 publicaciones para el usuario que hayas seleccionado y ahora si vamos a nuestra app y al perfil del usuario que le creamos las publicaciones con el código anterior veremos finalmente la páginacion ya que se deben mostrar esos 100 posts, pero con la condición del objeto .paginate()
para que solo muestre 3 posts por página y el resultado es el siguiente:
Como se puede apreciar en la imagen, ya tenemos activa la páginación y al hacer click sobre página siguiente podré navegar entre las distintas páginas y si navegamos a la página explorar y a la de inicio sucederá lo mismo. Con esto ya tenemos nuesta páginación completamente integrada a nuestro blog.
Si deseas eliminar las publicaciones que hicimos cuando inyectamos el código puedes borrar todas esas publicaciones inyectando el siguiente código de la misma manera que el anterior, pero asegurate de pasar el nombre de usuario del usuario en el parámetro username:
from datetime import datetime i = 0 while(i<=100): i +=1 u = Usuario.query.filter_by(username='UsuariAlQueLeBorrarásTodasSusPublicaciones').first() posts = Pubs.query.filter_by(id_usuario=u.id).delete() bdd.session.commit()
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: