如何在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)

也就是说,有多个Companies,每个都有RatesClients的范围。每个Client都应该有一个基Rate,这个基Rate是从它的父类Company's Rates中选择的,而不是另一个Company's Rates

当创建一个添加Client的表单时,我想删除Company选项(因为它已经通过Company页面上的“添加客户端”按钮选择),并将Rate选项限制为该Company

在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,我不清楚这是否将在管理界面之外工作。

谢谢。(这似乎是一个非常基本的要求,但如果我应该重新设计一些东西,我愿意听取建议。)

169738 次浏览

ForeignKey由django.forms表示。ModelChoiceField是一个ChoiceField,它的选项是一个模型QuerySet。参见ModelChoiceField的引用。

因此,为字段的queryset属性提供一个QuerySet。这取决于你的表单是如何构建的。如果构建显式表单,则会直接为字段命名。

form.rate.queryset = Rate.objects.filter(company_id=the_company.id)

如果你使用默认的ModelForm对象form.fields["rate"].queryset = ...

这是在视图中显式完成的。不要到处乱黑。

除了S.Lott的回答和评论中提到的becomingGuru,可以通过覆盖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})

这对于重用很有用,比如如果在许多模型上需要通用的过滤器(通常我声明一个抽象的Form类)。如。

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博客材料,那里有很多好的材料。

所以,我真的试图理解这一点,但似乎Django仍然没有使它非常简单。我不是那么笨,但我就是看不到任何(多少)简单的解决办法。

我发现对于这类事情,必须覆盖Admin视图通常是非常丑陋的,而且我发现的每个示例都不能完全应用于Admin视图。

这在我做的模型中是很常见的情况,我发现没有明显的解决方案是令人震惊的……

我有这些类:

# models.py
class Company(models.Model):
# ...
class Contract(models.Model):
company = models.ForeignKey(Company)
locations = models.ManyToManyField('Location')
class Location(models.Model):
company = models.ForeignKey(Company)

这在为Company设置Admin时产生了一个问题,因为它有内联的合同和位置,合同的m2m位置选项没有根据您当前编辑的公司进行适当的过滤。

简而言之,我需要一些管理选项来做这样的事情:

# admin.py
class LocationInline(admin.TabularInline):
model = Location
class ContractInline(admin.TabularInline):
model = Contract
class CompanyAdmin(admin.ModelAdmin):
inlines = (ContractInline, LocationInline)
inline_filter = dict(Location__company='self')

最终,我不关心过滤过程是放在基础CompanyAdmin上,还是放在ContractInline上。(将它放在内联更有意义,但它很难将基契约引用为“self”。)

有人知道像这条急需的捷径一样简单的东西吗?当我为这类事情创建PHP管理员时,这被认为是基本功能!事实上,它总是自动的,如果你真的不想要它,就必须关闭它!

如果你还没有创建表单,想要更改查询集,你可以这样做:

formmodel.base_fields['myfield'].queryset = MyModel.objects.filter(...)

这在使用通用视图时非常有用!

要做到这一点,用一个通用视图,如CreateView…

class AddPhotoToProject(CreateView):
"""
a view where a user can associate a photo with a project
"""
model = Connection
form_class = CreateConnectionForm




def get_context_data(self, **kwargs):
context = super(AddPhotoToProject, self).get_context_data(**kwargs)
context['photo'] = self.kwargs['pk']
context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)
return context
def form_valid(self, form):
pobj = Photo.objects.get(pk=self.kwargs['pk'])
obj = form.save(commit=False)
obj.photo = pobj
obj.save()


return_json = {'success': True}


if self.request.is_ajax():


final_response = json.dumps(return_json)
return HttpResponse(final_response)


else:


messages.success(self.request, 'photo was added to project!')
return HttpResponseRedirect(reverse('MyPhotos'))

最重要的是…

    context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)

, 在这里阅读我的文章

这很简单,适用于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子类化,然后修改下面的方法。最终的结果是用户界面只显示与他们相关的内容,同时允许你(超级用户)看到一切。

我重写了四个方法,前两个方法使用户无法删除任何东西,它还从管理站点删除按钮。

第三个覆盖过滤任何包含引用的查询(在示例“user”或“porcupine”中(仅作为示例)。

最后一个覆盖过滤模型中的任何外键字段,以过滤与基本查询集相同的可用选项。

通过这种方式,您可以提供一个易于管理的前端管理站点,允许用户摆弄自己的对象,并且您不必记得输入我们上面谈到的特定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)

更公开的方法是在Admin类中调用get_form。它也适用于非数据库字段。例如,在这里我有一个名为'_terminal_list'的字段,可以在特殊情况下使用,从get_list(请求)中选择几个终端项,然后根据request.user进行过滤:

class ChangeKeyValueForm(forms.ModelForm):
_terminal_list = forms.ModelMultipleChoiceField(
queryset=Terminal.objects.all() )


class Meta:
model = ChangeKeyValue
fields = ['_terminal_list', 'param_path', 'param_value', 'scheduled_time',  ]


class ChangeKeyValueAdmin(admin.ModelAdmin):
form = ChangeKeyValueForm
list_display = ('terminal','task_list', 'plugin','last_update_time')
list_per_page =16


def get_form(self, request, obj = None, **kwargs):
form = super(ChangeKeyValueAdmin, self).get_form(request, **kwargs)
qs, filterargs = Terminal.get_list(request)
form.base_fields['_terminal_list'].queryset = qs
return form

根据Django文档,你可以使用模型表单的__init__方法对默认的queryset应用过滤器。

https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

class CountryAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['capital'].queryset = self.instance.cities.all()


class CountryAdmin(admin.ModelAdmin):
form = CountryAdminForm

在运行时(例如在CreateView中)限制ModelForm的ForeignKey字段的选择的一个好方法是通过覆盖视图中的get_form_class()来为base_fields['field_name']设置limit_choices_to

例如,在创建客户端时,将费率的选择限制为URL中标识的公司:

class ClientCreateView(LoginRequired, CreateView):
model = Client
fields = '__all__'
    

def get_form_class(self):
modelform = super().get_form_class()
modelform.base_fields['rate'].limit_choices_to = {'company': self.kwargs['company']}
return modelform