Tutorial de Django Parte 6: Listas genéricas y vistas de detalle
Este tutorial extiende nuestro sitio web de la BibliotecaLocal, añadiendo páginas de listas y detalles de libros y autores. Aquí aprenderemos sobre vistas genéricas basadas en clases, y mostraremos cómo éstas pueden reducir la cantidad de código que tienes que escribir para casos de uso común. También entraremos en el manejo de URL en gran detalle, mostrando cómo realizar un emparejamiento de patrones básico.
Requisitos previos: |
Completa todos los tópicos anteriores del tutorial, incluyendo Tutorial de Django Parte 5: Creación de tu página de inicio. |
---|---|
Objetivo: |
Entender dónde y cómo usar las vistas genéricas basadas en clases, y cómo extraer patrones desde las URLs y enviar la información a las vistas. |
Visión General
En este tutorial vamos a completar la primera versión del sitio web BibliotecaLocal añadiendo páginas de lista y detalle para libros y autores (o para ser más precisos, te mostraremos cómo implementar las páginas de libros, ¡y te dejaremos crear las páginas de autores por tí mismo!)
El proceso es similar al de creación de la página índice, que mostramos en el tutorial anterior. Aquí también necesitaremos crear mapeos URL, vistas y plantillas. La principal diferencia es que para las páginas de detalle tendremos el reto adicional de extraer información desde patrones en las URLs y pasarla a la vista. Para estas páginas vamos a mostrar un tipo de vista completamente diferente: vistas genéricas de lista y detalle basadas en clases. Estas pueden reducir significativamente la cantidad de código requerido para las vistas, haciéndolas más fáciles de escribir y mantener.
La parte final del tutorial mostrará cómo paginar tu información al usar vistas de lista genéricas basadas en clases.
Página de lista de libros
La página de lista de libros desplegará una lista con todos los registros de libros disponibles en la página, a la cual se accede usando la URL: catalog/books/
. La página desplegará un título y un autor para cada registro, siendo el título un hipervículo a la página de detalle de libro relacionada. La página tendrá la misma estructura y navegación que todas las demás páginas en el sitio, y por tanto podemos extender la plantilla base (base_generic.html) que creamos en el tutorial anterior.
Mapeo URL
Abre /catalog/urls.py y copia allí la línea que se muestra abajo en negrita. De manera muy similar al mapeo de nuestro índice, esta función path()
define un patrón que se comparará con la URL ('books/'), una función de vista a la que se llamará si la URL coincide (views.BookListView.as_view()
) y un nombre para esta asignación concreta.
urlpatterns = [
path('', views.index, name='index'),
path('books/', views.BookListView.as_view(), name='books'),
]
Como se discutió en el tutorial anterior, la URL debió previamente haber coincidido con /catalog
, de modo que la vista será llamada en realidad para la URL: /catalog/books/
.
La función de vista tiene un formato diferente al anterior — eso es porque esta vista será en realidad implementada como una clase. Heredaremos desde una función de vista genérica existente que ya hace la mayoría de lo que queremos que esta función de vista haga, en lugar de escribir la nuestra propia desde el inicio.
Para las vistas basadas en clases de Django, accedemos a una función de vista apropiada llamando al método de clase as_view()
. Esto hace todo el trabajo de crear una instancia de la clase, y asegurarse de que los métodos controladores correctos sean llamados para las solicitudes HTTP entrantes.
Vista (basada en clases)
Podríamos fácilmente escribir la vista de lista de libros como una función regular (tal como nuestra vista de índice anterior), la cual consultaría a la base de datos por todos los libros, y luego llamar a render()
para pasar dicha lista a una plantilla específica. Sin embargo, en lugar de eso usaremos una vista de lista genérica basada en clases (ListView) — una clase que hereda desde una vista existente. Debido a que la vista genérica ya implementa la mayoría de la funcionalidad que necesitamos, y sigue la práctica adecuada de Django, seremos capaces de crear una vista de lista más robusta con menos código, menos repetición, y por último menos mantenimiento.
Abre catalog/views.py, y copia el siguiente código al final del archivo:
from django.views import generic
class BookListView(generic.ListView):
model = Book
¡Eso es todo! La vista genérica consultará a la base de datos para obtener todos los registros del modelo especificado (Book
) y renderizará una plantilla ubicada en /locallibrary/catalog/templates/catalog/book_list.html (que crearemos más abajo). Dentro de la plantilla puedes acceder a la lista de libros mediante la variable de plantilla llamada object_list
O book_list
(esto es, genéricamente, "nombre_del_modelo_list
").
Nota: Esta ruta complicada para la ubicación de la plantilla no es un error de digitación — las vistas genéricas buscan plantillas en /application_name/the_model_name_list.html
(catalog/book_list.html
en este caso) dentro del directorio de la aplicación /application_name/templates/
(/catalog/templates/
).
Puedes añadir atributos para cambiar el comportamiento por defecto de arriba. Por ejemplo, puedes especificar otro archivo de plantilla si necesitas tener múltiples vistas que usen el mismo modelo, o puedes querer usar un nombre diferente de variable de plantilla si book_list
no resulta intuitivo para tu caso particular de uso de plantilla. Posiblemente la variación más útil es cambiar/filtrar el conjunto de resultados que se devuelve, así, en lugar de listar todos los libros podrías listar los 5 libros más leídos por otros usuarios.
class BookListView(generic.ListView):
model = Book
context_object_name = 'my_book_list' # su propio nombre para la lista como variable de plantilla
queryset = Book.objects.filter(title__icontains='war')[:5] # Consigue 5 libros que contengan el título de guerra.
template_name = 'books/my_arbitrary_template_name_list.html' # Especifique su propio nombre/ubicación de plantilla
Sobreescribiendo métodos en vistas basadas en clases
Si bien no necesitamos hacerlo aquí, puedes también sobreescribir algunos de los métodos de clase.
Por ejemplo, podemos sobreescribir el método get_queryset()
para cambiar la lista de registros devueltos. Esto es más flexible que simplemente ajustar el atributo queryset
como lo hicimos en el fragmento de código anterior (aunque en este caso no existe un beneficio real):
class BookListView(generic.ListView):
model = Book
def get_queryset(self):
return Book.objects.filter(title__icontains='war')[:5] # Consigue 5 libros que contengan el título de guerra.
Podríamos también sobreescribir get_context_data()
con el objeto de pasar variables de contexto adicionales a la plantilla (ej. la lista de libros se pasa por defecto). El fragmento de abajo muestra cómo añadir una variable llamada "some_data" al contexto (la misma estaría entonces disponible como una variable de plantilla).
class BookListView(generic.ListView):
model = Book
def get_context_data(self, **kwargs):
# Llame primero a la implementación base para obtener un contexto.
context = super(BookListView, self).get_context_data(**kwargs)
# Obtenga el blog del id y agréguelo al contexto.
context['some_data'] = 'Estos son solo algunos datos'
return context
Cuando se hace esto es importante seguir este patrón:
- Primero obtener el contexto existente desde nuestra superclase.
- Luego añadir tu nueva información de contexto.
- Luego devolver el nuevo contexto (actualizado).
Nota: Revisa Vistas genéricas integradas basadas en clases (documentación de Django) para muchos más ejemplos de lo que puedes hacer.
Creando la plantilla de Vista de Lista
Crea el archivo HTML /locallibrary/catalog/templates/catalog/book_list.html y copia allí el texto de abajo. Como se discutió antes, este es el archivo de plantilla por defecto esperado por la vista de lista genérica basada en clases (para un modelo llamado Book
en una aplicación llamada catalog
).
Las plantillas para las vistas genéricas son como cualquier otra plantilla (si bien el contexto/información enviada a la plantilla puede variar, por supuesto). Como con nuestra plantilla índice, extendemos nuestra plantilla base en la primera línea, y luego reemplazamos el bloque llamado content
.
{% extends "base_generic.html" %}
{% block content %}
<h1>Lista de libros</h1>
{% if book_list %}
<ul>
{% for book in book_list %}
<li>
<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
</li>
{% endfor %}
</ul>
{% else %}
<p>No hay libros en la biblioteca.</p>
{% endif %}
{% endblock %}
La vista envía el contexto (lista de libros) por defecto como object_list
y book_list
(son áliases, cualquiera de ellos funcionará).
Ejecución condicional
Usamos las etiquetas de plantilla if
, else
y endif
para revisar si la book_list
ha sido definida y no está vacía. Si book_list
está vacía, entonces la cláusula else
despliega un texto que explica que no existen libros a listar. Si Book_list
no está vacía, entonces iteramos a través de la lista de libros.
{% if book_list %}
<!-- código aquí para listar los libros -->
{% else %}
<p>No hay libros en la biblioteca.</p>
{% endif %}
La condicional de arriba solo revisa un caso, pero puedes revisar condiciones adicionales usando la etiqueta de plantilla elif
(ej. {% elif var2 %}
). Para mayor información sobre operadores condicionales mira: if, ifequal/ifnotequal, y ifchanged en Etiquetas y filtros de plantilla integrados (Django Docs).
Lazos For
La plantilla usa las etiquetas de plantilla for y endfor
para iterar a través de la lista de libros, como se muestra abajo. Cada iteración asigna a la variable de plantilla book
la información para el ítem actual de la lista.
{% for book in book_list %}
<li> <!-- código aquí obtener información de cada elemento del libro --> </li>
{% endfor %}
Si bien no se usan aquí, Django creará otras variables dentro del lazo que puedes usar para monitorear la iteración. Por ejemplo, puedes revisar la variable forloop.last
para realizar un procesamiento condicional la última vez que el lazo se ejecuta.
Accediendo a las variables
El código dentro del lazo crea un ítem de lista para cada libro, que muestra tanto el título (como un enlace hacia la vista de detalle que aún no creamos) como el autor.
<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
Accedemos a los campos del registro del libro asociado usando la "notación de punto" (ej. book.title
y book.author
), donde el texto que sigue a la palabra book
es el nombre del campo (como se definió en el modelo).
También podemos invocar funciones en el modelo desde dentro de nuestra plantilla — en este caso invocamos a book.get_absolute_url()
para obtener una URL que se podría usar para desplegar la página de detalle relacionada. Esto funciona siempre y cuando la función no tenga ningún argumento (¡no hay forma de enviar argumentos!).
Nota: Debemos tener cuidado de los "efectos secundarios" al invocar funciones en las plantillas. Aquí solo obtenemos una URL para desplegar, pero una función puede hacer casi cualquier cosa — ¡no quisieramos borrar nuestra base de datos (por ejemplo) solo por renderizar nuestra plantilla!
Actualizar la plantilla base
Abre la plantilla base (/locallibrary/catalog/templates/base_generic.html) e inserta {% url 'books' %} en el enlace URL para Todos los libros, como se muestra abajo. Esto habilitará el enlace en todas las páginas (podemos ubicar esto exitosamente en su lugar ahora que hemos creado el mapeo url "books").
<li><a href="{% url 'index' %}">Inicio</a></li>
<li><a href="{% url 'books' %}">Todos los libros</a></li>
<li><a href="">Todos los autores</a></li>
¿Cómo se ve?
Aún no podrás ver la lista de libros, porque aún nos falta una dependencia — el mapeo URL para las páginas de detalle de libros, que se necesita para crear los hipervínculos a los libros individuales. Mostraremos tanto la lista de libros como las vistas de detalle después de la siguiente sección.
Página de detalle de libros
La página de detalle de libro desplegará información sobre un libro específico, a la que se accede usando la URL catalog/book/<id>
(donde <id>
es la clave primaria para el libro). Además de los campos en el modelo Book
(autor, resumen, ISBN, idioma, y género), listaremos también los detalles de las copias disponibles (BookInstances
) incluyendo su estado, fecha de devolución esperada, edición e id. Esto permitirá a nuestros lectores no solo saber sobre el libro en sí, sino también confirmar si está disponible o cuándo lo estará.
Mapeo URL
Abre /catalos/urls.py y añade el mapeador URL 'book-detail' que se muestra abajo en negrita. Esta función path()
define un patrón, una vista de detalle genérica basada en clases asociada, y un nombre.
urlpatterns = [
path('', views.index, name='index'),
path('books/', views.BookListView.as_view(), name='books'),
path('book/<int:pk>', views.BookDetailView.as_view(), name='book-detail'),
]
Para la ruta book-detail el patrón URL utiliza una sintaxis especial para capturar el id específico del libro que queremos ver. La sintaxis es muy sencilla: los corchetes angulares definen la parte de la URL a capturar, encerrando el nombre de la variable que la vista puede utilizar para acceder a los datos capturados. Por ejemplo, <algo>, capturará el patrón marcado y pasará el valor a la vista como una variable "algo". Opcionalmente puedes añadir al nombre de la variable una etiqueta que defina el tipo de datos (int, str, slug, uuid, path).
En este caso utilizamos '<int:pk>'
para capturar el id del libro, que debe ser una cadena con un formato especial y pasarlo a la vista como un parámetro llamado pk
(abreviatura de primary key). Este es el id que se está utilizando para almacenar el libro de forma única en la base de datos, tal y como se define en el Modelo de Libros.
Nota: Como ya se discutió antes, nuestra URL coincidente es en realidad catalog/book/<digits>
(como estamos en la aplicación catalog, se asume /catalog/
).
Advertencia: La vista de detalle genérica basada en clases espera que se le envíe un parámetro llamado pk. Si estás escribiendo tu propia vista de función, puedes usar el nombre de parámetro que quieras, o incluso enviar la información como un argumento sin nombre.
Introducción avanzada a path/expresiones regulares
Nota: No necesitarás esta sección para completar el tutorial. La proporcionamos porque conocer esta opción es probable que sea útil en tu futuro centrado en Django.
La combinación de patrones proporcionada por path()
es simple y útil para los casos (muy comunes) en los que sólo desea capturar cualquier cadena o entero. Si necesita un filtrado más refinado (por ejemplo, filtrar sólo cadenas que tengan un cierto número de caracteres) puede utilizar el método re_path().
Este método se utiliza igual que path()
, salvo que permite especificar un patrón utilizando una Expresión regular. Por ejemplo, la ruta anterior podría haberse escrito como se muestra a continuación:
re_path(r'^book/(?P<pk>\d+)$', views.BookDetailView.as_view(), name='book-detail'),
Las expresiones regulares son una herramienta increíblemente potente para la creación de patrones. Francamente, son poco intuitivas y pueden intimidar a los principiantes. A continuación encontrará un breve manual.
Lo primero que hay que saber es que las expresiones regulares deberían ser declaradas normalmente usando la sintaxis de literal de cadena sin procesar (esto es, están encerradas así: r'<tu expresión regular va aquí>').
Las partes principales de la sintaxis que necesitarás saber para declarar las coincidencias de patrones son:
Símbolo | Significado |
---|---|
^ | Coincide con el inicio del texto |
$ | Coincide con el fin del texto |
\d | Coincide con un dígito (0, 1, 2, ... 9) |
\w |
Concide con un caracter de palabra, ej. cualquier caracter del alfabeto en mayúscula o minúscula, dígito o guión bajo (_) |
+ |
Concide con uno o más caracteres del precedente. Por ejemplo, para
coincidir con uno o más dígitos usarías |
* |
Concide con cero o más caracteres del precedente. Por ejemplo, para
coincidir con nada o una palabra podrías usar |
( ) |
Captura la parte del patrón dentro de los paréntesis. Todos los valores capturados serán enviados a la vista como parámetros sin nombre (si se captura múltiples patrones, los parámetros asociados serán enviados en el órden en que fueron declaradas las capturas). |
(?P<name>...) |
Captura el patrón (indicado por ...) como una variable con nombre (en este caso "name"). Los valores capturados se envían a la vista con el nombre especificado. Tu vista debe, por tanto, ¡declarar un argumento con el mismo nombre! |
[ ] |
Concide con un caracter del conjunto. Por ejemplo, [abc] coincidirá con 'a' o 'b' o 'c'. [-\w] coincidrá con el caracter '-' o con cualquier letra. |
¡La mayoría de los caracteres restantes se pueden tomar literalmente!
Consideremos algunos ejemplos reales de patrones:
Patrón | Descripción |
---|---|
r'^book/(?P<pk>\d+)$' |
Esta es la RE usada en nuestro mapeador url. Concide con una cadena
que tiene También captura todos los dígitos (?P<pk>\d+) y los envía a la vista como un parámetro llamado 'pk'. ¡Los valores capturados siempre se envían como cadena!
Por ejemplo, esto coincidiría con |
r'^book/(\d+)$' |
Esta expresión coincide con las mismas URLs que el caso anterior. La información capturada se enviaría a la vista como un argumento sin nombre. |
r'^book/(?P<stub>[-\w]+)$' |
Esta expresión coincide con una cadena que tiene
Este es un patrón bastante típico para un "talón". Estos talones
representan claves primarias en URLs amigables basadas en palabras
para la información. Podrías usar un talón si quisieras que la URL de
un libro sea más informativa. Por ejemplo,
|
Puedes capturar múltiples patrones en una sola comparación, y por tanto codificar bastantes datos diferentes en una URL.
Nota: Como reto, considera cómo podrías codificar una url para listar todos los libros publicados en un año, mes y día en particular, y la RE que podría usarse para la comparación.
Enviado opciones adicionales en tus mapeos URL
Una característica que no hemos utilizado aquí, pero que puede resultarte valiosa, es que puedes pasar un diccionario que contenga opciones adicionales a la vista (utilizando el tercer argumento sin nombre de la función path()
). Este enfoque puede ser útil si quieres usar la misma vista para múltiples recursos, y pasar datos para configurar su comportamiento en cada caso.
Por ejemplo, en base al path mostrado a continuación, para una petición a /myurl/halibut/
Django llamará a views.my_view(request, fish='halibut', my_template_name='some_path')
.
path('myurl/<fish>', views.my_view, {'my_template_name': 'some_path'}, name='aurl'),
Nota: Tanto las opciones extra como los patrones capturados con nombre se envían a la vista como argumentos con nombre. Si usas el mismo nombre tanto para un patrón capturado como para una opción extra, solo el valor del patrón capturado será enviado a la vista (el valor especificado para la opción extra será eliminado).
Vista (basada en clases)
Abre catalog/views.py y copia el siguiente código al final del archivo:
class BookDetailView(generic.DetailView):
model = Book
¡Eso es todo! Lo único que necesitas hacer ahora es crear una plantilla llamada /locallibrary/catalog/templates/catalog/book_detail.html, y la vista enviará la información en la base de datos para el registro del libro específico, extraído por el mapeador URL. Dentro de la plantilla puedes acceder a la lista de libros mediante la variable de plantilla llamada object
o book
(esto es, genéricamente, "el_nombre_del_modelo
").
Si lo necesitas puedes cambiar la plantilla usada y el nombre del objeto de contexto usado para referenciar al libro en la plantilla. Puedes también sobreescribir métodos para, por ejemplo, añadir información adicional al contexto.
¿Qué sucede si el registro no existe?
Si un registro solicitado no existe, la vista de detalle genérica basada en clases lanzará automáticamente por tí una excepción de tipo Http404 — en producción, esto desplegará automáticamente una página apropiada de "recurso no encontrado", que puedes personalizar si lo deseas.
Solo para darte una idea sobre cómo funciona esto, el fragmento de código de abajo demuestra cómo implementarías la vista basada en clases como una función, si no estuvieras usando la vista de detalle genérica basada en clases.
def book_detail_view(request,pk):
try:
book_id=Book.objects.get(pk=pk)
except Book.DoesNotExist:
raise Http404("Book does not exist")
#book_id=get_object_or_404(Book, pk=pk)
return render(
request,
'catalog/book_detail.html',
context={'book':book_id,}
)
Primero, la vista intenta recuperar el registro del libro específico desde el modelo. Si esto falla, la vista debería lanzar una excepción de tipo Http404
para indicar que el libro "no se encuentra". El último paso es, como de costumbre, llamar a render()
con el nombre de la plantilla y la información del libro en el parámetro context
(como un diccionario).
Nota: get_object_or_404()
(que se muestra comentado arriba), es un atajo conveniente para lanzar una excepción de tipo Http404
si el registro no se encuentra.
Creando la plantilla de Vista de Detalle
Crea el archivo HTML /locallibrary/catalog/templates/catalog/book_detail.html y ponle el contenido de abajo. Como se discutió antes, este es el nombre de archivo por defecto esperado por la vista de detalle genérica basada en clases (para un modelo llamado Book
en una aplicación llamada catalog
).
{% extends "base_generic.html" %}
{% block content %}
<h1>Title: {{ book.title }}</h1>
<p><strong>Autor:</strong> <a href="">{{ book.author }}</a></p> <!-- enlace de detalle del autor aún no definido -->
<p><strong>Resumen:</strong> {{ book.summary }}</p>
<p><strong>ISBN:</strong> {{ book.isbn }}</p>
<p><strong>Idioma:</strong> {{ book.language }}</p>
<p><strong>Genero:</strong> {% for genre in book.genre.all %} {{ genre }}{% if not forloop.last %}, {% endif %}{% endfor %}</p>
<div style="margin-left:20px;margin-top:20px">
<h4>Copias</h4>
{% for copy in book.bookinstance_set.all %}
<hr>
<p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
{% if copy.status != 'a' %}<p><strong>Pendiente de devolución:</strong> {{copy.due_back}}</p>{% endif %}
<p><strong>Imprimir:</strong> {{copy.imprint}}</p>
<p class="text-muted"><strong>Id:</strong> {{copy.id}}</p>
{% endfor %}
</div>
{% endblock %}
Nota: El enlace author
en la plantilla de arriba tiene una URL vacía porque no hemos creado aún una página de detalle de autor. Una vez que esta exista, deberías actualizar la URL así:
<a href="{% url 'author-detail' book.author.pk %}"><strong>{{ book.author }}</strong></a>
Aunque es un poco más larga, casi todo lo que existe en esta plantilla se ha descrito previamente:
- Extendemos nuestra plantilla base y sobreescribimos el bloque "content"
- Usamos procesamiento condicional para determinar si desplegar o no contenidos específicos
- Usamos lazos
for
para iterar a través de listas de objetos. - Accedemos a los campos de contexto usando la notación de puntos (como hemos usado la vista de detalle genérica, el contexto se llama
book
; podríamos también usar "object
")
Lo interesante que no hemos visto previamente es la función book.bookinstance_set.all()
. Este método es "automágicamente" creado por Django para devolver el conjunto de registros de BookInstance
asociado con un Book
en particular.
{% for copy in book.bookinstance_set.all %}
<!-- code to iterate across each copy/instance of a book -->
{% endfor %}
Este método es necesario porque has declarado un campo ForeignKey
(uno-a-muchos) únicamente en la lado "uno" de la relación. Como no haces nada para declarar la relación en el otro modelo ("muchos"), este no tiene ningún campo para obtener el conjunto de registros asociados. Para superar este problema, Django construye una función apropiadamente llamada "búsqueda reversa" que puedes usar. El nombre de la función se construye convirtiendo a minúsculas el nombre del modelo donde la ForeignKey
fue declarada, seguido por _set
(así, la función creada en Book
es bookinstance_set()
).
Nota: Aquí usamos all()
para obtener todos los registros (la opción por defecto). A pesar de que puedes usar el método filter()
para obtener un subconjunto de registros en el código, no puedes hacerlo directamente en las plantillas porque no puedes especificar argumentos para las funciones.
Ten también cuidado de que si no defines un orden (en tu vista o modelo basado en clases), verás errores arrojados por el servidor de dearrollo como este:
[29/May/2017 18:37:53] "GET /catalog/books/?page=1 HTTP/1.1" 200 1637 /foo/local_library/venv/lib/python3.5/site-packages/django/views/generic/list.py:99: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <QuerySet [<Author: Ortiz, David>, <Author: H. McRaven, William>, <Author: Leigh, Melinda>]> allow_empty_first_page=allow_empty_first_page, **kwargs)
Eso sucede porque el objeto paginador espera ver una cláusula ORDER BY siendo ejecutada en tu base de datos subyacente. Sin ella, ¡no puede estar seguro de que los registros devueltos están en el orden correcto!
Este tutorial no llegó a la Paginación (aún, pero pronto lo hará), pero como no puedes uar sort_by()
y enviar un parámetro (el mismo con filter()
descrito arriba) tendrás que escoger entre tres opciones:
- Añadir un
ordering
dentro de una declaraciónclass Meta
en tu modelo. - Añadir un atributo
queryset
en tu vista basada en clases personalizada, especificando unorder_by()
. - Añadir un método
get_queryset
a tu vista basada en clases pesonalizada y también especificar elorder_by()
.
Si te decides por la opción class Meta
para el modelo Author (probablemente no tan flexible como personalizar la vista basada en clases, pero lo suficientemente fácil), terminarás con algo como esto:
class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField(null=True, blank=True)
date_of_death = models.DateField('Died', null=True, blank=True)
def get_absolute_url(self):
return reverse('author-detail', args=[str(self.id)])
def __str__(self):
return '%s, %s' % (self.last_name, self.first_name)
class Meta:
ordering = ['last_name']
Por supuesto, el campo no tiene que ser last_name
: podría ser cualquier otro.
Y por último, pero no menos importante, deberías ordenar por un atributo/columna que tenga un índice real (único o no) en tu base de datos para evitar problemas de rendimiento. Por supuesto, esto no será necesario aquí (y probablemente nos estemos adelantando mucho) para la pequeña cantidad de libros (¡y usuarios!), pero es algo a tener en cuenta para proyectos futuros.
¿Cómo se ve?
En este punto deberíamos haber creado todo lo necesario para desplegar tanto la lista de libros como las páginas de detalles de libros. Ejecuta el servidor (python3 manage.py runserver
) y dirígete en tu navegador a http://127.0.0.1:8000/
.
Advertencia: No hagas click aún en ningún enlace de autor o de detalles de autores — ¡los crearás en el reto!
Haz click en el enlace Todos los libros para desplegar la lista de libros.
Luego haz click en un enlace a uno de tus libros. Si todo está correcto, deberías ver algo como la siguiente pantalla.
Paginación
Si apenas tienes unos pocos registros, nuestra página de lista de libros se verá bien. Sin embargo, cuando llegues a las decenas o centenas de registros la página tomará progresivamente más tiempo en cargar (y tendrá demasiado contenido para navegar adecuadamente). La solución a este problema es añadir paginación a tus vistas de lista, reduciendo el número de Ítems desplegados en cada página.
Django incluye un excelente soporte para paginación. Mejor aún, este está incluído en las vistas de lista genéricas basadas en clases, ¡así que no tienes que hacer mucho para habilitarlo!
Vistas
Abre catalog/views.py, y añadie la línea paginate_by
que se muestra abajo en negrita.
class BookListView(generic.ListView):
model = Book
paginate_by = 10
Con esta adición, apenas tengas más de 10 registros la vista comenzará a paginar la información que envía a la plantilla. A las diferentes páginas se accede usando parámetros GET — para acceder a la página 2 usarías la URL: /catalog/books/?page=2
.
Plantillas
Ahora que la información está paginada, necesitamos añadir soporte a la plantilla para desplazarse a través del conjunto de resultados. Como podríamos querer hacer lo mismo en todas las vistas de lista, lo haremos de una forma en la que puede ser añadida a la plantilla base.
Abre /locallibrary/catalog/templates/base_generic.html y copia el siguiente bloque pagination
debajo de nuestro bloque content
(resaltado abajo en negrita). El código primero revisa si la paginación está habilitada en la página actual. Si lo está, añade enlaces next
y previous
apropiados (y el número de la página actual).
{% block content %}{% endblock %}
{% block pagination %}
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">anterior</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{{ request.path }}?page={{ page_obj.next_page_number }}">siguiente</a>
{% endif %}
</span>
</div>
{% endif %}
{% endblock %}
page_obj
es un objeto Paginator que existirá si se usa la paginación en la página actual. Te permite obtener toda la información sobre la página actual, páginas anteriores, cuántas páginas hay, etc.
Usamos {{ request.path }}
para obtener la URL de la página actual para crear a su vez los enlaces de paginación. Esto es útil, porque es independiente del objeto que estamos paginando.
¡Eso es todo!
¿Cómo se ve?
La captura de pantalla de abajo muestra cómo se ve la paginación — si no has ingresado más de 10 títulos en tu base de datos, puedes probarlo más fácilmente reduciendo el número especificado en la línea paginate_by
en tu archivo catalog/views.py. Para obtener el resultado de abajo lo cambiamos a paginate_by = 2
.
Los enlaces de paginación se muestran en la parte de abajo, con enlaces de next/previous desplegados dependiendo de en qué página estés
Rétate a tí mismo
El reto en este artículo es crear las vistas de lista y detalle para autores, que se requieren para completar el proyecto. Estas deberían estar disponibles en las siguientes URLs:
catalog/authors/
— La lista de todos los autores.catalog/author/<id>
— La vista de detalle para el autor específico con un valor en el campo de clave primaria de<id>
El código requerido para los mapeadores URL y las vistas debería ser virtualmente idéntico a las vistas de lista y detalle para Book
que creamos arriba. Las plantillas serán diferentes, pero tendrán un comportamiento similar.
Nota:
- Una vez que has creado el mapeador URL para la página de lista de autores, necesitarás también actualizar el enlace Todos los autores en la plantilla base. Sigue el mismo proceso que hicimos cuando actualizamos el enlace Todos los libros.
- Una vez que has creado el mapeador URL para la página de detalle de autores, deberías también actualizar la plantilla de vista de detalle de libros (/locallibrary/catalog/templates/catalog/book_detail.html) de modo que el enlace de autor apunte a tu nueva página de detalle de autor (en lugar de ser una URL vacía). La línea cambiará para añadir la etiqueta de plantilla que se muestra en negrita abajo.
<p><strong>Autor:</strong> <a href="{% url 'author-detail' book.author.pk %}">{{ book.author }}</a></p>
Cuando termines, tus páginas deberían lucir similares a las capturas de pantalla de abajo.
Resumen
Felicitaciones, ¡la funcionalidad de nuestra biblioteca básica está ahora completa!
En este artículo hemos aprendido cómo usar las vistas genéricas basadas en clases de lista y detalle, y las hemos usado para crear páginas para ver nuestros libros y autores. Sobre la marcha hemos aprendido sobre coincidencia de patrones con expresiones regulares, y cómo puedes enviar información desde las URLs a tus vistas. Hemos también aprendido unos cuantos trucos más para usar plantillas. Por último hemos mostrado cómo paginar vistas de lista, de modo que nuestras listas sean manejables incluso si tenemos muchos registros.
En los siguientes artículos extenderemos esta biblioteca para añadir soporte para cuentas de usuario, y así demostrar la autenticación de usuarios, permisos, sesiones y formularios.
Mira también
- Vistas genéricas basadas en clases incluídas (documentación de Django)
- Vistas genéricas de despliegue (documentación de Django)
- Introducción a las vistas basadas en clases (documentación de Django)
- Etiquetas de plantilla y filtros incluídos (documentación de Django).
- Paginación (documentación de Django)