Как я могу использовать пользовательское поле поиска (свойство модели) для поиска в 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, по умолчанию он ищет записи, в которых ЛЮБОЕ из полей соответствует условиям поиска. Код Абдула делал так, что поисковый запрос должен был соответствовать ОБОИМ пользовательским полям и любому из полей поиска. Запись, которая соответствовала только пользовательскому полю, игнорировалась. Мой код исправляет это.

Спасибо Абдул - я многому научился из вашего кода.

Вернуться на верх