如何在Django ModelForm中过滤ForeignKey选择?

问题:如何在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,每个都有一个Rates和的范围Clients。每个数据库Client都应有一个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也不清楚是否可以在管理界面之外使用。

谢谢。(这似乎是一个非常基本的要求,但是如果我应该重新设计一些内容,我可以提出建议。)

Say I have the following in my 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)

I.e. there are multiple Companies, each having a range of Rates and Clients. Each Client should have a base Rate that is chosen from it’s parent Company's Rates, not another Company's Rates.

When creating a form for adding a Client, I would like to remove the Company choices (as that has already been selected via an “Add Client” button on the Company page) and limit the Rate choices to that Company as well.

How do I go about this in Django 1.0?

My current forms.py file is just boilerplate at the moment:

from models import *
from django.forms import ModelForm

class ClientForm(ModelForm):
    class Meta:
        model = Client

And the views.py is also basic:

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})

In Django 0.96 I was able to hack this in by doing something like the following before rendering the template:

manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]

ForeignKey.limit_choices_to seems promising but I don’t know how to pass in the_company.id and I’m not clear if that will work outside the Admin interface anyway.

Thanks. (This seems like a pretty basic request but if I should redesign something I’m open to suggestions.)


回答 0

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 = ...

这是在视图中明确完成的。不得乱动。

ForeignKey is represented by django.forms.ModelChoiceField, which is a ChoiceField whose choices are a model QuerySet. See the reference for ModelChoiceField.

So, provide a QuerySet to the field’s queryset attribute. Depends on how your form is built. If you build an explicit form, you’ll have fields named directly.

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

If you take the default ModelForm object, form.fields["rate"].queryset = ...

This is done explicitly in the view. No hacking around.


回答 1

除了S.Lott的回答以及在注释中提到的成为Guru之外,还可以通过覆盖该ModelForm.__init__函数来添加queryset过滤器。(这可以很容易地应用于常规表单)它可以帮助重用并保持视图功能整洁。

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博客材料,其中有很多不错的材料。

In addition to S.Lott’s answer and as becomingGuru mentioned in comments, its possible to add the queryset filters by overriding the ModelForm.__init__ function. (This could easily apply to regular forms) it can help with reuse and keeps the view function tidy.

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})

This can be useful for reuse say if you have common filters needed on many models (normally I declare an abstract Form class). E.g.

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')

Other than that I’m just restating Django blog material of which there are many good ones out there.


回答 2

这很简单,并且可以在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)

This is simple, and works with 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
    ....

You don’t need to specify this in a form class, but can do it directly in the ModelAdmin, as Django already includes this built-in method on the ModelAdmin (from the docs):

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)

An even niftier way to do this (for example in creating a front-end admin interface that users can access) is to subclass the ModelAdmin and then alter the methods below. The net result is a user interface that ONLY shows them content that is related to them, while allowing you (a super-user) to see everything.

I’ve overridden four methods, the first two make it impossible for a user to delete anything, and it also removes the delete buttons from the admin site.

The third override filters any query that contains a reference to (in the example ‘user’ or ‘porcupine’ (just as an illustration).

The last override filters any foreignkey field in the model to filter the choices available the same as the basic queryset.

In this way, you can present an easy to manage front-facing admin site that allows users to mess with their own objects, and you don’t have to remember to type in the specific ModelAdmin filters we talked about above.

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)

remove ‘delete’ buttons:

    def get_actions(self, request):
        actions = super(FrontEndAdmin, self).get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

prevents delete permission

    def has_delete_permission(self, request, obj=None):
        return False

filters objects that can be viewed on the admin site:

    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

filters choices for all foreignkey fields on the admin site:

    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)

回答 3

为此,可以使用通用视图,例如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)

在这里阅读我的文章

To do this with a generic view, like 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'))

the most important part of that…

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

, read my post here


回答 4

如果尚未创建表单,但想更改查询集,则可以执行以下操作:

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

当您使用通用视图时,这非常有用!

If you haven’t created the form and want to change the queryset you can do:

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

This is pretty useful when you are using generic views!


回答 5

因此,我确实试图理解这一点,但是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)

在为公司设置管理员时,这会产生问题,因为它同时具有合同和位置的内联,并且根据您当前正在编辑的公司未正确过滤合同的“位置”的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上。(将其放在内联中更有意义,但是很难将基本合同称为“自身”。)

有没有人知道像这个急需的捷径一样简单的东西?早在我让PHP管理员进行此类工作时,这被视为基本功能!实际上,它始终是自动的,如果您真的不想要它,则必须将其禁用!

So, I’ve really tried to understand this, but it seems that Django still doesn’t make this very straightforward. I’m not all that dumb, but I just can’t see any (somewhat) simple solution.

I find it generally pretty ugly to have to override the Admin views for this sort of thing, and every example I find never fully applies to the Admin views.

This is such a common circumstance with the models I make that I find it appalling that there’s no obvious solution to this…

I’ve got these classes:

# 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)

This creates a problem when setting up the Admin for Company, because it has inlines for both Contract and Location, and Contract’s m2m options for Location are not properly filtered according to the Company that you’re currently editing.

In short, I would need some admin option to do something like this:

# 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')

Ultimately I wouldn’t care if the filtering process was placed on the base CompanyAdmin, or if it was placed on the ContractInline. (Placing it on the inline makes more sense, but it makes it hard to reference the base Contract as ‘self’.)

Is there anyone out there who knows of something as straightforward as this badly needed shortcut? Back when I made PHP admins for this sort of thing, this was considered basic functionality! In fact, it was always automatic, and had to be disabled if you really didn’t want it!


回答 6

一种更公共的方法是在Admin类中调用get_form。它也适用于非数据库字段。例如,这里我在表单上有一个名为“ _terminal_list”的字段,在特殊情况下可用于从get_list(request)中选择多个终端项,然后根据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

A more public way is by calling get_form in Admin classes. It also works for non-database fields too. For example here i have a field called ‘_terminal_list’ on the form that can be used in special cases for choosing several terminal items from get_list(request), then filtering based on 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