Home > Net >  How can I use a custom search field (model property) to search in Django Admin?
How can I use a custom search field (model property) to search in Django Admin?

Time:09-27

This is very similar to this question, but unfortunately, I still couldn't get it working.

I have a model, with a property that combines a few fields:

class Specimen(models.Model):
    lab_number = ...
    patient_name = ...
    specimen_type = ...

    @property
    def specimen_name(self):
        return f"{self.lab_number}_{self.patient_name}_{self.specimen_type}"

In Django Admin, when someone does a search, I can use the search_fields attribute in the Model Admin to specify real fields, but not the specimen_name custom field:


def specimen_name(inst):
    return inst.specimen_name
specimen_name.short_description = "Specimen Name"

class SpecimenModelAdmin(admin.ModelAdmin):
    list_display = ('specimen_name', 'patient_name', 'lab_number', 'specimen_type')
    search_fields = ('patient_name', 'lab_number', 'specimen_type')

Doing a search using the code above, it will search the individual fields, but if I try to search for a full specimen_name in Django Admin, it won't find it, because none of the fields contain the exact, full specimen name.

The SO question I linked to above pointed me in the right direction - using get_search_results. My code now looks something like this:

class SpecimenModelAdmin(admin.ModelAdmin):
    ...
    search_fields = ('patient_name', 'lab_number', 'specimen_type')

    def get_search_results(self, request, queryset, search_term):
        if not search_term:
            return queryset, False

        queryset, may_have_duplicates = super().get_search_results(
            request, queryset, search_term,
        )

        search_term_list = search_term.split(' ')
        specimen_names = [q.specimen_name for q in queryset.all()]
        results = []

        for term in search_term_list:
            for name in specimen_names:
                if term in name:
                    results.append(name)
                    break

        # Return original queryset, AND any new results we found by searching the specimen_name field
        # The True indicates that it's possible that we will end up with duplicates
        # I assume that means Django will make sure only unique results are returned when that's set
        return queryset   results, True

As far as I know, I can't do a queryset.filter(specimen_name=SOMETHING). .filter won't recognize the @property method as a field in needs to search. That's why I write my own loop to do the searching.

The code above will obviously not work. You can't just add a list to a queryset. How would I return an actual queryset?

CodePudding user response:

The correct way to filter on a property is to make an equivalent annotation for the property and filter on that instead. Looking at your property all it does is it concatenates some of the fields, corresponding to that Django has the Concat database function. Hence you can do the following annotation:

from django.db.models import Value
from django.db.models.functions import Concat


queryset = queryset.annotate(
    specimen_name=Concat("lab_number", Value("_"), "patient_name", Value("_"), "specimen_type")
)
# Note: If you use Django version >=3.2 you can use "alias" instead of "annotate"

Then you can change your get_search_results as follows:

from django.db.models import Value, Q
from django.db.models.functions import Concat
from django.utils.text import (
    smart_split, unescape_string_literal
)


class SpecimenModelAdmin(admin.ModelAdmin):
    ...
    search_fields = ('patient_name', 'lab_number', 'specimen_type')

    def get_search_results(self, request, queryset, search_term):
        queryset = queryset.annotate(
            specimen_name=Concat(
                "lab_number",
                Value("_"),
                "patient_name",
                Value("_"),
                "specimen_type"
            )
        )
        queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term)
        for bit in smart_split(search_term):
            if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
                bit = unescape_string_literal(bit)
             queryset = queryset.filter(Q(specimen_name__icontains=bit))
        return queryset, may_have_duplicates

Note: The above will likely stop giving you results unless you set search_fields to an empty tuple / list.

Continuing down this line perhaps with the annotation you can have specimen_name in search_fields by overriding get_queryset and hence skip overriding get_search_results:

class SpecimenModelAdmin(admin.ModelAdmin):
    ...
    search_fields = ('patient_name', 'lab_number', 'specimen_type', 'specimen_name')
    
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.annotate(
            specimen_name=Concat(
                "lab_number",
                Value("_"),
                "patient_name",
                Value("_"),
                "specimen_type"
            )
        )
        return qs
  • Related