Personalización avanzada del admin Django

Intermedio
Django
Django
Actualizado: 18/04/2026

Fieldsets: organizar el formulario

Los fieldsets organizan los campos del formulario de edición en secciones con nombres y opciones:

@admin.register(Articulo)
class ArticuloAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {
            'fields': ['titulo', 'slug', 'autor', 'categoria']
        }),
        ('Contenido', {
            'fields': ['resumen', 'contenido', 'imagen_destacada'],
        }),
        ('Publicación', {
            'fields': ['publicado', 'fecha_publicacion', 'destacado'],
            'description': 'Controla la visibilidad y programación del artículo',
        }),
        ('SEO', {
            'fields': ['seo_titulo', 'seo_descripcion', 'seo_imagen'],
            'classes': ['collapse'],  # La sección empieza colapsada
        }),
        ('Metadatos', {
            'fields': ['fecha_creacion', 'fecha_modificacion', 'visitas'],
            'classes': ['collapse'],
        }),
    ]

    prepopulated_fields = {'slug': ['titulo']}
    readonly_fields = ['fecha_creacion', 'fecha_modificacion', 'visitas']

    def get_readonly_fields(self, request, obj=None):
        """Campos de solo lectura que dependen del estado del objeto."""
        readonly = list(self.readonly_fields)
        if obj and obj.publicado:
            readonly.append('autor')  # No permitir cambiar el autor de artículos publicados
        if not request.user.is_superuser:
            readonly.append('destacado')
        return readonly

Diagrama conceptual de Personalización avanzada del admin Django

Sobreescribir save_model y delete_model

@admin.register(Pedido)
class PedidoAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        if not change:  # Solo en creación
            obj.creado_por = request.user
        obj.ultima_modificacion_por = request.user
        super().save_model(request, obj, form, change)

        # Lógica posterior al guardado
        if obj.estado == 'confirmado':
            enviar_email_confirmacion(obj)

    def delete_model(self, request, obj):
        """Lógica antes de eliminar."""
        import logging
        logger = logging.getLogger(__name__)
        logger.info(f'Pedido {obj.id} eliminado por {request.user}')
        super().delete_model(request, obj)

    def get_queryset(self, request):
        """Filtrar el queryset según el usuario."""
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(creado_por=request.user)

Personalizar plantillas del admin

Django permite sobreescribir cualquier plantilla del admin creando el archivo en templates/admin/:

templates/
    admin/
        base_site.html          # Cabecera global del admin
        index.html              # Página de inicio del admin
        catalogo/
            producto/
                change_list.html    # Listado de productos
                change_form.html    # Formulario de edición de producto
<!-- templates/admin/base_site.html -->
{% extends "admin/base.html" %}

{% block branding %}
<div id="site-name">
    <a href="{% url 'admin:index' %}">
        <img src="/static/img/logo_admin.png" alt="Logo" height="30">
        Panel de administración
    </a>
</div>
{% endblock %}

{% block extrastyle %}
{{ block.super }}
<style>
    #header { background: #1a1a2e; }
    .module h2 { background: #16213e; }
</style>
{% endblock %}

Campos calculados con anotaciones

from django.db.models import Count, Sum

@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
    list_display = ['nombre', 'num_productos', 'valor_total_stock']

    def get_queryset(self, request):
        return super().get_queryset(request).annotate(
            _num_productos=Count('producto'),
            _valor_total=Sum('producto__precio'),
        )

    def num_productos(self, obj):
        return obj._num_productos
    num_productos.short_description = 'Productos'
    num_productos.admin_order_field = '_num_productos'

    def valor_total_stock(self, obj):
        if obj._valor_total:
            return f'{obj._valor_total:.2f} €'
        return '0,00 €'
    valor_total_stock.short_description = 'Valor total'
    valor_total_stock.admin_order_field = '_valor_total'
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Organizar los campos del formulario de edición en secciones con fieldsets. Definir readonly_fields para campos no editables en el formulario. Usar prepopulated_fields para generar slugs automáticamente. Personalizar plantillas del admin sobreescribiendo las plantillas base. Añadir lógica personalizada sobreescribiendo save_model y get_queryset.