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