如何在Django ModelForm中过滤ForeignKey选项?
假设我在我的 models.py
文件中有以下内容:
class Company(models.Model):
name = ...
class Rate(models.Model):
company = models.ForeignKey(Company)
name = ...
class Client(models.Model):
name = ...
company = models.ForeignKey(Company)
base_rate = models.ForeignKey(Rate)
也就是说,有多个 公司
,每个公司都有一系列的 费率
和 客户
。每个 客户
应该有一个基础的 费率
,这个费率是从它所属的 公司的费率
中选择的,而不是从其他公司的费率中选择。
在创建一个添加 客户
的表单时,我想去掉 公司
的选择(因为这个已经通过在 公司
页面上的“添加客户”按钮选择过了),并且将 费率
的选择限制为该 公司
的费率。
我该如何在 Django 1.0 中实现这个功能呢?
我现在的 forms.py
文件目前只是一些基础代码:
from models import *
from django.forms import ModelForm
class ClientForm(ModelForm):
class Meta:
model = Client
而 views.py
也很简单:
from django.shortcuts import render_to_response, get_object_or_404
from models import *
from forms import *
def addclient(request, company_id):
the_company = get_object_or_404(Company, id=company_id)
if request.POST:
form = ClientForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(the_company.get_clients_url())
else:
form = ClientForm()
return render_to_response('addclient.html', {'form': form, 'the_company':the_company})
在 Django 0.96 中,我能够通过在渲染模板之前做一些类似下面的操作来实现这个功能:
manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]
ForeignKey.limit_choices_to
看起来很有希望,但我不知道怎么传入 the_company.id
,而且我不确定这是否能在管理界面之外工作。
谢谢。(这看起来是个很基本的请求,但如果我需要重新设计什么,我也乐意听取建议。)
9 个回答
这很简单,适用于Django 1.4:
class ClientAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ClientAdminForm, self).__init__(*args, **kwargs)
# access object through self.instance...
self.fields['base_rate'].queryset = Rate.objects.filter(company=self.instance.company)
class ClientAdmin(admin.ModelAdmin):
form = ClientAdminForm
....
你不需要在表单类中指定这些内容,而是可以直接在ModelAdmin中做,因为Django已经在ModelAdmin中包含了这个内置方法(根据文档):
ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs)¶
'''The formfield_for_foreignkey method on a ModelAdmin allows you to
override the default formfield for a foreign keys field. For example,
to return a subset of objects for this foreign key field based on the
user:'''
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
还有一种更聪明的方法(比如在创建一个用户可以访问的前端管理界面时),就是继承ModelAdmin,然后修改下面的方法。这样做的结果是,用户界面只会显示与用户相关的内容,而你(超级用户)可以看到所有内容。
我重写了四个方法,前两个方法让用户无法删除任何东西,同时也从管理网站上移除了删除按钮。
第三个重写的方法会过滤掉任何包含(在这个例子中是'用户'或'刺猬',只是举个例子)的查询。
最后一个重写的方法会过滤模型中的任何外键字段,以便过滤出与基本查询集相同的可选项。
通过这种方式,你可以呈现一个易于管理的前端管理网站,让用户可以处理自己的对象,而你不需要记得输入我们上面提到的特定ModelAdmin过滤器。
class FrontEndAdmin(models.ModelAdmin):
def __init__(self, model, admin_site):
self.model = model
self.opts = model._meta
self.admin_site = admin_site
super(FrontEndAdmin, self).__init__(model, admin_site)
移除“删除”按钮:
def get_actions(self, request):
actions = super(FrontEndAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
防止删除权限
def has_delete_permission(self, request, obj=None):
return False
过滤在管理网站上可以查看的对象:
def get_queryset(self, request):
if request.user.is_superuser:
try:
qs = self.model.objects.all()
except AttributeError:
qs = self.model._default_manager.get_queryset()
return qs
else:
try:
qs = self.model.objects.all()
except AttributeError:
qs = self.model._default_manager.get_queryset()
if hasattr(self.model, ‘user’):
return qs.filter(user=request.user)
if hasattr(self.model, ‘porcupine’):
return qs.filter(porcupine=request.user.porcupine)
else:
return qs
过滤管理网站上所有外键字段的选择:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if request.employee.is_superuser:
return super(FrontEndAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
else:
if hasattr(db_field.rel.to, 'user'):
kwargs["queryset"] = db_field.rel.to.objects.filter(user=request.user)
if hasattr(db_field.rel.to, 'porcupine'):
kwargs["queryset"] = db_field.rel.to.objects.filter(porcupine=request.user.porcupine)
return super(ModelAdminFront, self).formfield_for_foreignkey(db_field, request, **kwargs)
除了S.Lott的回答,还有成为Guru在评论中提到的,我们可以通过重写ModelForm.__init__
这个函数来添加查询集的过滤器。(这同样适用于普通表单)这样做可以帮助我们重用代码,并且让视图函数看起来更整洁。
class ClientForm(forms.ModelForm):
def __init__(self,company,*args,**kwargs):
super (ClientForm,self ).__init__(*args,**kwargs) # populates the post
self.fields['rate'].queryset = Rate.objects.filter(company=company)
self.fields['client'].queryset = Client.objects.filter(company=company)
class Meta:
model = Client
def addclient(request, company_id):
the_company = get_object_or_404(Company, id=company_id)
if request.POST:
form = ClientForm(the_company,request.POST) #<-- Note the extra arg
if form.is_valid():
form.save()
return HttpResponseRedirect(the_company.get_clients_url())
else:
form = ClientForm(the_company)
return render_to_response('addclient.html',
{'form': form, 'the_company':the_company})
这在需要在多个模型上使用相同的过滤器时特别有用(通常我会声明一个抽象的表单类)。例如:
class UberClientForm(ClientForm):
class Meta:
model = UberClient
def view(request):
...
form = UberClientForm(company)
...
#or even extend the existing custom init
class PITAClient(ClientForm):
def __init__(company, *args, **args):
super (PITAClient,self ).__init__(company,*args,**kwargs)
self.fields['support_staff'].queryset = User.objects.exclude(user='michael')
除此之外,我只是重申了一些Django博客的内容,网上有很多不错的资源。
ForeignKey在Django中是用django.forms.ModelChoiceField来表示的。简单来说,ModelChoiceField就是一个选择字段,它的选项来自于一个模型的查询集(QuerySet)。想了解更多,可以查看ModelChoiceField的文档。
所以,你需要把一个查询集提供给这个字段的queryset
属性。这取决于你是怎么构建你的表单的。如果你是手动创建表单,那么字段的名字会直接对应。
form.rate.queryset = Rate.objects.filter(company_id=the_company.id)
如果你使用的是默认的ModelForm对象,像这样:form.fields["rate"].queryset = ...
这个操作是在视图中明确进行的,不需要搞什么复杂的操作。