如何使用 limit_choices_to 的上下文过滤 ModelAdmin autocomplete_fields 结果

Joh*_*ker 3 python django django-models django-admin

我有一种情况,我希望利用 Django 的自动完成管理小部件,它尊重引用模型字段限制。

例如,我有以下Collection模型,该模型kind具有指定选项的属性。

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

    name = models.CharField()
    kind = models.CharField(choices=COLLECTION_KINDS)
    ...
Run Code Online (Sandbox Code Playgroud)

另一个模型ScheduledCollection引用Collection了一个ForeignKey实现limit_choices_to选项的字段。此模型的目的是将元数据与Collection特定用例的a 相关联。

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

    start_date = models.DateField()
    end_date = models.DateField()
    ...
Run Code Online (Sandbox Code Playgroud)

两种模型都注册了ModelAdmin. 该Collection模型实现search_fields.

@register(models.Collection)
class CollectionAdmin(ModelAdmin):
    ...
    search_fields = ['name']
    ...
Run Code Online (Sandbox Code Playgroud)

ScheduledCollection模型实现autocomplete_fields

@register(models.ScheduledCollection)
class ScheduledCollectionAdmin(ModelAdmin):
    ...
    autocomplete_fields = ['collection']
    ...
Run Code Online (Sandbox Code Playgroud)

这有效,但并不完全符合预期。自动完成从Collection模型生成的视图中检索结果。在limit_choices_to不过滤的结果,并在保存才会生效。

有人建议实施get_search_resultsget_querysetCollectionAdmin模型上。我能够做到这一点并过滤结果。但是,这会全面改变Collection搜索结果。我不知道如何根据关系获得更多上下文get_search_resultsget_queryset有条件地过滤结果。

在我的情况下,我希望有多种选择Collection和几种具有不同limit_choices_to选项的元模型,并让自动完成功能尊重这些限制。

我不希望这会自动工作,也许这应该是一个功能请求。在这一点上,我不知道如何根据选择限制(或任何条件)过滤自动完成的结果。

不使用autocomplete_fieldsDjango 管理员的默认<select>小部件过滤结果。

小智 7

触发 http referer 很丑,所以我做了一个更好的版本:子类化 AutocompleteSelect 并发送额外的查询参数以允许 get_search_results 自动查找正确的 limit_choices_to。只需将这个 mixin 包含在您的 ModelAdmin 中(对于源模型和目标模型)。作为奖励,它还增加了 ajax 请求的延迟,因此您在过滤器中输入时不会向服务器发送垃圾邮件,使选择更宽并设置 search_fields 属性(对我的系统来说是正确的“translations__name”,自定义您的或省略并像以前一样在 ModelAdmins 上单独设置):

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

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:
    /sf/ask/3874149121/
    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.ModelAdmin#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']

Run Code Online (Sandbox Code Playgroud)


小智 1

我有同样的问题。这有点 hacky,但这是我的解决方案:

  1. 覆盖您正在搜索并想要过滤的 ModelAdmin 的 get_search_results
  2. 使用请求引用头来获取您需要的神奇上下文,以根据关系的来源应用适当的过滤器
  3. 从适当的foreignkey的_meta中获取limit_choices_to
  4. 预过滤查询集,然后传递给 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)

Run Code Online (Sandbox Code Playgroud)

这种方法的缺点是我们拥有的唯一上下文是在管理中编辑的模型,而不是模型的哪个字段,因此如果您的 ScheduledCollection 模型有 2 个具有不同 limit_choices_to 的集合自动完成字段(例如 individual_collection 和 Collaborative_collection),我们就不能从引用头中推断出这一点并以不同的方式对待它们。此外,内联管理员将拥有基于他们内联的父事物的引用网址,而不是反映他们自己的模型。但它适用于基本情况。

希望 Django 的新版本将有一个更清晰的解决方案,例如自动完成选择小部件发送一个额外的查询参数及其正在编辑的模型和字段名称,以便 get_search_results 可以准确地查找所需的过滤器,而不是(可能不准确)推断引用头。