为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

四季花海 2021-12-16 15:10:57
我有一种情况,我希望利用 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特定用例的a 相关联。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_fieldsDjango 管理员的默认<select>小部件过滤结果。
查看完整描述

3 回答

?
江户川乱折腾

TA贡献1851条经验 获得超5个赞

触发 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:

    https://stackoverflow.com/questions/55344987/how-to-filter-modeladmin-autocomplete-fields-results-with-the-context-of-limit-c

    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']


查看完整回答
反对 回复 2021-12-16
?
30秒到达战场

TA贡献1828条经验 获得超6个赞

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

  1. 覆盖您正在搜索并要过滤的 ModelAdmin 的 get_search_results

  2. 使用请求引用标头获取您需要根据关系来源应用适当过滤器的神奇上下文

  3. 从适当的 ForeignKey 的 _meta 中获取 limit_choices_to

  4. 预过滤查询集,然后传递给超级方法。

所以对于你的模型:

@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)


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


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


查看完整回答
反对 回复 2021-12-16
?
森林海

TA贡献2011条经验 获得超2个赞

这是另一种在自动完成字段中仅获取选择子集的解决方案。此解决方案不会更改主模型 ( Collection)的默认行为,因此您仍然可以在应用程序中使用带有完整集的自动完成功能来拥有其他视图。


下面是它的工作原理:


带管理器的集合的代理模型

创建一个代理模型来表示 的一个子集Collection,例如CollaborativeCollection表示“协作”类型的集合。您还需要一个管理器来将代理模型的初始查询集限制为预期的子集。


class CollaborativeCollectionManager(models.Manager):

    def get_queryset(self):

        return (

            super()

            .get_queryset()

            .filter(kind="collaborative")

        )



class CollaborativeCollection(models.Model):

    class Meta:

        proxy = True


    objects = CollaborativeCollectionManager()


更新外键以使用代理模型

接下来更新外键ScheduledCollection以使用代理模型。请注意,limit_choices_to如果您不需要其他任何功能,则可以删除该功能。


class ScheduledCollection(models.Model):

    ...

    collection = models.ForeignKey(CollaborativeCollection)


    start_date = models.DateField()

    end_date = models.DateField()

    ...

为代理定义管理模型

最后定义代理的管理模型。


@admin.register(CollaborativeCollection)

class CollaborativeCollectionAdmin(admin.ModelAdmin): 

    search_fields = ["name"]

请注意,您还可以get_search_results()在管理模型中定义自定义,而不是管理器。但是,我发现经理方法似乎更高效。从概念上讲,它也有更多的声音,因为所有查询都CollaborativeCollection将只返回协作集合。


查看完整回答
反对 回复 2021-12-16
  • 3 回答
  • 0 关注
  • 333 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信