Cómo crear un listado paginado en Django paso a paso con Bootstrap 5

Video thumbnail

Los listados de información son un elemento primordial en cualquier aplicación, ya sea de tipo blog, de productos, para realizar un CRUD, etc; todo esto forma parte de un misma lógica y estructura que es cargar el listado de elementos y puede que bajo un criterio en particular (que estén aprobados, paginados etc.) y vamos a ver cómo podemos crear una vista o template de listados empleando Django.

Vamos a partir de la entrada anterior en la cual ya creamos ejecutamos la migración a nuestra base de datos MySQL; por lo tanto, recuerda que para crear la migración tuvimos que tener creado un modelo.

Obtener todos los registros de la base de datos con el ORM de Django (all())

Así que; lo primero que necesitamos es poder obtener todos los elementos; para esto podemos emplear el ORM que nos provee Django que en conjunto con el modelo podemos realizar cualquier operación CRUD que en este caso nos interesa la operación de solo lectura.

Vamos a ir a nuestro archivo de vistas llamado views de nuestra aplicación firstApp y vamos a crear una nueva función llamada index:

def index(request):

Y lo primero que vamos a hacer es obtener los registros; es decir, los comentarios para eso tenemos que cargar el modelo que definimos anteriormente para eso en este mismo archivo de vista (recuerda que el archivo de models se encuentra en la misma app):

from .models import Comment

Y ahora desde la función que creamos anteriormente simplemente tenemos que referir al modelo que cargamos; es decir, la clase:

comments = Comment.objects.all()

Aquí como mencionamos, referencia nos la clase modelo llamada Comment, luego él objects seguido del método tipo CRUD que queramos emplear que en este caso como queremos obtener todos los registros tenemos que emplear el método llamado all:

Y con esto obtenemos todos los registros; ahora simplemente tenemos que pasarlos a la vista; para eso podemos emplear la función llamada tender como hicimos anteriormente con los parámetros usuales que serían:

  1. El request
  2. La página HTML o template
  3. Un diccionario con los datos que vamos a pasar a la vista.
return render(request,'index.html',{'comments' : comments })

Finalmente, el código queda de la siguiente forma:

from .models import Comment

def index(request):
comments = Comment.objects.all()
return render(request,'index.html',{'comments' : comments })

El modelo empleado luce como el siguiente:

# models.py
from django.db import models
class Comment(models.Model):
   text = models.TextField()
   date_posted = models.DateTimeField(auto_now_add=True)

El template (Vista HTML), iterar todos los registros

Luego, vamos a crear un template dentro de fistApp que recuerda que viene siendo la vista en HTML y una carpeta llamada templates que tienes que crear que vamos a llamar como index.html o el nombre que tú quieras que tenga:

firstProject\firstApp\templates\index.html

Este template, vamos a colocar una página HTML básica y simplemente una directiva llamada for, que funciona como cualquier ciclo for en cualquier lenguaje de programación.

Como puedes ver, recibe un objeto iterable que en este caso sería la colección de comentarios y luego los queremos imprimir por pantalla; para imprimir un valor por pantalla simplemente tenemos que emplear las dobles llaves:

<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Listado de comentarios</title></head>
<body>
  {% for c in comments %}
  <div>
    <p>{{ c.text }} - {{ c.date_posted }}</p>
  </div>
  {% endfor %}
</body>
</html>

Para la directiva for, tienes que emplear las etiquetas lógicas que son las que están formadas por una llave y un porcentaje.

Crear una ruta para la página de listado

Ahora ya tenemos casi todo listo, vamos a crear la ruta en el archivo llamado (dentro de la aplicación que tambien tienes que crear) urls.py dentro de la aplicación firstApp:

firstProject\firstApp\urls.py

app_name='comment'
urlpatterns = [
path('', views.index, name='index'),
]

Y recuerda cargar el archivo anterior (el de las rutas) dentro de tu proyecto; para eso:

firstProject\firstProject\urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('fisrtApp/', include('firstApp.urls'))
]

Como puedes ver, en el archivo de urls de la aplicación, simplemente registramos una ruta, sin path ''. que sea resuelta por la función de index del archivo index.py y le colocamos un nombre de manera opcional. Luego, registramos TODAS las rutas (en este caso solamente una) de la aplicación a nivel de proyecto, para eso las incluimos mediante la función include de Django.

