Comment ajouter un filtre de texte à Django Admin

Comment remplacer la recherche Django par des filtres de texte pour des champs spécifiques

Pour une meilleure expérience de lecture, consultez cet article sur mon site Web.

Lors de la création d'une nouvelle page d'administration Django, une conversation commune entre le développeur et le personnel d'assistance peut ressembler à ceci:

Développeur: Hé, j’ajoute une nouvelle page d’administrateur pour les transactions. Pouvez-vous me dire comment vous voulez rechercher des transactions?
Support: Bien sûr, je cherche généralement par nom d'utilisateur.
Développeur: Cool.
search_fields = (
    nom d'utilisateur,
)
Rien d'autre?
Support: Parfois, je souhaite également effectuer une recherche par adresse électronique de l'utilisateur.
Développeur: OK.
search_fields = (
   nom d'utilisateur,
   user__email,
)
Support: Et le prénom et le nom bien sûr.
Développeur: Ouais, d'accord.
search_fields = (
    nom d'utilisateur,
    user__email,
    user__first_name,
    user__last_name,
)
Est-ce que c'est ça?
Support: Dans certains cas, je dois effectuer une recherche en fonction du numéro du bon de paiement.
Développeur: OK.
search_fields = (
    nom d'utilisateur,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
)
Rien d'autre?
Support: Certains clients envoient leurs factures et posent des questions. Je cherche donc également par numéro de facture.
Développeur: FINE!
search_fields = (
    nom d'utilisateur,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
    facture__nom de facture,
)
OK, tu es sûr que c'est ça?
Support: Les développeurs nous envoient parfois des tickets et utilisent ces longues chaînes aléatoires. Je ne suis jamais vraiment sûr de ce qu’ils sont, alors je cherche et espère que tout ira pour le mieux.
Développeur: Celles-ci sont appelées UUID.
search_fields = (
    nom d'utilisateur,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
    facture__nom de facture,
    uid,
    user__uid,
    paiement__uide,
    facture__uide,
)
Alors c'est ça?
Support: Oui, pour l'instant…

Le problème avec les champs de recherche

Les champs de recherche de Django Admin sont excellents - jetez un tas de champs dans search_fields et Django se chargera du reste.

Le problème avec le champ de recherche commence quand il y en a trop.

Lorsque l’administrateur souhaite effectuer une recherche par UID ou par courrier électronique, Django n’a aucune idée de ce qu’il avait l'intention de faire. Il doit donc effectuer une recherche dans tous les champs répertoriés dans search_fields. Ces requêtes "de correspondance" ont des clauses WHERE énormes et de nombreuses jointures et peuvent rapidement devenir très lentes.

L'utilisation d'un ListFilter normal n'est pas une option - ListFilter affichera une liste de choix à partir des valeurs distinctes du champ. Certains champs énumérés ci-dessus sont uniques et les autres ont de nombreuses valeurs distinctes. Afficher les choix n'est pas une option.

Combler le fossé entre Django et l'utilisateur

Nous avons commencé à réfléchir aux moyens de créer plusieurs champs de recherche, un pour chaque champ ou groupe de champs. Nous pensions que si l'utilisateur souhaitait effectuer une recherche par courrier électronique ou par UID, il n'y avait aucune raison de rechercher dans un autre champ.

Après réflexion, nous avons trouvé une solution: un filtre SimpleListFilter personnalisé:

  • ListFilter permet une logique de filtrage personnalisée.
  • ListFilter peut avoir un modèle personnalisé.
  • Django prend déjà en charge plusieurs ListFilters.

Nous voulions que cela ressemble à ceci:

Un filtre de liste de texte

Implémentation de InputFilter

Ce que nous voulons faire, c'est avoir un ListFilter avec une entrée de texte au lieu de choix.

Avant de plonger dans la mise en œuvre, commençons par la fin. Voici comment nous voulons utiliser notre InputFilter dans un ModelAdmin:

classe UIDFilter (InputFilter):
    nom_paramètre = 'uid'
    titre = _ ('UID')
 
    def queryset (self, request, queryset):
        si self.value () n'est pas None:
            uid = self.value ()
            retourne queryset.filter (
                Q (uid = uid) |
                Q (payment__uid = uid) |
                Q (user__uid = uid)
            )

Et utilisez-le comme n'importe quel autre filtre de liste dans ModelAdmin:

classe TransactionAdmin (admin.ModelAdmin):
    ...
    list_filter = (
        UUIDFilter,
    )
    ...
  • Nous créons un filtre personnalisé pour le champ uuid - UIDFilter.
  • Nous avons défini le nom_paramètre dans l'URL sur uid. Une URL filtrée par uid ressemblera à ceci / admin / app / transaction? Uid =
  • Si l'utilisateur a entré un uid, nous effectuons une recherche par transaction, id de paiement ou identifiant d'utilisateur.

Jusqu'ici, cela ressemble à un ListFilter personnalisé standard.

Maintenant que nous avons une meilleure idée de ce que nous voulons, implémentons notre InputFilter:

Classe InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    def lookups (self, request, model_admin):
        # Dummy, nécessaire pour afficher le filtre.
        revenir ((),)

Nous héritons de SimpleListFilter et remplaçons le modèle. Nous n’avons aucune recherche et nous souhaitons que le modèle rende une entrée de texte au lieu de choix:

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

{% blocktrans avec filter_title = title%} De {{filter_title}} {% endblocktrans%}
      
  •     
                    

Nous utilisons un balisage similaire au filtre de liste existant de Django pour le rendre natif. Le modèle affiche un formulaire simple avec une action GET et un champ de texte pour le paramètre. Lorsque ce formulaire est soumis, l'URL sera mise à jour avec le nom du paramètre et la valeur soumise.

Joue bien avec d'autres filtres

Jusqu'à présent, notre filtre fonctionne, mais seulement s'il n'y a pas d'autres filtres. Si nous voulons jouer avec d’autres filtres, nous devons les prendre en compte dans notre formulaire. Pour ce faire, nous devons connaître leurs valeurs.

Le filtre de liste a une autre fonction appelée «choix». La fonction accepte un objet liste de modifications qui contient toutes les informations sur la vue actuelle et renvoie une liste de choix.

Nous n’avons pas le choix, nous allons donc utiliser cette fonction pour extraire tous les filtres qui ont été appliqués au jeu de requêtes et les exposer au modèle:

Classe InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    def lookups (self, request, model_admin):
        # Dummy, nécessaire pour afficher le filtre.
        revenir ((),)
    choix def (auto, liste de changements):
        # Ne saisissez que l'option "tout".
        all_choice = next (super (). choices (liste de modifications))
        all_choice ['query_parts'] = (
            (k, v)
            pour k, v dans changelist.get_filters_params (). items ()
            si k! = self.nom_paramètre
        )
        céder tout choix

Pour inclure les filtres, nous ajoutons un champ de saisie masqué pour chaque paramètre:

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

{% blocktrans avec filter_title = title%} de {{filter_title}} {% endblocktrans%}
      
  •     {% with choices.0 as all_choice%}     

        {% pour k, v dans all_choice.query_parts%}
        
        {% endfor%}
        
    
    {% terminer par %}
  

Nous avons maintenant un filtre avec une entrée de texte qui joue bien avec d’autres filtres. La seule chose qui reste à faire est d’ajouter une option «claire».

Pour effacer le filtre, nous avons besoin d'une URL incluant tous les filtres sauf le nôtre:

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

    
{% si pas tous_choice.selected%}
   
{% fin si %}
...

Voilà!

Voici ce que nous obtenons:

InputFilter avec d'autres filtres et un bouton de suppression

Le code complet:

Prime

Rechercher plusieurs mots similaires à la recherche Django

Vous avez peut-être remarqué que lors de la recherche de plusieurs mots, Django trouve des résultats qui incluent au moins un des mots, mais pas tous.

Par exemple, si vous recherchez un utilisateur “John Duo”, Django trouvera à la fois “John Foo” et “Bar Due”. Ceci est très pratique lorsque vous recherchez des éléments tels que le nom complet, les noms de produits, etc.

Nous pouvons implémenter une condition similaire en utilisant notre InputFilter:

depuis django.db.models import Q
Classe UserFilter (InputFilter):
    nom_paramètre = 'utilisateur'
    titre = _ ('utilisateur')
    def queryset (self, request, queryset):
        term = self.value ()
        si term est None:
            revenir
        any_name = Q ()
        pour bit dans term.split ():
            any_name & = (
                Q (user__first_name__icontains = bit) |
                Q (user__last_name__icontains = bit)
            )
        return queryset.filter (n'importe quel nom)

Ça y est ...!

Découvrez mes autres articles sur Django Admin: