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

时间:2019-03-25 19:17:16

标签: 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)
    ...

另一个模型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()
    ...

两个模型都注册了ModelAdminCollection模型实现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_querysetCollectionAdmin。我能够做到这一点并过滤结果。但是,这将全面更改Collection个搜索结果。我不知道如何在get_search_resultsget_queryset中获得更多上下文,以便根据某种关系有条件地过滤结果。

在我的情况下,我想为Collection选择多个选项,并选择多个具有不同limit_choices_to选项的元模型,并且自动完成功能应遵守这些限制。

我不希望这能自动执行,也许这应该是功能请求。在这一点上,我无所适从如何根据选择限制(或任何条件)过滤自动完成的结果。

在不使用autocomplete_fields的情况下,Django管理员的默认<select>小部件会过滤结果。

4 个答案:

答案 0 :(得分:1)

这是另一种仅在自动完成字段中获取选项子集的解决方案。此解决方案不会更改主模型 (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 的查询都只会返回协作集合。

答案 1 :(得分:0)

我有完全相同的问题。有点黑,但这是我的解决方案:

  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)

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

希望新版本的Django将有一个更干净的解决方案,例如autocomplete select小部件发送一个额外的查询参数及其正在编辑的模型和字段名称,以便get_search_results可以准确地查找所需的过滤器,而不是(可能不准确) )从引荐来源标头进行推断。

答案 2 :(得分:0)

触发http引用很丑陋,所以我做了一个更好的版本:子类化AutocompleteSelect并发送额外的查询参数,以允许get_search_results自动查找正确的limit_choices_to。只需在您的ModelAdmin中包含此混合(对于源模型和目标模型)。另外,它还会增加ajax请求的延迟,因此您在输入过滤器时不会向服务器发送垃圾邮件,扩大选择范围,并将search_fields属性(设置为'translations__name',这对于我的系统是正确的,针对您的,或者像以前一样省略并单独在ModelAdmins上进行设置):

grep

答案 3 :(得分:0)

在 Django 3.2 中,@Uberdude 提出的解决方案不再有效,因为 AutocompleteSelect 的构造函数现在采用字段而不是关系。

这是 formfield_for_foreignkey 方法所需的更新代码:

def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name in self.get_autocomplete_fields(request) or\
                db_field.name in self.get_autocomplete_cb_fields(request):
            db = kwargs.get('using')
            if db_field.name in self.get_autocomplete_cb_fields(request):
                kwargs['widget'] = AutocompleteSelectCb(
                    db_field, self.admin_site, using=db, for_field=db_field)
            else:
                kwargs['widget'] = AutocompleteSelect(
                    db_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)