Как отфильтровать результаты ModelAdmin autocomplete_fields в контексте limit_choices_to - PullRequest
1 голос
/ 25 марта 2019

У меня есть ситуация, когда я хочу использовать административный виджет автозаполнения Django, который учитывает ограничение поля ссылочных моделей.

Например, у меня есть следующая Collection модель с атрибутом kind с указанными вариантами выбора.

class Collection(models.Model):
    ...
    COLLECTION_KINDS = (
        ('personal', 'Personal'),
        ('collaborative', 'Collaborative'),
    )

    name = models.CharField()
    kind = models.CharField(choices=COLLECTION_KINDS)
    ...

Другая модель ScheduledCollection ссылается на Collection с полем ForeignKey, которое реализует опцию limit_choices_to. Цель этой модели - связать метаданные с Collection для конкретного варианта использования.

class ScheduledCollection(models.Model):
    ...
    collection = models.ForeignKey(Collection, limit_choices_to={'kind': 'collaborative'})

    start_date = models.DateField()
    end_date = models.DateField()
    ...

Обе модели зарегистрированы с ModelAdmin. Модель Collection реализует search_fields.

@register(models.Collection)
class CollectionAdmin(ModelAdmin):
    ...
    search_fields = ['name']
    ...

Модель ScheduledCollection реализует autocomplete_fields

@register(models.ScheduledCollection)
class ScheduledCollectionAdmin(ModelAdmin):
    ...
    autocomplete_fields = ['collection']
    ...

Это работает, но не совсем так, как ожидалось. Автозаполнение извлекает результаты из представления, созданного моделью Collection. limit_choices_to не фильтрует результаты и применяется только после сохранения.

Было предложено реализовать get_search_results или get_queryset на модели CollectionAdmin. Я смог сделать это и отфильтровать результаты. Однако это меняет Collection результаты поиска по всем направлениям. Я не знаю, как получить больше контекста в get_search_results или get_queryset для условной фильтрации результатов на основе отношений.

В моем случае я хотел бы иметь несколько вариантов для Collection и несколько метамоделей с различными параметрами limit_choices_to и иметь функцию автозаполнения с учетом этих ограничений.

Я не ожидаю, что это будет работать автоматически, и, возможно, это должен быть запрос функции. На данный момент я не знаю, как отфильтровать результаты автозаполнения в отношении ограничения выбора (или любого условия).

Без использования autocomplete_fields виджет по умолчанию для администратора Django <select> фильтрует результаты.

Ответы [ 2 ]

0 голосов
/ 02 апреля 2019

Запуск http реферера был уродливым, поэтому я сделал лучшую версию: создаю подкласс AutocompleteSelect и отправляю дополнительные параметры запроса, чтобы позволить get_search_results автоматически искать правильный limit_choices_to. Просто включите этот миксин в ModelAdmin (для исходной и целевой моделей). В качестве бонуса он также добавляет задержку к запросам ajax, поэтому вы не спамите сервер при вводе в фильтр, расширяете выбор и устанавливаете атрибут search_fields (значение «translations__name», которое подходит для моей системы, настройте для ваш или опустите и установите индивидуально на ModelAdmins, как и раньше):

from django.contrib.admin import widgets
from django.utils.http import urlencode


class AutocompleteSelect(widgets.AutocompleteSelect):
    """
    Improved version of django's autocomplete select that sends an extra query parameter with the model and field name
    it is editing, allowing the search function to apply the appropriate filter.
    Also wider by default, and adds a debounce to the ajax requests
    """

    def __init__(self, rel, admin_site, attrs=None, choices=(), using=None, for_field=None):
        super().__init__(rel, admin_site, attrs=attrs, choices=choices, using=using)
        self.for_field = for_field

    def build_attrs(self, base_attrs, extra_attrs=None):
        attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
        attrs.update({
            'data-ajax--delay': 250,
            'style': 'width: 50em;'
        })
        return attrs

    def get_url(self):
        url = super().get_url()
        url += '?' + urlencode({
            'app_label': self.for_field.model._meta.app_label,
            'model_name': self.for_field.model._meta.model_name,
            'field_name': self.for_field.name
        })
        return url


class UseAutocompleteSelectMixin:
    """
    To avoid ForeignKey fields to Event (such as on ReportColumn) in admin from pre-loading all events
    and thus being really slow, we turn them into autocomplete fields which load the events based on search text
    via an ajax call that goes through this method.
    Problem is this ignores the limit_choices_to of the original field as this ajax is a general 'search events'
    without knowing the context of what field it is populating. Someone else has exact same problem:
    /9450121/kak-otfiltrovat-rezultaty-modeladmin-autocompletefields-v-kontekste-limitchoicesto
    So fix this by adding extra query parameters on the autocomplete request,
    and use these on the target ModelAdmin to lookup the correct limit_choices_to and filter with it.
    """

    # Overrides django.contrib.admin.options.BaseModelAdmin#formfield_for_foreignkey
    # Is identical except in case db_field.name is in autocomplete fields it constructs our improved AutocompleteSelect
    # instead of django's and passes it extra for_field parameter
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name in self.get_autocomplete_fields(request):
            db = kwargs.get('using')
            kwargs['widget'] = AutocompleteSelect(db_field.remote_field, self.admin_site, using=db, for_field=db_field)
            if 'queryset' not in kwargs:
                queryset = self.get_field_queryset(db, db_field, request)
                if queryset is not None:
                    kwargs['queryset'] = queryset

            return db_field.formfield(**kwargs)

        return super().formfield_for_foreignkey(db_field, request, **kwargs)

    # In principle we could add this override in a different mixin as adding the formfield override above is needed on
    # the source ModelAdmin, and this is needed on the target ModelAdmin, but there's do damage adding everywhere so combine them.
    def get_search_results(self, request, queryset, search_term):
        if 'app_label' in request.GET and 'model_name' in request.GET and 'field_name' in request.GET:
            from django.apps import apps
            model_class = apps.get_model(request.GET['app_label'], request.GET['model_name'])
            limit_choices_to = model_class._meta.get_field(request.GET['field_name']).get_limit_choices_to()
            if limit_choices_to:
                queryset = queryset.filter(**limit_choices_to)
        return super().get_search_results(request, queryset, search_term)

    search_fields = ['translations__name']

0 голосов
/ 02 апреля 2019

У меня была точно такая же проблема.Это немного странно, но вот мое решение:

  1. Переопределить get_search_results модели ModelAdmin, которую вы ищете и хотите отфильтровать
  2. Используйте заголовок реферера запроса, чтобы получить нужный вам волшебный контекстприменить соответствующий фильтр на основе источника отношения
  3. Извлечь limit_choices_to из соответствующего _meta
  4. соответствующего ForeignKey Предварительно отфильтровать набор запросов и затем перейти к методу super.

Итак, для ваших моделей:

@register(models.Collection)
class CollectionAdmin(ModelAdmin):
    ...
    search_fields = ['name']

    def get_search_results(self, request, queryset, search_term):
        if '<app_name>/scheduledcollection/' in request.META.get('HTTP_REFERER', ''):
            limit_choices_to = ScheduledCollection._meta.get_field('collection').get_limit_choices_to()
            queryset = queryset.filter(**limit_choices_to)
        return super().get_search_results(request, queryset, search_term)

Недостатком этого подхода является единственный контекст, который у нас есть, это редактируемая модель в админке, а не какое поле модели, поэтому если ваша модель ScheduledCollection имеет2 коллекции полей автозаполнения (скажем, personal_collection иlaborative_collection) с разными limit_choices_to, мы не можем вывести это из заголовка реферера и трактовать их по-разному.Также у встроенных администраторов будет URL-адрес реферера, основанный на родительской вещи, для которой они встроены, а не отражающий их собственную модель.Но в основных случаях это работает.

Надеемся, что в новой версии Django будет более чистое решение, такое как виджет выбора автозаполнения, отправляющий дополнительный параметр запроса с редактируемой моделью и именем поля, чтобы get_search_results могточно искать требуемые фильтры вместо (возможно, неточно) выводя из заголовка реферера.

...