Y con esto, si entramos en la ruta anterior:

Listado de comentarios

Ya tenemos nuestro listado.

Cómo crear un listado paginado en Django paso a paso

Cuando empezamos a crear los típicos listados, una de las primeras operaciones que tenemos que hacer es mostrar una tabla paginada; todo eso forma parte de casi cualquier aplicación. Pero claro, cuando los registros comienzan a crecer, mostrar todo en una sola página deja de ser práctico según lo que implementamos en el apartado anterior y es por eso que decidimos paginarla.

Ahí entra en juego la paginación. En esta guía te enseñaré cómo crear un listado paginado en Django, paso a paso, usando tanto funciones personalizadas como vistas genéricas (ListView), para que puedas aplicarlo en cualquier proyecto.

1. Qué es un listado paginado y por qué usarlo en Django

Un listado paginado es simplemente una forma de dividir grandes volúmenes de información en páginas más pequeñas.
Esto mejora el rendimiento del sitio y la experiencia del usuario, ya que evita cargar cientos de registros de golpe.

Ventajas de la paginación

  • Menor tiempo de carga.
  • Interfaz más limpia y navegable.
  • Menor consumo de recursos del servidor.
  • Posibilidad de usar librerías CSS (como Bootstrap) para un diseño atractivo.

Cuando probé esto por primera vez en Django, me di cuenta de que el framework ya trae todo lo necesario para hacerlo: solo hay que entender cómo funciona el Paginator y cómo pasar la información al template.

Paginación del lado de la vista y el template

Vamos a paginar en ambos lados, en un lado, el cliente o la página web, desplegar el listado, colocarlo bonito, con CSS con Bootstrap en este caso, aunque puedes emplear CSS personalizado, Bulma, Tailwind, o cualquier cosa con la que trabajes.

Vamos a aprender a cómo podemos crear un sencillo sistema de paginación bastante genérico y reutilizable para nuestro listado.

Esto es una práctica bastante común hoy en día que queremos hacer una paginación de un listado o tal de registros, como publicaciones de un post, un listado de productos para una tienda en línea etc.

2. Preparando el entorno: modelo, vista y template base

Antes de paginar, necesitamos tener algo que mostrar. En mi caso, trabajé con un modelo llamado Comment, pero puedes usar cualquiera (por ejemplo, Post o Producto) según lo implementado en el apartado anterior. Hasta aquí ya tendríamos nuestro listado funcionando. Pero todavía no está paginado.

3. Implementar la paginación con la clase Paginator

Siguiendo nuestro ejemplo vamos a crear un sencillo sistema de paginación para los comentarios. Django incluye una clase llamada Paginator que se encarga de dividir los registros en páginas. Solo hay que indicarle cuántos elementos queremos por página.

Paginación en Django

En Django tenemos una clase llamada Paginator la cual recibe dos argumento, el listado de todos los registros, y el nivel de paginación, es decir, si quieres una página con 5 registros (tamaño de la página 5) 10, etc.

Como puedes ver, también validamos que recibimos un parámetro vía get para indicar que pagina queremos ver, en este caso vamos a emplear el parámetro que viaja via get, llamado page, pero puede tener cualquier nombre; recuerda colocar alguna validación para evitar problemas y que maneje un rango correspondiente:

page_number = request.GET.get('page')

Así que, con la clase Paginator de Django le pasamos cuales es el listado que queremos paginar y el nivel de paginación; el resto del código queda igual que la entrada anterior:

# views.py
from django.core.paginator import Paginator

def index(request):
    comments = Comment.objects.all()
    paginator = Paginator(comments, 5)  # 5 elementos por página
    page_number = request.GET.get('page')
    comments_page = paginator.get_page(page_number)
    return render(request, 'index.html', {'comments_page': comments_page})

Generar los enlaces o links para la paginación

El código principal sería exactamente el mismo que el empleado anteriormente, pero ahora vamos a agregar algunos enlaces de paginación; como puedes ver, para crear un esquema reutilizable, estamos incluyendo una vista, que vienen siendo la de los enlaces de navegación:

<body>
    {% for c in comments_page %}
    <div>
        <p>
            {{ c.text|escape }}
            {{ c.date_posted|date:"D d M Y" }}
        </p>
    </div>
    {% endfor %}
 
    {% include "partials/pagination.html" with page_obj=comments_page  %}
</body>

4. Paginación con vistas genéricas (ListView)

Si prefieres un enfoque más limpio, puedes usar la vista genérica ListView, que ya trae la paginación integrada.

# views.py
from django.views.generic import ListView
from .models import Comment
class ComentariosView(ListView):
   model = Comment
   template_name = 'index.html'
   context_object_name = 'comments'
   paginate_by = 5

Y en tu urls.py:

from django.urls import path
from .views import ComentariosView
urlpatterns = [
   path('', ComentariosView.as_view(), name='comentarios'),
]

En el template:

{% for c in comments %}
 <p>{{ c.text }} - {{ c.date_posted }}</p>
{% endfor %}
{% include "partials/pagination.html" with page_obj=page_obj %}

Django se encarga automáticamente de la lógica de paginación. Siempre que podamos, deberíamos de emplear las Vistas Basadas en Clases para nuestros desarrollos, al ser más modulares y fácilmente reutilizables.

También podemos emplear el esquema de paginación empleando las vistas genéricas que nos ofrece Django; específicamente la vista genérica ListView, para las listas.

Tenemos que definir simplemente una clase que extienda de ListView y especificar algunos parámetros:

  1. El modelo con el cual vamos a trabajar
  2. La ubicación del template, que perfectamente podemos emplear el del listado anterior
  3. El nivel de paginación.

Además de esto, tenemos que crear una función llamada get_queryset la cual recibe como parámetro la página y tenemos que devolver una instancia de la página actual.

Hay muchos aspectos que puedes personalizar, en el siguiente código, puedes ver más parámetros que son de facil interpretación:

from django.views.generic import ListView
from django.core.paginator import Paginator
 
class Comentarios(ListView): 
    model = Comment 
    template_name = 'index.html' 
    paginate_by = 3
 
def get_queryset(self,pag=1): 
    comments = Comment.objects.all() 
    paginacion = Paginator(comments,'cantidad objetos') 
    if len(paginacion.page(1)) == 0: 
        return None 
    else: 
        return paginacion.page(pag)

El parámetro que vamos a pasarle al template se llama objet_list, por lo tanto, tenemos que cambiar la referencia en el template:

{% for c in object_list  %}

6. Construir los enlaces de paginación con diseño de Bootstrap  5

La vista para generar los enlaces la vamos a guardar en la siguiente ubicación como pudiste ver en el código anterior:

partials/pagination.html

Esta vista simplemente va a recibir el listado completo de registros que queremos paginar; el código en si no tiene nada de complicado, simplemente verificar si tenemos enlaces anteriores o posteriores y finalmente iterar según el rango de paginación que tengamos para generar los enlaces de paginación; en este ejemplo, usamos Bootstrap 5:

<nav aria-label="Page navigation example">
    <ul class="pagination justify-content-center mt-5">
  
          {% if page_obj.has_previous %}
              <li class="page-item">
                  <a class="page-link" href="?page={{ page_obj.previous_page_number }}">previous</a>
              </li>
          {% endif %}
  
          {% for i in page_obj.paginator.page_range %}
  
              {% if i == page_obj.number %}
                  <li class="page-item active">
                      <a class="page-link" href="#">{{ i }}</a>
                  </li>
              {% else %}
                  <li class="page-item">
                      <a class="page-link" href="?page={{ i }}">{{ i }}</a>
                  </li>
              {% endif %}
              
          {% endfor %}
  
          {% if page_obj.has_next %}
              <li class="page-item">
                  <a class="page-link" href="?page={{ page_obj.next_page_number }}">next</a>
              </li>
          {% endif %}
  
    </ul>
  </nav>

El código aunque parezca complicado si lo analizas un poco se auto explica solo, simplemente queremos construir enlaces de navegación, tenemos dos condicionales, al inicio y final del bloque simplemente para preguntar si existen enlaces previos o siguientes a mostrar; y en el medio, pintamos todos los enlaces de navegación.

Explicación de parámetros empleados

Empleamos varios parámetros en la vista para construir nuestros enlaces de navegación:

  • page_obj.has_previous: Booleano que indica si hay una página anterior.
  • page_obj.has_next: Booleano que indica si hay una página posterior.
  • page_obj.previous_page_number: Devuelve el número de la página anterior.
  • page_obj.next_page_number: Devuelve el número de la siguiente página.
  • page_obj.number: Devuelve el número de la página actual.
  • page_obj.paginator.num_pages: Devuelve el número total de páginas.

