【问题标题】:How do I filter ForeignKey choices in a Django ModelForm?如何过滤 Django ModelForm 中的 ForeignKey 选择?
【发布时间】:2010-09-22 11:02:07
【问题描述】:

假设我的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 都应该有一个从其父Company's Rates 中选择的基Rate,而不是另一个Company's Rates

创建用于添加Client 的表单时,我想删除Company 选项(因为已经通过Company 页面上的“添加客户端”按钮选择了该选项)并限制@987654336 @ 也可以选择 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/…
  • 如今,如果您使用的是通用编辑视图(CreateView 等),那么我在 ModelForm 上过滤 ForeignKey 选择的首选方法是在视图中覆盖 get_form_class()。然后您可以设置 base_fields['my_field_name'].limit_choices_to - 例如见stackoverflow.com/questions/70399761

标签: python django django-forms


【解决方案1】:

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) ?还是通过字典?
  • 好的,感谢您扩展示例,但我似乎必须使用 form.fields["rate"].queryset 来避免“'ClientForm' 对象没有属性 'rate'”,是吗?遗漏了什么? (并且您的示例也应该是 form.rate.queryset 以保持一致。)
  • 在表单的__init__方法中设置字段的查询集不是更好吗?
  • @SLott 最后的评论不正确(或者我的网站不应该工作:)。您可以通过在覆盖的方法中使用 super(...).__init__ 调用来填充验证数据。如果您要进行其中几个查询集更改,则通过覆盖 init 方法来打包它们会更加优雅。
  • @Slott 欢呼,我添加了一个答案,因为它需要 600 多个字符来解释。即使这个问题很老,它的谷歌分数也很高。
【解决方案2】:

除了 S.Lott 的回答和 cmets 中提到的 becomeGuru 之外,还可以通过覆盖 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 博客材料,其中有很多不错的。

【讨论】:

  • 第一个代码 sn-p 中有一个错字,您在 __init__() 中定义了两次 args 而不是 args 和 kwargs。
  • 我更喜欢这个答案,我认为将表单初始化逻辑封装在表单类中而不是视图方法中更干净。干杯!
【解决方案3】:

这很简单,适用于 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)

【讨论】:

  • 我应该补充一点,这对于具有相似的感兴趣参考字段的多个模型管理员来说,这是一个通用的自定义表单。
  • 如果您使用的是 Django 1.4+,这是最好的答案
【解决方案4】:

使用通用视图来执行此操作,例如 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)

read my post here

【讨论】:

  • 我首先将查询集赋值放在表单中,但出现了表单错误。通过将查询集分配移动到视图,不再出错。
【解决方案5】:

如果您尚未创建表单并想更改查询集,您可以这样做:

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

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

【讨论】:

    【解决方案6】:

    所以,我真的试图理解这一点,但似乎 Django 仍然没有让这变得非常简单。我并不是那么愚蠢,但我只是看不到任何(有点)简单的解决方案。

    我发现必须为这类事情覆盖管理视图通常很丑陋,而且我发现的每个示例都从未完全适用于管理视图。

    在我制作的模型中,这种情况很常见,以至于我发现没有明显的解决方案,这令人震惊……

    我有这些课程:

    # 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 上。 (将它放在内联上更有意义,但很难将基本 Contract 引用为 'self'。)

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

    【讨论】:

      【解决方案7】:

      更公开的方式是在管理类中调用 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
      

      【讨论】:

        【解决方案8】:

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

        例如,在创建客户时,将 Rate 的选择限制为 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
        

        【讨论】:

          【解决方案9】:

          根据 Django 文档,您可以使用模型表单的 __init__ 方法将过滤器应用于默认查询集。

          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
          

          【讨论】:

            猜你喜欢
            • 2018-02-25
            • 2022-11-12
            • 2020-01-14
            • 2014-12-10
            • 2022-01-20
            • 2017-07-26
            • 2012-04-29
            相关资源
            最近更新 更多