Módulo 4 · Lección 31 de 45
Personalizar el panel
Veinte atributos que convierten una tabla de filas en una herramienta usable, y el N+1 que el panel resuelve solo y el que no.
El panel sin configurar enseña una lista de objetos con su __str__ y poco más. Con veinte líneas en la clase ModelAdmin pasa a tener columnas, buscador, filtros, acciones en lote y formularios anidados, y deja de parecer una tabla para parecer una herramienta.
Y hay una cosa que no se suele contar: el panel no es gratis en rendimiento. Una lista con una columna mal elegida hace una consulta por fila, igual que cualquier plantilla, y como no hay ninguna vista escrita nadie lo mira. Eso también está medido aquí.
La lista#
@admin.register(Contacto)
class ContactoAdmin(admin.ModelAdmin):
list_display = ['nombre', 'correo', 'grupo', 'tiene_telefono', 'creado']
list_display_links = ['nombre']
list_filter = ['grupo', 'creado', 'etiquetas']
search_fields = ['nombre', 'correo']
ordering = ['nombre']
list_per_page = 25
date_hierarchy = 'creado'
list_editable = ['correo']Cada uno hace una cosa concreta:
| Atributo | Qué hace |
|---|---|
list_display | Las columnas. Campos, métodos o propiedades |
list_display_links | Cuáles llevan a la ficha. Por defecto, la primera |
list_filter | La barra lateral de filtros |
search_fields | El buscador de arriba |
ordering | El orden por defecto de esta lista |
list_per_page | Cuántas filas por página. Son cien de serie |
date_hierarchy | La navegación por año, mes y día de arriba |
list_editable | Campos que se cambian sin abrir la ficha |
list_filter merece un apunte: los filtros por fecha traen rangos hechos (hoy, esta semana, este mes), y sobre un ForeignKey o un ManyToMany listan las opciones. Ahí hay una trampa de escala: un filtro sobre una relación con diez mil filas pinta diez mil opciones en la barra lateral.
Y search_fields admite los mismos sufijos que un filter, con una particularidad: sin sufijo usa icontains, y con = delante del nombre ('=correo') busca exacto, que es mucho más rápido cuando hay un índice.
Columnas calculadas#
Una columna puede ser un método:
@admin.display(description='¿Tiene teléfono?', boolean=True)
def tiene_telefono(self, obj):
return bool(obj.telefono)El decorador configura la columna: description es el título, y boolean=True hace que se pinte un icono de sí o no en vez de la palabra.
Por defecto una columna calculada no se puede ordenar, porque Django no sabe traducirla a SQL. Ese es el mismo límite de la lección 15: lo que se calcula en Python no existe para la base de datos. La salida es la misma, calcularlo allí:
@admin.register(Grupo)
class GrupoAdmin(admin.ModelAdmin):
list_display = ['nombre', 'cuantos']
def get_queryset(self, peticion):
return super().get_queryset(peticion).annotate(_cuantos=Count('contactos'))
@admin.display(description='Contactos', ordering='_cuantos')
def cuantos(self, obj):
return obj._cuantosCon annotate el dato viene de la consulta, y el ordering del decorador le dice al panel por qué columna de SQL ordenar. Es el patrón para cualquier columna de recuento o de total.
Y para pintar HTML, la regla de la lección 19 sin cambios:
from django.utils.html import format_html
@admin.display(description='Foto')
def miniatura(self, obj):
if not obj.foto:
return '—'
return format_html('<img src="{}" height="40">', obj.foto.url)format_html y no una f-string, porque el panel también escapa.
La ficha#
fieldsets = [
(None, {'fields': ['nombre', 'correo']}),
('Clasificación', {'fields': ['grupo', 'etiquetas'], 'classes': ['collapse']}),
('Otros', {'fields': ['telefono', 'notas', 'creado']}),
]
readonly_fields = ['creado']
filter_horizontal = ['etiquetas']
autocomplete_fields = ['grupo']fieldsets agrupa los campos en secciones con título, y 'classes': ['collapse'] las deja plegadas. Un formulario de veinte campos sin secciones es ilegible, y con tres secciones se entiende.
Cuando no hace falta agrupar, fields sirve para elegir y ordenar, y también acepta tuplas para poner dos campos en la misma línea:
fields = [('nombre', 'correo'), 'telefono', 'notas']readonly_fields enseña el valor sin dejar cambiarlo, y es obligatorio para los campos con auto_now_add, que no son editables. También admite métodos, lo que permite mostrar datos calculados dentro de la ficha.
Los dos últimos son los que arreglan los desplegables:
filter_horizontal sustituye el selector múltiple por dos cajas con búsqueda, que es lo único usable con más de diez opciones.
autocomplete_fields convierte un desplegable en un campo que busca según escribes, cargando por peticiones en lugar de volcar la tabla entera. Es la respuesta a lo que quedó pendiente en la lección 26, y tiene un requisito: el modelo al que apunta necesita search_fields en su propio ModelAdmin, porque es lo que usa la búsqueda. Sin eso, el panel da un error de comprobación diciéndolo.
Modelos anidados#
Para editar lo relacionado desde la misma página:
class FichaInline(admin.StackedInline):
model = Ficha
extra = 0
@admin.register(Contacto)
class ContactoAdmin(admin.ModelAdmin):
inlines = [FichaInline]StackedInline pone cada uno en bloque y TabularInline en filas de tabla; la segunda va mejor con pocos campos. El extra son los formularios vacíos que aparecen para añadir, y cero es casi siempre lo que se quiere, porque el valor por defecto llena la pantalla.
Los anidados son la mejor parte del panel y también la más cara: cada uno añade consultas y formularios. Tres anidados con veinte filas cada uno hacen una página lenta.
Acciones en lote#
Una función que recibe la selección y hace algo con ella:
@admin.action(description='Vaciar el teléfono de los seleccionados')
def vaciar_telefono(modeladmin, peticion, consulta):
actualizados = consulta.update(telefono='')
modeladmin.message_user(peticion, f'{actualizados} contactos actualizados.')
@admin.register(Contacto)
class ContactoAdmin(admin.ModelAdmin):
actions = [vaciar_telefono]acciones: [('delete_selected', 'Eliminar contactos seleccionado/s'),
('vaciar_telefono', 'Vaciar el teléfono de los seleccionados')]El tercer argumento es un QuerySet con lo seleccionado, así que se puede usar update(), recorrerlo o exportarlo. message_user deja el aviso verde de arriba, que es el sistema de mensajes de la lección 35.
El aspecto#
Tres líneas que quitan el "Django administration" de todas las páginas:
admin.site.site_header = 'Agenda de contactos'
admin.site.site_title = 'Agenda'
admin.site.index_title = 'Gestión'<title>Seleccione contacto a modificar | Agenda</title>Y para lo demás, el mecanismo es el de la lección 16: las plantillas de DIRS se miran antes que las de las aplicaciones, así que un fichero en plantillas/admin/base_site.html sustituye al del panel. Lo razonable es heredar del original y cambiar solo un bloque:
{% extends "admin/base_site.html" %}
{% block extrastyle %}<link rel="stylesheet" href="{% static 'admin_propio.css' %}">{% endblock %}