Come aggiungere un filtro di testo a Django Admin

Come sostituire la ricerca di Django con filtri di testo per campi specifici

Per una migliore esperienza di lettura, consulta questo articolo sul mio sito Web.

Quando si crea una nuova pagina di amministrazione Django, una conversazione comune tra lo sviluppatore e il personale di supporto potrebbe suonare in questo modo:

Sviluppatore: Ehi, sto aggiungendo una nuova pagina di amministrazione per le transazioni. Puoi dirmi come vuoi cercare le transazioni?
Supporto: certo, di solito cerco solo il nome utente.
Sviluppatore: Cool.
search_fields = (
    user__username,
)
Qualunque altra cosa?
Supporto: a volte voglio anche cercare l'indirizzo email dell'utente.
Sviluppatore: OK.
search_fields = (
   user__username,
   user__email,
)
Supporto: e il nome e cognome ovviamente.
Sviluppatore: Sì, ok.
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
)
È così?
Supporto: a volte devo cercare il numero del voucher di pagamento.
Sviluppatore: OK.
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
)
Qualunque altra cosa?
Supporto: alcuni clienti inviano le loro fatture e fanno domande, quindi cerco anche il numero della fattura.
Sviluppatore: FINE!
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
    invoice__invoice_number,
)
OK, sei sicuro che sia?
Supporto: Beh, gli sviluppatori a volte ci inoltrano i biglietti e usano queste lunghe stringhe casuali. Non sono mai veramente sicuro di cosa siano, quindi cerco solo e spero per il meglio.
Sviluppatore: si chiamano UUID.
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
    invoice__invoice_number,
    uid,
    user__uid,
    payment__uid,
    invoice__uid,
)
Quindi è così?
Supporto: Sì, per ora ...

Il problema con i campi di ricerca

I campi di ricerca di Django Admin sono fantastici: lancia un sacco di campi in search_fields e Django gestirà il resto.

Il problema con il campo di ricerca inizia quando ce ne sono troppi.

Quando l'utente amministratore desidera effettuare ricerche tramite UID o e-mail, Django non ha idea di cosa intendesse l'utente, quindi deve cercare in tutti i campi elencati in search_fields. Queste query "match any" hanno clausole WHERE enormi e molti join e possono rapidamente diventare molto lente.

L'uso di un normale ListFilter non è un'opzione: ListFilter visualizzerà un elenco di scelte tra i valori distinti del campo. Alcuni campi che abbiamo elencato sopra sono unici e gli altri hanno molti valori distinti: mostrare le scelte non è un'opzione.

Colmare il divario tra Django e l'utente

Abbiamo iniziato a pensare ai modi in cui possiamo creare più campi di ricerca, uno per ciascun campo o gruppo di campi. Abbiamo pensato che se l'utente desidera effettuare ricerche tramite e-mail o UID non c'è motivo di cercare in nessun altro campo.

Dopo qualche pensiero abbiamo trovato una soluzione: un SimpleListFilter personalizzato:

  • ListFilter consente una logica di filtro personalizzata.
  • ListFilter può avere un modello personalizzato.
  • Django ha già il supporto per più ListFilter.

Volevamo che fosse così:

Un filtro elenco di testo

Implementazione di InputFilter

Quello che vogliamo fare è avere un ListFilter con un input di testo anziché delle scelte.

Prima di approfondire l'implementazione, iniziamo dalla fine. Ecco come vogliamo utilizzare il nostro InputFilter in un ModelAdmin:

class UIDFilter (InputFilter):
    parameter_name = 'uid'
    title = _ ('UID')
 
    def queryset (self, request, queryset):
        se self.value () non è None:
            uid = self.value ()
            return queryset.filter (
                Q (uid = uid) |
                Q (payment__uid = uid) |
                Q (user__uid = uid)
            )

E usalo come qualsiasi altro filtro elenco in un ModelAdmin:

classe TransactionAdmin (admin.ModelAdmin):
    ...
    list_filter = (
        UUIDFilter,
    )
    ...
  • Creiamo un filtro personalizzato per il campo uuid - UIDFilter.
  • Impostiamo il parametro_name nell'URL su uid. Un URL filtrato da uid apparirà come questo / admin / app / transazione? Uid =
  • Se l'utente immette un uid, cerchiamo per transazione, per utente di pagamento o per utente.

Finora questo è proprio come un normale ListFilter personalizzato.

Ora che abbiamo un'idea migliore di ciò che vogliamo, implementiamo il nostro InputFilter:

class InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    ricerche def (self, request, model_admin):
        # Manichino, necessario per mostrare il filtro.
        ritorno ((),)

Ereditiamo da SimpleListFilter e sovrascriviamo il modello. Non abbiamo alcuna ricerca e vogliamo che il modello esegua il rendering di un input di testo anziché le opzioni:

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

{% blocktrans with filter_title = title%} Di {{filter_title}} {% endblocktrans%}

      
  •     
                    

Usiamo un markup simile al filtro elenco esistente di Django per renderlo nativo. Il modello esegue il rendering di un modulo semplice con un'azione GET e un campo di testo per il parametro. Quando viene inviato questo modulo, l'URL verrà aggiornato con il nome del parametro e il valore inviato.

Gioca bene con altri filtri

Finora il nostro filtro funziona ma solo se non ci sono altri filtri. Se vogliamo giocare bene con altri filtri, dobbiamo considerarli nella nostra forma. Per farlo, dobbiamo ottenere i loro valori.

Il filtro elenco ha un'altra funzione chiamata "scelte". La funzione accetta un oggetto elenco modifiche che contiene tutte le informazioni sulla vista corrente e restituisce un elenco di scelte.

Non abbiamo alcuna scelta, quindi useremo questa funzione per estrarre tutti i filtri che sono stati applicati al queryset ed esporli al modello:

class InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    ricerche def (self, request, model_admin):
        # Manichino, necessario per mostrare il filtro.
        ritorno ((),)
    scelte def (auto, elenco modifiche):
        # Prendi solo l'opzione "tutto".
        all_choice = next (super (). scelte (elenco modifiche))
        all_choice ['query_parts'] = (
            (k, v)
            per k, v in changelist.get_filters_params (). items ()
            se k! = self.parameter_name
        )
        cedere all_choice

Per includere i filtri aggiungiamo un campo di input nascosto per ogni parametro:

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

{% blocktrans with filter_title = title%} Di {{filter_title}} {% endblocktrans%}

      
  •     {% with options.0 come all_choice%}     
        {% per k, v in all_choice.query_parts%}
        
        {% endfor%}
        
    
    {% endwith%}
  

Ora abbiamo un filtro con un input di testo che funziona bene con altri filtri. L'unica cosa rimasta da fare per aggiungere un'opzione "cancella".

Per cancellare il filtro abbiamo bisogno di un URL che includa tutti i filtri tranne il nostro:

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

    
{% if not all_choice.selected%}
    ⨉ {% trans 'Remove'%}  
{% finisci se %}
...

Ecco!

Questo è ciò che otteniamo:

Filtro di input con altri filtri e un pulsante Rimuovi

Il codice completo:

indennità

Cerca più parole simili alla ricerca di Django

Avrai notato che durante la ricerca di più parole Django trova risultati che includono almeno una delle parole e non tutte.

Ad esempio, se cerchi un utente "John Duo", Django troverà sia "John Foo" che "Bar Due". Questo è molto comodo quando si cercano cose come il nome completo, i nomi dei prodotti e così via.

Possiamo implementare una condizione simile usando il nostro InputFilter:

da django.db.models import Q
class UserFilter (InputFilter):
    parameter_name = 'user'
    title = _ ('Utente')
    def queryset (self, request, queryset):
        term = self.value ()
        se il termine è Nessuno:
            ritorno
        any_name = Q ()
        per bit in term.split ():
            any_name & = (
                Q (user__first_name__icontains = bit) |
                Q (user__last_name__icontains = bit)
            )
        return queryset.filter (any_name)

Questo è!

Dai un'occhiata agli altri miei post su Django Admin: