Sådan tilføjes et tekstfilter til Django Admin

Sådan udskiftes Django-søgning med tekstfiltre til specifikke felter

For en bedre læseoplevelse, se denne artikel på min hjemmeside.

Når du opretter en ny Django Admin-side, kan en fælles samtale mellem udvikleren og supportpersonen muligvis lyde sådan:

Udvikler: Hej, jeg tilføjer en ny admin-side til transaktioner. Kan du fortælle mig, hvordan du vil søge efter transaktioner?
Support: Ja, jeg søger normalt kun efter brugernavnet.
Udvikler: Cool.
search_fields = (
    user__username,
)
Ellers andet?
Support: Jeg vil undertiden også søge efter brugerens e-mail-adresse.
Udvikler: OK.
search_fields = (
   user__username,
   user__email,
)
Support: Og for- og efternavnet selvfølgelig.
Udvikler: Ja, OK.
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
)
Er det det?
Support: Nå, nogle gange er jeg nødt til at søge efter betalingskuponnummeret.
Udvikler: OK.
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
)
Ellers andet?
Support: Nogle kunder sender deres fakturaer og stiller spørgsmål, så jeg søger også efter fakturanummeret.
Udvikler: FINE!
search_fields = (
    user__username,
    user__email,
    user__first_name,
    user__last_name,
    payment__voucher_number,
    invoice__invoice_number,
)
OK, er du sikker på, at dette er det?
Support: Nå, udviklere videresender undertiden billetter til os, og de bruger disse lange tilfældige strenge. Jeg er aldrig rigtig sikker på, hvad de er, så jeg bare søger og håber på det bedste.
Udvikler: Disse kaldes UUID'er.
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,
)
Så er det det?
Support: Ja, for nu ...

Problemet med søgefelter

Django Admin-søgefelter er store - kaste en masse felter i search_fields, og Django vil håndtere resten.

Problemet med søgefelt begynder, når der er for mange af dem.

Når administratorbrugeren ønsker at søge med UID eller e-mail, har Django ingen idé om, at det er det, brugeren havde til hensigt, så den er nødt til at søge efter alle felterne, der er anført i search_fields. Disse "matcher enhver" -forespørgsler har enorme HVOR-klausuler og masser af sammenføjninger og kan hurtigt blive meget langsomme.

Brug af en almindelig ListFilter er ikke en mulighed - ListFilter gengiver en liste over valg fra feltets forskellige værdier. Nogle felter, vi har anført ovenfor, er unikke, og de andre har mange forskellige værdier - At vise valg er ikke en mulighed.

Bridging af kløften mellem Django og brugeren

Vi begyndte at tænke på måder, vi kan oprette flere søgefelter på - et for hvert felt eller gruppe af felter. Vi troede, at hvis brugeren ønsker at søge via e-mail eller UID, er der ingen grund til at søge efter noget andet felt.

Efter nogle tanker kom vi frem til en løsning - en brugerdefineret SimpleListFilter:

  • ListFilter giver mulighed for tilpasset filtreringslogik.
  • ListFilter kan have en brugerdefineret skabelon.
  • Django har allerede understøttelse af flere ListFilters.

Vi ønskede, at det skulle se sådan ud:

Et tekstlistefilter

Implementering af InputFilter

Hvad vi vil gøre er at have en ListFilter med en tekstindtastning i stedet for valg.

Lad os starte fra slutningen, før vi dykker ned i implementeringen. Sådan ønsker vi at bruge vores InputFilter i en ModelAdmin:

klasse UIDFilter (InputFilter):
    parameter_name = 'uid'
    title = _ ('UID')
 
    def queryset (selv, anmodning, queryset):
        hvis self.value () ikke er Ingen:
            uid = self.value ()
            return queryset.filter (
                Q (uid = uid) |
                Q (betaling__uid = uid) |
                Q (user__uid = uid)
            )

Og brug det som ethvert andet listefilter i en ModelAdmin:

klasse TransactionAdmin (admin.ModelAdmin):
    ...
    list_filter = (
        UUIDFilter,
    )
    ...
  • Vi opretter et brugerdefineret filter til det uuid felt - UIDFilter.
  • Vi indstiller parameternavnet i URL'en til at være uid. En URL, der er filtreret af uid, vil se sådan ud / admin / app / transaktion? Uid =
  • Hvis brugeren indtastede en uid, søger vi efter transaktions uid, betalings uid eller user uid.

Indtil videre er dette ligesom en almindelig brugerdefineret ListFilter.

Nu hvor vi har en bedre idé om, hvad vi ønsker, lad os implementere vores InputFilter:

klasse InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    def-opslag (selv, anmodning, model_admin):
        # Dummy, krævet for at vise filteret.
        Vend tilbage ((),)

Vi arver fra SimpleListFilter og tilsidesætter skabelonen. Vi har ikke nogen opslag, og vi ønsker, at skabelonen skal give en tekstinput i stedet for valg:

// skabeloner / admin / input_filter.html
{% belastning i18n%}

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

      
  •     
                    

Vi bruger lignende markering som Djangos eksisterende listefilter for at gøre det oprindeligt. Skabelonen gengiver en simpel form med en GET-handling og et tekstfelt til parameteren. Når denne formular indsendes, opdateres URL'en med parameternavnet og den indsendte værdi.

Spil pænt med andre filtre

Indtil videre fungerer vores filter, men kun hvis der ikke er andre filtre. Hvis vi vil lege pænt med andre filtre, er vi nødt til at overveje dem i vores form. For at gøre det, er vi nødt til at få deres værdier.

Listefilteret har en anden funktion kaldet “valg”. Funktionen accepterer et skiftelisteobjekt, der indeholder alle oplysninger om den aktuelle visning og returnerer en liste over valg.

Vi har ikke nogen valg, så vi vil bruge denne funktion til at udtrække alle filtre, der blev anvendt på queryset og udsætte dem for skabelonen:

klasse InputFilter (admin.SimpleListFilter):
    template = 'admin / input_filter.html'
    def-opslag (selv, anmodning, model_admin):
        # Dummy, krævet for at vise filteret.
        Vend tilbage ((),)
    def valg (self, changelist):
        # Grib kun indstillingen "alle".
        all_choice = næste (super (). valg (skifteliste))
        all_choice ['query_parts'] = (
            (k, v)
            for k, v i changelist.get_filters_params (). elementer ()
            hvis k! = self.parameter_name
        )
        give all_choice

For at inkludere filtre tilføjer vi et skjult inputfelt for hver parameter:

// skabeloner / admin / input_filter.html
{% belastning i18n%}

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

      
  •     {% med valg.0 som all_choice%}     
        {% for k, v i all_choice.query_parts%}
        
        {% endfor%}
        
    
    {% endwith%}
  

Nu har vi et filter med en tekstindtastning, der spiller pænt med andre filtre. Det eneste, der er tilbage at gøre det for at tilføje en "klar" mulighed.

For at rydde filteret har vi brug for en URL, der inkluderer alle filtre undtagen vores:

// skabeloner / admin / input_filter.html
...

    
{% hvis ikke all_choice.selected%}
    ⨉ {% trans 'Fjern'%}  
{% Afslut Hvis %}
...

Sådan!

Dette er, hvad vi får:

InputFilter med andre filtre og en fjern-knap

Den komplette kode:

Bonus

Søg i flere ord, der ligner Django-søgning

Du har måske bemærket, at når du søger på flere ord, finder Django resultater, der indeholder mindst et af ordene og ikke alle.

Hvis du f.eks. Søger efter en bruger “John Duo”, vil Django finde både “John Foo” og “Bar Due”. Dette er meget praktisk, når du søger efter ting som fuldt navn, produktnavne og så videre.

Vi kan implementere en lignende tilstand ved hjælp af vores InputFilter:

fra django.db.models import Q
klasse UserFilter (InputFilter):
    parameter_name = 'bruger'
    title = _ ('Bruger')
    def queryset (selv, anmodning, queryset):
        udtryk = selvværdi ()
        hvis udtrykket er Ingen:
            Vend tilbage
        any_name = Q ()
        for bit in term.split ():
            any_name & = (
                Q (bruger__first_name__icontains = bit) |
                Q (user__last_name__icontains = bit)
            )
        returner queryset.filter (ethvert navn)

Dette er det!

Tjek mine andre indlæg på Django Admin: