Как я могу использовать пользовательское поле поиска (свойство модели) для поиска в Django Admin?
Это очень похоже на этот вопрос, но, к сожалению, я так и не смог заставить его работать.
У меня есть модель, со свойством, которое объединяет несколько полей:
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}"
В Django Admin, когда кто-то выполняет поиск, я могу использовать атрибут search_fields
в Model Admin для указания реальных полей, но не пользовательского поля specimen_name
:
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')
Выполняя поиск с помощью кода выше, он будет искать по отдельным полям, но если я попытаюсь найти полное имя_образца в Django Admin, он не найдет его, потому что ни одно из полей не содержит точного, полного имени образца.
Вопрос SO, на который я ссылался выше, указал мне правильное направление - использование get_search_results
. Теперь мой код выглядит примерно так:
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
Насколько я знаю, я не могу сделать queryset.filter(specimen_name=SOMETHING)
. .filter
не распознает метод @property
как поле, в котором нужно произвести поиск. Поэтому я пишу свой собственный цикл для выполнения поиска.
Приведенный выше код, очевидно, не будет работать. Вы не можете просто добавить список в queryset. Как бы я мог вернуть реальный набор запросов?
Правильным способом фильтрации по свойству является создание эквивалентной аннотации для свойства и фильтрация по ней. Если посмотреть на ваше свойство, то все, что оно делает, это конкатенирует некоторые поля, для этого в Django есть функция базы данных Concat
. Следовательно, вы можете сделать следующую аннотацию:
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"
Тогда вы можете изменить свой get_search_results
следующим образом:
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
Примечание: Вышеуказанное, скорее всего, перестанет давать результаты, если вы не установите search_fields
в пустой кортеж / список.
Продолжая эту линию, возможно, с аннотацией вы можете иметь specimen_name
в search_fields
, переопределив get_queryset
и, следовательно, пропустив переопределение 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
Основываясь на ответе Абдула, который очень помог, я смог немного изменить его, чтобы получить то, что я хотел:
def get_search_results(self, request, queryset, search_term):
# The results of the built-in search, based on search_fields
queryset_a, may_have_duplicates = super().get_search_results(request, queryset, search_term)
# Queryset B starts off equal to the original queryset with
# anotations
queryset_b = queryset.alias(
speci_name=Concat(
"lab_number",
Value("_"),
Replace("patient_name", Value(" "), Value(".")),
Value("_"),
Cast("alberta_health_number", CharField())
)
)
# Filter out queryset_b on every search term
for bit in smart_split(search_term):
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
bit = unescape_string_literal(bit)
queryset_b = queryset_b.filter(Q(speci_name__icontains=bit))
# Return both querysets
# Since we're doing 2 separate searches and combining them, it's
# not impossible for there to be duplicates, so we set
# may_have_duplicates return value to True, which will have Django
# filter out the duplicates
return (queryset_a | queryset_b), True
Небольшая проблема, с которой я столкнулся с кодом Абдула, заключалась в том, что вместо того, чтобы выполнять поиск с помощью search_fields
и добавлять эти результаты к результатам поиска на основе этого нового пользовательского поля, он объединял фильтры
Если бы вы выполнили поиск на основе полного поля specimen_name
, super()
вернул бы пустой набор запросов, а дальнейшая фильтрация на этом этапе вернула бы еще один пустой набор запросов.
Здесь мой код выполняет поиск по умолчанию, вызывая super()
, затем поиск на основе нового пользовательского поля, и складывает результаты вместе.
Когда вы выполняете поиск в Django Admin, по умолчанию он ищет записи, в которых ЛЮБОЕ из полей соответствует условиям поиска. Код Абдула делал так, что поисковый запрос должен был соответствовать ОБОИМ пользовательским полям и любому из полей поиска. Запись, которая соответствовала только пользовательскому полю, игнорировалась. Мой код исправляет это.
Спасибо Абдул - я многому научился из вашего кода.