如何限制 Django raw_id_field 的 ForeignKey 选择的选择

Posted

技术标签:

【中文标题】如何限制 Django raw_id_field 的 ForeignKey 选择的选择【英文标题】:How to limit choices of ForeignKey choices for Django raw_id_field 【发布时间】:2014-06-30 08:04:44 【问题描述】:

当使用raw_id_fields 选项显示 Django 管理员中的 ForeignKey 字段时,如何限制它们显示的选项?

当呈现为选择框时,define a custom ModelForm 可以简单地设置该字段的 queryset 值以及所需的选项。然而,当使用raw_id_fields 渲染时,这个查询集似乎被完全忽略了。它会生成指向ForeignKey 模型的链接,允许您通过弹出窗口从该模型中选择任何记录。您仍然可以通过自定义 URL 来过滤这些值,但我找不到通过 ModelAdmin 执行此操作的方法。

【问题讨论】:

【参考方案1】:

如果您只需要过滤静态类型,@Dmitriy Sintsov 的回答非常棒,但在我的情况下,我在两个模型之间都有外来关系,我希望它根据我正在使用的特定 ID 进行过滤。

要构建on his answer,想象Project拥有Blog,并且在选择Blog过滤时,您希望它仅显示与Project相关的那些。他的答案的这两个变化完成了:

    在小部件中添加一个新变量——我将其命名为project_id
class BlogRawIdWidget(widgets.ForeignKeyRawIdWidget):

    def __init__(self, *args, **kwargs):
        self.project_id = kwargs.pop('project_id')
        super().__init__(*args, **kwargs)
    修改调用该小部件的行:
rel=Project._meta.get_field('blog').remote_field, admin_site=site,
        project_id=self.instance.project.id)

完整代码如下:

from django.contrib import admin
from django.contrib.admin import widgets
from django.contrib.admin.sites import site
from django import forms

class BlogRawIdWidget(widgets.ForeignKeyRawIdWidget):

    def __init__(self, *args, **kwargs):
        self.project_id = kwargs.pop('project_id')
        super().__init__(*args, **kwargs)

    def url_parameters(self):
        res = super().url_parameters()
        res['type__exact'] = 'PROJ'
        return res

class ProjectAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['blog'].queryset = Blog.objects.filter(type='PROJ')
        self.fields['blog'].widget = BlogRawIdWidget(rel=Project._meta.get_field('blog').remote_field, admin_site=site,
        project_id=self.instance.project.id)

    class Meta:
        # Django 1.8 convenience:
        fields = '__all__'
        model = Project

class ProjectAdmin(admin.ModelAdmin):
    form = ProjectAdminForm
    raw_id_fields = ('blog',)

【讨论】:

【参考方案2】:

我在我的 Django 1.8 / Python 3.4 项目中使用类似于 FSp 的方法:

from django.contrib import admin
from django.contrib.admin import widgets
from django.contrib.admin.sites import site
from django import forms

class BlogRawIdWidget(widgets.ForeignKeyRawIdWidget):
    def url_parameters(self):
        res = super().url_parameters()
        res['type__exact'] = 'PROJ'
        return res

class ProjectAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['blog'].queryset = Blog.objects.filter(type='PROJ')
        self.fields['blog'].widget = BlogRawIdWidget(rel=Project._meta.get_field('blog').remote_field, admin_site=site)

    class Meta:
        # Django 1.8 convenience:
        fields = '__all__'
        model = Project

class ProjectAdmin(admin.ModelAdmin):
    form = ProjectAdminForm
    raw_id_fields = ('blog',)

django.admin 中仅选择blog.type == 'PROJ' 作为外键Project.blog。因为不幸的是,最终用户可能并且愿意选择任何东西。

【讨论】:

如果类型是静态的,我喜欢这个答案,总是“PROJ”。但是,如果您希望它是动态的,例如基于选定的站点,您将如何做到这一点。在主窗体中,您将选择一个站点。然后,您可以将内联形式的 raw_id_field 选择限制为该站点。但我遇到的问题是,表单在用户有机会选择站点之前就被实例化了 在 Django 2.0 中,属性 Field.rel 已被删除 (docs.djangoproject.com/en/2.0/releases/2.0/…)。你应该改用BlogRawIdWidget(rel=Project._meta.get_field('blog').remote_field, admin_site=site) @straykiwi 我找到了一个适用于动态值的解决方案,我已将其添加为新答案:***.com/a/66429352/7560156【参考方案3】:

我创建了一个遗传解决方案来解决自定义参数传递到弹出窗口的问题。 您只需在项目中复制此代码:

from django.contrib.admin import widgets

class GenericRawIdWidget(widgets.ForeignKeyRawIdWidget):
    url_params = []

    def __init__(self, rel, admin_site, attrs=None, \
        using=None, url_params=[]):
        super(GenericRawIdWidget, self).__init__(
            rel, admin_site, attrs=attrs, using=using)
        self.url_params = url_params

    def url_parameters(self):
        """
        activate one or more filters by default
        """
        res = super(GenericRawIdWidget, self).url_parameters()
        res.update(**self.url_params)

        return res

然后,你可以这样使用:

field.widget = GenericRawIdWidget(YOURMODEL._meta.get_field('YOUR_RELATION').rel,
            admin.site, url_params="<YOURMODEL>__id__exact":     object_id)

我是这样用的:

class ANSRuleInline(admin.TabularInline):
    model = ANSRule 
    form = ANSRuleInlineForm
    extra = 1
    raw_id_fields = ('parent',)

    def __init__(self, *args, **kwargs):
        super (ANSRuleInline,self ).__init__(*args,**kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        formfield = super(ANSRuleInline, self).formfield_for_dbfield(db_field, **kwargs)
        request = kwargs.get("request", None)
        object_id = self.get_object(request)

        if db_field.name == 'parent':
            formfield.widget = GenericRawIdWidget(ANSRule._meta.get_field('parent').rel,
                admin.site, url_params="pathology__id__exact": object_id)

        return formfield

    def get_object(self, request):
        object_id = request.META['PATH_INFO'].strip('/').split('/')[-1]
        try:
            object_id = int(object_id)
        except ValueError:
            return None
        return object_id

当我使用GenericRawIdWidget 时,我将一个dict 传递给url_params,它将用于url。

【讨论】:

【参考方案4】:

如果您需要根据模型实例过滤 raw_id list_view 弹出窗口,您可以使用以下示例:

1.编写自定义小部件

class RawIdWidget(widgets.ForeignKeyRawIdWidget):

    def url_parameters(self):
        res = super(RawIdWidget, self).url_parameters()
        object = self.attrs.get('object', None)
        if object:
            # Filter variants by product_id
            res['product_id'] = object.variant.product_id
        return res

2。在表单初始化时传递实例

class ModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(ModelForm, self).__init__(*args, **kwargs)
        obj = kwargs.get('instance', None)
        if obj and obj.pk is not None:
            self.fields['variant'].widget = RawIdWidget(
                rel=obj._meta.get_field('variant').rel,
                admin_site=admin.site,
                # Pass the object to attrs
                attrs='object': obj
            )

【讨论】:

【参考方案5】:

对于实际项目,我发现给定的解决方案(自定义 ModelAdmin 查询集)有点过于严格。

我所做的,通常是这样的:

在我的ModelAdmin 中创建一个自定义过滤器(例如,子类化admin.SimpleListFilter,参见doc)

如下创建我的小部件ForeignKeyRawIdWidget 的子类:

class CustomRawIdWidget(ForeignKeyRawIdWidget):

    def url_parameters(self):
        """
        activate one or more filters by default
        """

        res = super(CustomRawIdWidget, self).url_parameters()

        res["<filter_name>__exact"] = "<filter_value>"

        return res

请注意,自定义小部件的唯一作用是“预选”过滤器,而过滤器又负责“限制”查询集

使用自定义小部件:

class MyForm(forms.ModelForm):

    myfield = forms.ModelChoiceField(queryset=MyModel.objects.all(),
        ...
        widget=CustomRawIdWidget(
             MyRelationModel._meta.get_field('myfield').rel,
             admin.site))

这种方法的一个弱点是小部件选择的过滤器不会阻止从该模型中选择其他实例。如果需要,我会覆盖方法ModelAdmin.save_model(...)(请参阅doc)以检查相关实例是否只是允许的实例。

我发现这种方法有点复杂,但比限制整个 ModelAdmin 的查询集灵活得多。

【讨论】:

【参考方案6】:

下面的方法对我有用,但它是一个查询集,会影响需要使用 Customer 模型的每个管理员。但是,如果您有另一个管理员,例如需要不同查询集的发票,您可能想尝试一下模型代理。

型号

class Customer(models.Model):
    name = models.CharField(max_length=100)
    is_active = models.BooleanField()

class Order(models.Model):
    cust = models.ForeignKey(Customer)

管理员

class CustomerAdmin(admin.ModelAdmin):         
    def queryset(self, request):
        qs = super(CustomerAdmin, self).queryset(request)           
        return qs.filter(is_active=1)

class OrderAdmin():     
    raw_id_fields = ('cust', )    

【讨论】:

谢谢,这实际上非常简单和聪明。此外,您可以检查请求中的 GET 参数并调整过滤器(如果它是弹出窗口等)。 qs.filter(is_active=1) 也会影响CustomerAdmin。如果过滤器参数是从Order 模型定义的,或者参数不适用于CustomerAdmin,这将不起作用。例如,在 OrderAdmin 中,我们希望 cust 仅列出活跃客户,但在 CustomerAdmin 中,我们希望同时列出活跃和非活跃客户。

以上是关于如何限制 Django raw_id_field 的 ForeignKey 选择的选择的主要内容,如果未能解决你的问题,请参考以下文章

Django admin.py 的raw_id_fields属性的作用

django admin TabularInline raw_id_fields 添加查询 搜索小图标显示

为啥我得到一个“(admin.E003)'raw_id_fields [N]'的值必须是ForeignKey或ManyToManyField。” Django应用程序中的错误?

如何强制 Django Admin 使用 select_related?

django-salmonella的使用

django admin中的ForeignKey字段