Enlaces CRUD en la tabla

Podemos definir los enlaces para el resto de las operaciones CRUD que implementaremos en otros artículos, algo como:

<a class="btn btn-sm btn-outline-primary" href="{% url 'detail' e.id %}">Go</a>

Indicando el nombre de la aplicación queda de una forma más reutilizable.

Para poder referenciar el nombre de la aplicación, debemos de definir el namespace:

mystore\mystore\urls.py

path('store/', include('elements.urls', namespace='elements')),

Para evitar un error como el siguiente:

ImproperlyConfiguredError about app_name when using namespace in include()

Y el:

mystore\elements\urls.py

app_name = 'elements'

Para evitar un error como el siguiente:

is not a registered namespace

Truncar datos de la descripción: truncatechars:'150' 

Cuando tenemos campos de texto largos y queremos recortarlos, es buena idea limitar su longitud para mejorar la presentación en la interfaz.

Para esto, podemos emplear el filtro truncatechars de Django:

<p>{{ e.description | truncatechars:'150' }}</p>

Cómo funciona:

Aquí indicamos el filtro truncatechars y especificamos la cantidad de caracteres que queremos mostrar.

Por ejemplo, para hacer una prueba rápida, podemos colocar 4 caracteres:

<p>{{ e.description | truncatechars:'4' }}</p>

En este caso, el texto no debería mostrar más de cuatro caracteres, y efectivamente se cumple.

Personalizando la longitud

Obviamente, no queremos limitar el texto a solo cuatro caracteres.
Para un ejemplo más realista, podemos establecer 150 caracteres, que suele ser suficiente para mostrar un resumen legible:

<p>{{ e.description | truncatechars:'150' }}</p>

De esta manera, el texto no será demasiado largo, evitando que aparezca como una novela, pero al mismo tiempo mostrará suficiente información.

La cantidad de caracteres se puede personalizar según tus necesidades y diseño de la página.

Conclusión

Con el filtro truncatechars, es muy sencillo recortar campos de texto largos y mostrar un resumen controlado, mejorando la legibilidad y la estética de la interfaz.

Conclusiones

La paginación en Django es una herramienta esencial para cualquier aplicación que maneje grandes volúmenes de datos.

Ya sea que uses vistas personalizadas o vistas genéricas (ListView), el patrón es el mismo: obtener los datos, dividirlos en páginas y mostrar enlaces navegables.

La paginación es un esquema que empleamos en cualquier framework moderno como hoy en día y al día día no puede faltar para poder paginar distintos tipos de desarrollo que queramos hacer, ya sea para emplearlos en una tabla, u otro elemento personalizable inclusive una RestApi.

Por supuesto, Django al manejar distintos tipos de templates, podemos definir la misma para cada tipo de template, ya sea en el uso de los ListViem como componente de vistas genéricas o una vista/template 100% personalizable por nosotros y todo girando con el queryset que viene siendo nuestra fuente de datos.

Por supuesto, Django al manejar distintos tipos de templates, podemos definir la misma para cada tipo de template, ya sea en el uso de los ListViem como componente de vistas genéricas o una vista/template 100% personalizable por nosotros. En mi caso, después de implementar esto varias veces, terminé creando una plantilla base que reutilizo en todos mis proyectos.

Te recomiendo hacerlo igual: define una estructura modular para no repetir código.

❓Preguntas frecuentes sobre la paginación en Django

¿Cuál es la forma más fácil de paginar un queryset?

Usar la clase Paginator de Django y el método get_page().

¿Qué diferencia hay entre Paginator y ListView?

Paginator se usa en vistas personalizadas (funciones), mientras que ListView ya trae la lógica integrada para paginar automáticamente.

¿Cómo integrar Bootstrap con la paginación?

Incluye el CSS de Bootstrap en tu proyecto y usa la estructura de <ul class="pagination"> como se mostró arriba.

El siguiente paso consiste es conocer sobre ¿Qué es Django Admin?.

Acepto recibir anuncios de interes sobre este Blog.

Vamos a conocer como podemos crear un listado paginado de registros con Django y Bootstrap 5 empleando la clase Paginator que nos ofrece el propio framework.

| 👤 Andrés Cruz

🇺🇸 In english