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


227

说我的内容如下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也不清楚是否可以在管理界面之外使用。

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


感谢您对“ limit_choices_to”的提示。它不能解决你的问题,但我:-)文档:docs.djangoproject.com/en/dev/ref/models/fields/...
guettli

Answers:


243

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

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


好的,这听起来很有希望。如何访问相关的Field对象?form.company.QuerySet = Rate.objects.filter(company_id = the_company.id)吗?还是通过字典?
汤姆(Tom)

1
好的,感谢您扩展示例,但是我似乎不得不使用form.fields [“ rate”]。queryset来避免“'ClientForm'对象没有属性'rate'”,我是否缺少某些内容?(并且您的示例也应该是form.rate.queryset以保持一致。)
汤姆(Tom

8
在表单的__init__方法中设置字段的查询集会更好吗?
Lakshman Prasad

1
@SLott最后的评论不正确(否则我的网站不应该工作:)。您可以通过在覆盖的方法中使用super(...).__ init__进行调用来填充验证数据。如果您要对这些查询集进行多次更改,则可以通过覆盖init方法来打包它们,以使其更加美观。
迈克尔

3
@Slott欢呼雀跃,我添加了一个答案,因为这需要600多个字符来解释。即使这个问题很老,它的google评分也很高。
迈克尔,

135

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


第一个代码段中有一个错字,您在__init __()中两次定义了args,而不是args和kwargs。
tpk

6
我更喜欢这个答案,我认为将表单初始化逻辑封装在form类中而不是在view方法中更为干净。干杯!
2012年

44

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

1
而且我应该补充一点,对于具有相似参考领域的多个模型管理员来说,这很适合作为通用的自定义表单。
nemesisfixx

如果您使用的是Django 1.4+,这是最好的答案
Rick Westera

16

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

在这里阅读我的文章


4

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

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

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


2

因此,我确实试图理解这一点,但是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管理员进行此类工作时,这被视为基本功能!实际上,它始终是自动的,如果您真的不想要它,则必须将其禁用!


0

一种更公共的方法是在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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.