CAPÍTULO 3 · Cada pieza encuentra su responsabilidad
Una tabla nueva no debería inventar otra paginación
La biblioteca empieza a dar resultados: las vistas pequeñas permiten reconocer lo propio de cada módulo. Pero la consistencia se pierde si una tabla devuelve results, otra inventa items y una tercera descarga todo para recortarlo en el navegador. El frontend termina adaptándose a diferencias que no responden a ninguna necesidad del negocio.
Acordamos que la paginación se resuelva en repoGenerico. El consumidor elige la base apropiada y aporta el queryset, la búsqueda y los campos de orden. El núcleo conserva límites, validación y formato de respuesta. Cuando usamos DataTables, su protocolo se adapta en ese lugar compartido. El conteo debe partir siempre de los registros que el usuario puede consultar.
Este acuerdo también orienta el trabajo con asistentes de código: antes de generar otra solución, deben leer la existente y reutilizarla. Ya tenemos respuestas acotadas y predecibles. Sin embargo, limitar el número de filas no garantiza que obtenerlas sea eficiente. Una página pequeña todavía puede ejecutar demasiadas consultas para representar sus relaciones.
Leer esta parte dentro del relato completo →DEL RELATO A LA PRÁCTICA
Vamos a desarrollar esta decisión con su mapa, explicación, código y ejercicio.
MIRA · CONECTA · COMPRENDE
Una ventana sobre un catálogo
Las flechas resumen el recorrido conceptual. Los controles y dependencias se detallan debajo.
Selecciona una pieza para destacar su responsabilidad.
Leer la explicación del mapa
- Parámetros. Busqueda, página y orden
- Validación. Limita rango y campos
- Consulta. Filtra antes de paginar
- Página. Muestra un subconjunto ordenado
01 / EL PROBLEMA
¿Para qué sirve?
Si cada pantalla inventa su paginación, los clientes reciben formatos, límites y errores distintos. El equipo necesita un punto común que también guíe el código generado con IA.
02 / COMPRENDER
El concepto, explicado
La consulta paginada es una funcionalidad de repoGenerico, no una función nueva que deba copiarse a cada app. Primero se elige la base: BaseListView para DataTables o presentación HTML/JSON, BaseListQueryView para JSON paginado simple y BaseListAllView solo para conjuntos completos acotados.
El flujo común valida parámetros, obtiene el queryset autorizado, cuenta el conjunto permitido, aplica búsqueda, cuenta coincidencias, ordena, pagina y serializa únicamente la página. La aplicación aporta modelo, serializer, permisos, búsqueda y campos; no vuelve a escribir Paginator en cada Query.
El máximo compartido examinado es 100. El adaptador convencional usa page y per_page. DataTables usa start, length, draw y search[value], y se identifica mediante X-DataTables-Request: true. La columna enviada por el navegador se resuelve con datatables_columns, nunca se acepta como expresión ORM arbitraria.
recordsTotal cuenta el conjunto autorizado antes de buscar y recordsFiltered las coincidencias después de buscar. Si cuentas todos los clientes antes de filtrar autorización, el número puede revelar información aunque las filas no se muestren.
La versión de la academia usa un contrato JSON reducido y devuelve 400 ante orden inválido; la base de tablas examinada recurre al orden por defecto. No son contratos idénticos. Para un proyecto concreto acuerda una sola política y pruébala: no cambies de comportamiento por lo que proponga una herramienta de generación.
El orden necesita un desempate único cuando hay valores repetidos. Añadir id o pk en la base es una mejora explícita que debe probarse; no debe asumirse que ya está en todas las implementaciones. Incluso un orden estable puede cambiar entre peticiones si llegan datos nuevos; si se requiere una vista consistente, evalúa snapshot o paginación por cursor.
get_page normaliza páginas fuera del rango. Para DataTables, convertir start a número de página supone offsets alineados con length: 20/10 corresponde a página 3. Un start de 15 no representa lo mismo que la página 2 de tamaño 10. Decide rechazar offsets no alineados o usar slicing validado en el adaptador común, sin resolverlo de forma distinta en cada tabla.
Elegir y conservar el contrato
| Modo | Entrada | Salida y uso |
|---|---|---|
| JSON simple del equipo | page, per_page, search, ordering | results, total_count, current_page, total_pages y navegación |
| DataTables | Header dedicado; start, length, draw, search[value], columnas | draw, recordsTotal, recordsFiltered, data |
| HTML de BaseListView | Parámetros de lista + template_name | Template con objects paginados y contexto |
| Lista completa acotada | Orden permitido | success, results, count; no es paginación |
| JSON de esta academia | page, per_page, search, ordering | success, results, total_count, current_page, total_pages |
DECISIONES DE LA BIBLIOTECA COMPARTIDA
Un ejemplo completo de la adaptación DataTables
Solicitud: header X-DataTables-Request: true, start=20, length=10, draw=4, search[value]=norte. La subclase permite nombre y mapea la columna 0 a nombre.
Si el usuario puede acceder a 80 ubicaciones y 23 coinciden con norte, la respuesta tiene recordsTotal=80, recordsFiltered=23, draw=4 y data con los últimos 3 registros. Nunca se serializan los 80 para luego recortar en JavaScript.
length=-1 significa “todos” para algunos clientes, pero viola el límite acordado de esta base. La tabla debe ofrecer tamaños permitidos y el servidor debe validar también solicitudes fabricadas.
Acuerdo del equipo para trabajar con asistentes de código
Antes de generar una Query, leer repoGenerico y una vista existente del mismo tipo. Pedir explícitamente reutilizar la base y sus hooks, conservar las claves JSON, la política de errores y el máximo de página.
No aceptar una nueva función paginate, otra envoltura de respuesta o una consulta sin límites solo porque funciona en una pantalla. Si falta una capacidad, proponer la extensión en el módulo común con una prueba de regresión.
La revisión compara comportamiento, no solo cantidad de líneas. Comprueba permisos, alcance, contadores, orden y errores además de la herencia. El archivo AGENTS.md del proyecto recoge estas reglas para futuras modificaciones.
03 / CONSTRUIR
Implementación paso a paso
- Identificar si la pantalla necesita JSON simple, DataTables o una lista acotada completa.
- Heredar la base correspondiente y declarar modelo, serializer y permisos.
- Restringir get_queryset antes de contar; optimizar relaciones necesarias.
- Personalizar búsqueda y orden mediante hooks, sin copiar get.
- Verificar máximo de página, valores inválidos, conjunto vacío, orden y contadores.
- Cambiar el núcleo compartido solo con una prueba que cubra sus consumidores.
04 / APLICAR
Ejemplo en SensorHub
"""Vistas genéricas sobre ORM, para la academia pública."""
from django.core.paginator import Paginator
from rest_framework.views import APIView
from rest_framework.response import Response
class BaseListQueryView(APIView):
model=None
serializer_class=None
allowed_ordering=('number',)
def get_queryset(self):return self.model.objects.all()
def apply_search(self,queryset,search):return queryset
def get(self,request):
try:
page=int(request.query_params.get('page',1))
size=int(request.query_params.get('per_page',10))
except (TypeError,ValueError):return Response({'success':False,'error':'Paginación inválida.'},status=400)
if page<1 or not 1<=size<=100:return Response({'success':False,'error':'page >= 1; per_page entre 1 y 100.'},status=400)
ordering=request.query_params.get('ordering','number')
if ordering.lstrip('-') not in self.allowed_ordering:return Response({'success':False,'error':'Orden no permitido.'},status=400)
queryset=self.apply_search(self.get_queryset(),request.query_params.get('search','')[:200]).order_by(ordering)
paginator=Paginator(queryset,size)
result=paginator.get_page(page)
return Response({'success':True,'results':self.serializer_class(result.object_list,many=True).data,'total_count':paginator.count,'current_page':result.number,'total_pages':paginator.num_pages})
# Consumidor REAL: academy/views/queries/catalog.py
from django.db.models import Q
from repoGenerico.views_base import BaseListQueryView
from academy.models import Lesson
from academy.serializers import LessonSerializer
class CatalogQuery(BaseListQueryView):
model=Lesson
serializer_class=LessonSerializer
def apply_search(self,queryset,search):
return queryset.filter(Q(title__icontains=search)|Q(concept__icontains=search)) if search else queryset
Cómo leer el ejemplo
- Este código sí existe en la academia y sirve /api/catalogo/.
- CatalogQuery hereda get y solo implementa la búsqueda particular.
- La validación y Paginator viven en repoGenerico; cada app no crea otra variante.
- Esta base es JSON simple: no afirma implementar DataTables ni toda la familia CRUD.
Error habitual
Duplicar la paginación en una Query nueva o cambiar el contrato de respuesta silenciosamente rompe la consistencia aunque el endpoint parezca funcionar.
05 / PRACTICAR
Tu turno
Añade una segunda consulta que busque por otro campo sin declarar get ni importar Paginator. Prueba en ambas per_page=0, per_page=101, page=no y búsqueda sin coincidencias.
Ver solución orientativa
Heredar BaseListQueryView y sobreescribir solo apply_search en esta academia. El handler compartido devuelve 400 para las tres entradas inválidas y una página vacía para la búsqueda sin coincidencias. En la biblioteca completa el hook equivalente se llama apply_search_filters: usa la interfaz de la base instalada, no mezcles nombres.
06 / VERIFICAR
Cómo sabes que funciona
- Las subclases no duplican Paginator ni el handler.
- El contrato de cada modo está documentado.
- Los conteos no atraviesan el alcance autorizado.
- Una misma prueba de parámetros se aplica a varios consumidores.
Para profundizar
Documentación oficial para comprobar configuración, permisos y condiciones vigentes. Revisión del tema: 2026-10-12.
CONTINUAR LA EXPERIENCIA
El siguiente paso del recorrido:
ORM eficiente y consultas N+1 →Continuar en modo relato