Cómo agregar un filtro de texto a Django Admin

Cómo reemplazar la búsqueda de Django con filtros de texto para campos específicos

Para una mejor experiencia de lectura, consulte este artículo en mi sitio web.

Al crear una nueva página de administración de Django, una conversación común entre el desarrollador y el personal de soporte puede sonar así:

Desarrollador: Hola, estoy agregando una nueva página de administración para las transacciones. ¿Me puede decir cómo desea buscar transacciones?
Soporte: Claro, generalmente solo busco por el nombre de usuario.
Desarrollador: Cool.
search_fields = (
    user__username,
)
¿Algo más?
Soporte: a veces también quiero buscar por la dirección de correo electrónico del usuario.
Desarrollador: OK.
search_fields = (
   user__username,
   usuario__email,
)
Soporte: Y el nombre y apellido, por supuesto.
Desarrollador: Sí, está bien.
search_fields = (
    user__username,
    usuario__email,
    user__first_name,
    user__last_name,
)
¿Es asi?
Soporte: Bueno, a veces necesito buscar por el número de comprobante de pago.
Desarrollador: OK.
search_fields = (
    user__username,
    usuario__email,
    user__first_name,
    user__last_name,
    pago__voucher_number,
)
¿Algo más?
Soporte: algunos clientes envían sus facturas y hacen preguntas, así que también busco por el número de factura.
Desarrollador: FINE!
search_fields = (
    user__username,
    usuario__email,
    user__first_name,
    user__last_name,
    pago__voucher_number,
    factura_número_de_factura,
)
OK, ¿estás seguro de que es esto?
Soporte: Bueno, los desarrolladores a veces nos envían tickets y usan estas largas cadenas aleatorias. Nunca estoy seguro de cuáles son, así que solo busco y espero lo mejor.
Desarrollador: se denominan UUID.
search_fields = (
    user__username,
    usuario__email,
    user__first_name,
    user__last_name,
    pago__voucher_number,
    factura_número_de_factura,
    uid
    user__uid,
    Payment__uid,
    factura__uida,
)
¿Entonces es eso?
Soporte: Sí, por ahora ...

El problema con los campos de búsqueda.

Los campos de búsqueda de administrador de Django son geniales: agregue un montón de campos en search_fields y Django se encargará del resto.

El problema con el campo de búsqueda comienza cuando hay demasiados.

Cuando el usuario administrador desea buscar por UID o correo electrónico, Django no tiene idea de que esto es lo que el usuario pretendía, por lo que debe buscar por todos los campos enumerados en search_fields. Estas consultas de "coincidencia cualquiera" tienen enormes cláusulas WHERE y muchas uniones y pueden volverse muy lentas rápidamente.

Usar un ListFilter normal no es una opción: ListFilter mostrará una lista de opciones de los distintos valores del campo. Algunos campos que enumeramos anteriormente son únicos y otros tienen muchos valores distintos: mostrar opciones no es una opción.

Cerrar la brecha entre Django y el usuario

Comenzamos a pensar en formas en que podemos crear múltiples campos de búsqueda, uno para cada campo o grupo de campos. Pensamos que si el usuario desea buscar por correo electrónico o UID, no hay razón para buscar por ningún otro campo.

Después de pensarlo, se nos ocurrió una solución: un SimpleListFilter personalizado:

  • ListFilter permite una lógica de filtrado personalizada.
  • ListFilter puede tener una plantilla personalizada.
  • Django ya tiene soporte para múltiples ListFilters.

Queríamos que se viera así:

Un filtro de lista de texto

Implementando InputFilter

Lo que queremos hacer es tener un ListFilter con una entrada de texto en lugar de opciones.

Antes de sumergirnos en la implementación, comencemos desde el final. Así es como queremos usar nuestro InputFilter en un ModelAdmin:

clase UIDFilter (InputFilter):
    nombre_parámetro = 'uid'
    title = _ ('UID')
 
    conjunto de consultas def (self, request, queryset):
        si self.value () no es None:
            uid = self.value ()
            return queryset.filter (
                Q (uid = uid) |
                Q (pago__uid = uid) |
                Q (user__uid = uid)
            )

Y úselo como cualquier otro filtro de lista en un ModelAdmin:

clase TransactionAdmin (admin.ModelAdmin):
    ...
    list_filter = (
        UUIDFilter,
    )
    ...
  • Creamos un filtro personalizado para el campo uuid: UIDFilter.
  • Configuramos el nombre_parámetro en la URL para que sea uid. Una URL filtrada por uid se verá así / admin / app / transacción? Uid =
  • Si el usuario ingresó un UID, buscamos por UID de transacción, UID de pago o UID de usuario.

Hasta ahora, esto es como un ListFilter personalizado normal.

Ahora que tenemos una mejor idea de lo que queremos, implementemos nuestro InputFilter:

clase InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    búsquedas de def (self, request, model_admin):
        # Dummy, requerido para mostrar el filtro.
        regreso ((),)

Heredamos de SimpleListFilter y anulamos la plantilla. No tenemos ninguna búsqueda y queremos que la plantilla presente un ingreso de texto en lugar de opciones:

// templates / admin / input_filter.html
{% load i18n%}

{% blocktrans con filter_title = title%} Por {{filter_title}} {% endblocktrans%}

      
  •     
                    

Utilizamos un marcado similar al filtro de lista existente de Django para que sea nativo. La plantilla presenta un formulario simple con una acción GET y un campo de texto para el parámetro. Cuando se envía este formulario, la URL se actualizará con el nombre del parámetro y el valor enviado.

Juega bien con otros filtros

Hasta ahora nuestro filtro funciona, pero solo si no hay otros filtros. Si queremos jugar bien con otros filtros, debemos considerarlos en nuestro formulario. Para hacer eso, necesitamos obtener sus valores.

El filtro de lista tiene otra función llamada "opciones". La función acepta un objeto de lista de cambios que contiene toda la información sobre la vista actual y devuelve una lista de opciones.

No tenemos ninguna opción, por lo que vamos a utilizar esta función para extraer todos los filtros que se aplicaron al conjunto de consultas y exponerlos a la plantilla:

clase InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    búsquedas de def (self, request, model_admin):
        # Dummy, requerido para mostrar el filtro.
        regreso ((),)
    opciones de definición (self, lista de cambios):
        # Tome solo la opción "todos".
        all_choice = next (super (). elecciones (lista de cambios))
        all_choice ['query_parts'] = (
            (k, v)
            para k, v en changelist.get_filters_params (). items ()
            if k! = self.parameter_name
        )
        rendimiento all_choice

Para incluir los filtros, agregamos un campo de entrada oculto para cada parámetro:

// templates / admin / input_filter.html
{% load i18n%}

{% blocktrans con filter_title = title%} Por {{filter_title}} {% endblocktrans%}

      
  •     {% con elecciones.0 como all_choice%}     
        {% para k, v en all_choice.query_parts%}
        
        {% endfor%}
        
    
    {% terminar con %}
  

Ahora tenemos un filtro con una entrada de texto que funciona bien con otros filtros. Lo único que queda por hacer es agregar una opción "clara".

Para borrar el filtro, necesitamos una URL que incluya todos los filtros excepto el nuestro:

// templates / admin / input_filter.html
...

    
{% si no all_choice.selected%}
    ⨉ {% trans 'Remove'%}  
{% terminara si %}
...

Voilà!

Esto es lo que obtenemos:

InputFilter con otros filtros y un botón eliminar

El código completo:

Prima

Buscar varias palabras similares a Django search

Es posible que haya notado que cuando busca varias palabras, Django encuentra resultados que incluyen al menos una de las palabras y no todas.

Por ejemplo, si busca un usuario "John Duo", Django encontrará tanto "John Foo" como "Bar Due". Esto es muy conveniente cuando busca cosas como nombre completo, nombres de productos, etc.

Podemos implementar una condición similar usando nuestro InputFilter:

de django.db.models import Q
clase UserFilter (InputFilter):
    nombre_parámetro = 'usuario'
    title = _ ('Usuario')
    conjunto de consultas def (self, request, queryset):
        term = self.value ()
        si el término es Ninguno:
            regreso
        any_name = Q ()
        para bit en term.split ():
            any_name & = (
                Q (user__first_name__icontains = bit) |
                Q (user__last_name__icontains = bit)
            )
        return queryset.filter (any_name)

¡Eso es todo!

Mira mis otras publicaciones en Django Admin: