【问题标题】:How do I restrict foreign keys choices to related objects only in django如何仅在 django 中将外键选择限制为相关对象
【发布时间】:2010-09-18 22:43:46
【问题描述】:

我有一个类似于以下的双向外来关系

class Parent(models.Model):
  name = models.CharField(max_length=255)
  favoritechild = models.ForeignKey("Child", blank=True, null=True)

class Child(models.Model):
  name = models.CharField(max_length=255)
  myparent = models.ForeignKey(Parent)

如何将 Parent.favoritechild 的选择限制为只有其父母是其自身的孩子?我试过了

class Parent(models.Model):
  name = models.CharField(max_length=255)
  favoritechild = models.ForeignKey("Child", blank=True, null=True, limit_choices_to = {"myparent": "self"})

但这会导致管理界面不列出任何子项。

【问题讨论】:

  • 我认为你不应该使用“null=True”。在 django 文档中查找它
  • null=True 指的是 CharFields。在这里,设置 null=True 是完全可以的(否则没有孩子就无法保存父母)

标签: python django django-models


【解决方案1】:

这不是 django 的工作方式。您只会创建一种方式的关系。

class Parent(models.Model):
  name = models.CharField(max_length=255)

class Child(models.Model):
  name = models.CharField(max_length=255)
  myparent = models.ForeignKey(Parent)

如果你试图从父母那里访问孩子,你会这样做 parent_object.child_set.all()。如果您在 myparent 字段中设置了一个 related_name,那么这就是您将其称为的名称。例如:related_name='children',那么你会做parent_object.children.all()

阅读docshttp://docs.djangoproject.com/en/dev/topics/db/models/#many-to-one-relationships了解更多信息。

【讨论】:

    【解决方案2】:

    您想在创建/编辑模型实例时限制管理界面中可用的选项吗?

    一种方法是验证模型。如果外部字段不是正确的选择,这可以让您在管理界面中引发错误。

    当然,Eric 的回答是正确的:你真的只需要一个外键,这里从子到父。

    【讨论】:

      【解决方案3】:

      @Ber:我已经在与此类似的模型中添加了验证

      class Parent(models.Model):
        name = models.CharField(max_length=255)
        favoritechild = models.ForeignKey("Child", blank=True, null=True)
        def save(self, force_insert=False, force_update=False):
          if self.favoritechild is not None and self.favoritechild.myparent.id != self.id:
            raise Exception("You must select one of your own children as your favorite")
          super(Parent, self).save(force_insert, force_update)
      

      这完全符合我的要求,但如果此验证可以限制管理界面下拉菜单中的选择,而不是在选择后进行验证,那就太好了。

      【讨论】:

        【解决方案4】:

        我刚刚在 Django 文档中遇到了ForeignKey.limit_choices_to。 尚不确定这是如何工作的,但这里可能是正确的。

        更新: ForeignKey.limit_choices_to 允许指定常量、可调用对象或 Q 对象来限制键的允许选择。一个常量显然在这里没有用,因为它对所涉及的对象一无所知。

        使用可调用(函数或类方法或任何可调用对象)似乎更有希望。但是,如何从 HttpRequest 对象访问必要信息的问题仍然存在。使用thread local storage 可能是一个解决方案。

        2。更新:这对我有用:

        我创建了上面链接中描述的中间件。它从请求的 GET 部分中提取一个或多个参数,例如“product=1”,并将此信息存储在线程本地变量中。

        接下来模型中有一个类方法,读取线程局部变量,返回一个id列表来限制外键字段的选择。

        @classmethod
        def _product_list(cls):
            """
            return a list containing the one product_id contained in the request URL,
            or a query containing all valid product_ids if not id present in URL
        
            used to limit the choice of foreign key object to those related to the current product
            """
            id = threadlocals.get_current_product()
            if id is not None:
                return [id]
            else:
                return Product.objects.all().values('pk').query
        

        如果没有选择任何可能的 ID,则返回包含所有可能 ID 的查询非常重要,这样正常的管理页面可以正常工作。

        然后将外键字段声明为:

        product = models.ForeignKey(
            Product,
            limit_choices_to={
                id__in=BaseModel._product_list,
            },
        )
        

        问题是您必须提供信息以通过请求限制选择。我在这里看不到访问“自我”的方法。

        【讨论】:

        • @Cerin:在 Django 中无论如何都在使用线程。对于 self 无法引用请求数据的情况,threadlocals 只是一种将请求中的信息以安全的方式传递给线程的方法。
        • 在上面的例子中没有使用实际的线程,它只是使用 threadlocals 来模拟更广泛的范围。您可以等效地使用存储在 Model 类或其他任何地方的值来执行此操作,这些地方将具有足够广泛的范围以在两个地方都可以访问。
        • 请注意,仅从 Django 1.7 开始(2014 年 9 月 3 日发布)才允许将可调用对象传递给 limit_choices_to(2014 年 9 月 3 日发布——我想知道当 SO 声称上次编辑是2008 年 11 月 16 日(!),可能是时间连续体中的一个小插曲?:-))
        • @miraculixx 我实际上是在当时的文档中发现的(一定是在 1.0 版左右)。而现在我又回到了未来:)
        • 有一个功能请求将实例参数传递给 limit_choices_to 可调用对象,但不幸的是,这似乎几乎不可能实现。见code.djangoproject.com/ticket/25306
        【解决方案5】:

        我正在尝试做类似的事情。似乎每个人都说“你应该只使用一种方式的外键”可能误解了你正在尝试做的事情。

        很遗憾,您想要做的 limit_choices_to={"myparent": "self"} 不起作用......这本来是干净和简单的。不幸的是,'self' 没有被评估,而是作为一个纯字符串传递。

        我想也许我可以做到:

        class MyModel(models.Model):
            def _get_self_pk(self):
                return self.pk
            favourite = models.ForeignKey(limit_choices_to={'myparent__pk':_get_self_pk})
        

        但是由于该函数没有传递一个自我参数,所以会出现错误:(

        似乎唯一的方法是将逻辑放入使用此模型的所有表单中(即将查询集传递给表单字段的选项)。这很容易完成,但在模型级别拥有它会更加干燥。您覆盖模型的保存方法似乎是防止无效选择通过的好方法。

        更新
        另一种方式见我稍后的答案https://stackoverflow.com/a/3753916/202168

        【讨论】:

        • 有一个功能请求将实例参数传递给可调用的 limit_choices_to,但不幸的是,这似乎几乎不可能实现。见 code.djangoproject.com/ticket/25306
        【解决方案6】:

        另一种方法是不将“favouritechild”fk 作为父模型上的字段。

        相反,您可以在 Child 上设置一个 is_favourite 布尔字段。

        这可能会有所帮助: https://github.com/anentropic/django-exclusivebooleanfield

        这样你就可以回避确保孩子只能成为他们所属父母的最爱的整个问题。

        视图代码会略有不同,但过滤逻辑会很简单。

        在管理员中,您甚至可以有一个内联子模型,该模型暴露了 is_favourite 复选框(如果您每个父级只有几个子级),否则管理员必须从子级一侧完成。

        【讨论】:

        • 但每个家长都有自己喜欢的孩子是合乎逻辑的。拥有一个布尔值意味着所有父母都有相同的收藏夹 - 我认为 Jeff 不想要。
        • 不,因为每个Child 只属于一个Parent。如果它是多对多关系,就像你说的那样,在这种情况下,布尔值将继续“通过”模型
        • 确实如此。您将如何使用此方法将其显示在管理员中?根据问题,在编辑父级时它应该是可见的。我认为 limit_choices_to 默认情况下不能通过模型​​或外键使用。
        • 正如我所说,您可以使用内联管理员来编辑Parent 中的相关Child 实例...所有孩子都会有is_favourite 复选框...有一个有点验证问题,自定义小部件的范围,但例如,如果您勾选了几个孩子 - 在这种情况下(使用 django-exclusivebooleanfield)最后一个孩子会赢
        【解决方案7】:

        这样做的新“正确”方式,至少从 Django 1.1 开始是通过覆盖 AdminModel.formfield_for_foreignkey(self, db_field, request, **kwargs)。

        见http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

        对于那些不想遵循以下链接的人来说,这是一个与上述问题模型相近的示例函数。

        class MyModelAdmin(admin.ModelAdmin):
            def formfield_for_foreignkey(self, db_field, request, **kwargs):
                if db_field.name == "favoritechild":
                    kwargs["queryset"] = Child.objects.filter(myparent=request.object_id)
                return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
        

        我只是不确定如何获取正在编辑的当前对象。我希望它实际上是在某个地方,但我不确定。

        【讨论】:

        • 一种可能是肮脏的黑客方式可以像这样从 url 解析 pk:desired_id = request.META['PATH_INFO'].strip('/').split('/')[-1] 我很想看到任何更好的方式来以一种干净的方式做到这一点。
        • 最后一行有错字:formfield_for_manytomany -> formfield_for_foreignkey
        【解决方案8】:

        “正确”的做法是使用自定义表单。从那里,您可以访问 self.instance,它是当前对象。示例 --

        from django import forms
        from django.contrib import admin 
        from models import *
        
        class SupplierAdminForm(forms.ModelForm):
            class Meta:
                model = Supplier
                fields = "__all__" # for Django 1.8+
        
        
            def __init__(self, *args, **kwargs):
                super(SupplierAdminForm, self).__init__(*args, **kwargs)
                if self.instance:
                    self.fields['cat'].queryset = Cat.objects.filter(supplier=self.instance)
        
        class SupplierAdmin(admin.ModelAdmin):
            form = SupplierAdminForm
        

        【讨论】:

        • 嘿@s29 我正在听从你的建议,但我收到了一个global name 'instance' is not defined,你能帮我吗?
        • @sgmart,大概你在 self.instance 中省略了“self”?
        • 此解决方案非常适用于需要self.instance 进行选择的情况。
        • 一个好主意,适用于已创建的对象,但我看不出如何使其适用于尚未具有 PK 或任何数据集的新对象。尤其是在内联管理表单中,您可能希望根据主对象过滤数据。
        • 对于 Django 1.8 及更高版本,您需要声明字段。例如,在 Meta: fields = '__ all __ ' 之后(我必须在 udnerscores 和 "all" 之间放置一些空格以便它们显示)
        【解决方案9】:

        如果您只需要 Django 管理界面中的限制,这可能会起作用。我基于来自另一个论坛的this answer - 虽然它是针对多对多关系的,但您应该能够替换formfield_for_foreignkey 以使其正常工作。在admin.py:

        class ParentAdmin(admin.ModelAdmin):
            def get_form(self, request, obj=None, **kwargs):
                self.instance = obj
                return super(ParentAdmin, self).get_form(request, obj=obj, **kwargs)
        
            def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
                if db_field.name == 'favoritechild' and self.instance:       
                    kwargs['queryset'] = Child.objects.filter(myparent=self.instance.pk)
                return super(ChildAdmin, self).formfield_for_foreignkey(db_field, request=request, **kwargs)
        

        【讨论】:

        • 那么...你如何更改favouritechild?您已将查询集限制为单行
        • 好地方。我编辑了查询,猜想这应该可行。
        【解决方案10】:
        from django.contrib import admin
        from sopin.menus.models import Restaurant, DishType
        
        class ObjInline(admin.TabularInline):
            def __init__(self, parent_model, admin_site, obj=None):
                self.obj = obj
                super(ObjInline, self).__init__(parent_model, admin_site)
        
        class ObjAdmin(admin.ModelAdmin):
        
            def get_inline_instances(self, request, obj=None):
                inline_instances = []
                for inline_class in self.inlines:
                    inline = inline_class(self.model, self.admin_site, obj)
                    if request:
                        if not (inline.has_add_permission(request) or
                                inline.has_change_permission(request, obj) or
                                inline.has_delete_permission(request, obj)):
                            continue
                        if not inline.has_add_permission(request):
                            inline.max_num = 0
                    inline_instances.append(inline)
        
                return inline_instances
        
        
        
        class DishTypeInline(ObjInline):
            model = DishType
        
            def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
                field = super(DishTypeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
                if db_field.name == 'dishtype':
                    if self.obj is not None:
                        field.queryset = field.queryset.filter(restaurant__exact = self.obj)  
                    else:
                        field.queryset = field.queryset.none()
        
                return field
        
        class RestaurantAdmin(ObjAdmin):
            inlines = [
                DishTypeInline
            ]
        

        【讨论】:

        • 为您的答案添加一些解释。只发布代码 - 没有多大帮助,也没有太多教益。
        【解决方案11】:

        @s29 答案的一个更简单的变体:

        而不是自定义表单, 您可以从视图中简单地限制表单字段中可用的选项:

        对我有用的是: 在forms.py中:

        class AddIncomingPaymentForm(forms.ModelForm):
            class Meta: 
                model = IncomingPayment
                fields = ('description', 'amount', 'income_source', 'income_category', 'bank_account')
        

        在views.py中:

        def addIncomingPayment(request):
            form = AddIncomingPaymentForm()
            form.fields['bank_account'].queryset = BankAccount.objects.filter(profile=request.user.profile)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-10-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-11-20
          • 1970-01-01
          相关资源
          最近更新 更多