如何限制Django raw_id_field的ForeignKey选项
在Django的后台管理中,如何限制在使用raw_id_fields选项时显示的ForeignKey
字段的选择项?
当这个字段以下拉框的形式展示时,我们可以很简单地定义一个自定义的ModelForm
,来设置这个字段的查询集(也就是可选项)。不过,当使用raw_id_fields
来展示时,这个查询集似乎完全被忽略了。它会生成一个链接到这个ForeignKey
所对应模型的窗口,让你可以通过弹出窗口选择该模型中的任何记录。虽然你仍然可以通过自定义URL来过滤这些值,但我找不到通过ModelAdmin
来实现这个的办法。
6 个回答
我创建了一个通用的解决方案,用来处理传递给弹出窗口的自定义参数。
你只需要把这段代码复制到你的项目里:
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
时,我把一个字典传给了 url_params,这个字典会用在网址上。
如果你想根据模型实例来过滤你的 raw_id 列表视图弹出窗口,可以参考下面的例子:
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}
)
我觉得这个解决方案(自定义 ModelAdmin
的查询集)对实际项目来说有点太严格了。
我通常会这样做:
- 在我的
ModelAdmin
中创建一个自定义过滤器(比如说,继承admin.SimpleListFilter
,具体可以参考文档) 创建我自己的小部件类
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(...)
方法(可以参考文档),来检查相关的实例是否都是允许的。
我觉得这种方法虽然稍微复杂一点,但比起限制整个 ModelAdmin
的查询集来说,灵活性要高得多。
下面这个方法对我来说是有效的,但它是一个查询集,会影响到每个需要使用客户模型的管理员。如果你有另一个管理员,比如发票管理员,需要不同的查询集,你可能需要尝试一下模型代理。
模型
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', )
在我的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
。因为最终用户可能会选择任何东西,这点很不幸。