Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 23 de 45

ListView y DetailView

Dos clases que sustituyen las dos vistas que ya tienes, la paginación que sale sola, y los nombres que hay que adivinar.

⏱ 15 min de lectura

Las dos vistas que escribiste en la lección 16 hacen lo que hacen el noventa por ciento de las páginas de cualquier sitio: enseñar una lista y enseñar un elemento. Django trae las dos hechas.

Lo interesante no es que ahorren líneas, que también. Es que traen resuelto lo que se te olvida: la paginación, el 404 cuando no existe, el filtrado por parámetros de la dirección. Y a cambio piden aprenderse unos cuantos nombres que no están escritos en ninguna parte de tu código, que es la pega de la lección anterior en su forma más concreta.

ListView#

La versión más corta que existe:

contactos/views_clase.py
from django.views.generic import ListView

from .models import Contacto


class ListaMinima(ListView):
        model = Contacto

Una línea de cuerpo. Con eso hace la consulta, pagina si se lo pides, prepara el contexto y elige la plantilla. Pero:

Salida en pantalla
HTTP 500
TemplateDoesNotExist: contactos/contacto_list.html

Falta la plantilla, y ahí está el primer nombre inventado. ListView busca aplicacion/modelo_list.html, todo en minúsculas, con el nombre del modelo pegado al _list. No lo has escrito en ningún sitio: sale de model.

Con la plantilla puesta:

Salida en pantalla
Claves: object_list=5 contacto_list=5

Y ahí está el segundo: los objetos llegan al contexto con dos nombres, object_list y contacto_list, el segundo formado igual que la plantilla. El genérico sirve para escribir plantillas reutilizables y el específico se lee mejor.

Una versión completa#

contactos/views_clase.py
class ListaCompleta(ListView):
        model = Contacto
    template_name = 'contactos/lista_cbv.html'
    context_object_name = 'contactos'
    paginate_by = 2

    def get_queryset(self):
                consulta = super().get_queryset().select_related('grupo')
        buscado = self.request.GET.get('q')
        if buscado:
                        consulta = consulta.filter(nombre__icontains=buscado)
        return consulta

    def get_context_data(self, **kwargs):
                contexto = super().get_context_data(**kwargs)
        contexto['buscado'] = self.request.GET.get('q', '')
        return contexto

Cuatro atributos y dos métodos, y ya es una lista paginada con buscador. Los dos métodos son los que se sobrescriben siempre:

get_queryset decide qué objetos. Empieza llamando al padre, que devuelve model.objects.all(), y encadena lo tuyo. Es el sitio del select_related, del filter y del order_by.

get_context_data añade lo que no son los objetos: el término buscado, los grupos para un desplegable, lo que haga falta. Con el super() de la lección anterior, que no es opcional.

Y fíjate en self.request. Dentro de una vista de clase, la petición está en self, no en un parámetro. Con ella se llega a self.request.GET, self.request.user y self.kwargs, que es donde están los trozos capturados de la dirección.

Paginación#

paginate_by mete tres cosas en el contexto: paginator, page_obj y is_paginated. Medidas:

Salida en pantalla
PAGINA: 1 de 3  total=5  hay_paginas=True
ANT: no  SIG: 2
NOMBRES: Ana Ruiz; Luis Pardo;

PAGINA: 2 de 3  total=5  hay_paginas=True
ANT: 1  SIG: 3
NOMBRES: Por id; Sin grupo;

La página se elige con un parámetro en la dirección, ?page=2, y una que no existe da 404:

Salida en pantalla
?page=99  ->  404

Lo que casi siempre se escribe igual, y conviene tenerlo en un trozo incluido de los de la lección 18:

contactos/templates/contactos/_paginacion.html
{% if is_paginated %}
<nav class="paginacion">
  {% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}">Anterior</a>
  {% endif %}

  <span>Página {{ page_obj.number }} de {{ paginator.num_pages }}</span>

  {% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">Siguiente</a>
  {% endif %}
</nav>
{% endif %}

DetailView#

python
class DetalleCbv(DetailView):
        model = Contacto
python
path('cbv/<int:pk>/', views_clase.DetalleCbv.as_view(), name='cbv-detalle'),
Salida en pantalla
Claves: object=Ana Ruiz  contacto=Ana Ruiz

Busca contactos/contacto_detail.html y deja el objeto con los dos nombres, igual que antes. El get_object_or_404 está dentro: si no existe, 404.

Lo único que hay que saber es de dónde saca el objeto, porque depende de lo que capture la ruta:

En la rutaQué hace
<int:pk>Busca por clave primaria
<slug:slug>Busca por el campo slug del modelo
<slug:slug> con slug_field = 'url'Busca por el campo url

Si la ruta no captura ni pk ni slug, la vista lanza una excepción explicando que no sabe qué buscar. Y si hace falta otra cosa —el objeto de la persona que ha entrado, por ejemplo—, se sobrescribe get_object.

DetailView también tiene get_queryset, y ahí es donde va el select_related y, sobre todo, el filtro de seguridad:

python
def get_queryset(self):
        return super().get_queryset().filter(propietario=self.request.user)

Con eso, pedir la ficha de otra persona da 404 en lugar de enseñarla. Es el patrón correcto y vuelve en el bloque de usuarios.