如何仅在django中将外键选择限制为相关对象

时间:2008-10-24 03:52:50

标签: python django django-models

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

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

但这导致管理界面没有列出任何子项。

11 个答案:

答案 0 :(得分:31)

我刚刚在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,
    },
)

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

答案 1 :(得分:27)

“正确”的方法是使用自定义表单。从那里,您可以访问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

答案 2 :(得分:14)

这种新的“正确”方式,至少在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)

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

答案 3 :(得分:12)

这不是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()

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

答案 4 :(得分:5)

如果您只需要Django管理界面中的限制,这可能会有效。我基于另一个论坛的this answer - 虽然它适用于ManyToMany关系,但您应该能够替换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)

答案 5 :(得分:3)

在创建/编辑模型实例时,是否要限制管理界面中的可用选项?

执行此操作的一种方法是验证模型。如果外部字段不是正确的选择,则可以在管理界面中引发错误。

当然,Eric的答案是正确的:你只需要一个外键,从孩子到父母。

答案 6 :(得分: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)

这正是我想要的,但如果这个验证可以限制管理界面下拉菜单中的选择而不是在选择后进行验证,那将是非常好的。

答案 7 :(得分:2)

我正在尝试做类似的事情。似乎每个人都说'你应该只有一个外键'可能会误解你正在尝试做什么。

令人遗憾的是,你想做的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})

但是由于函数没有通过自我arg传递错误:(

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

<强>更新
以另一种方式https://stackoverflow.com/a/3753916/202168

查看我以后的回答

答案 8 :(得分:1)

另一种方法是不要将'favouritechild'fk作为父模型的字段。

相反,你可以在Child上有一个is_favourite布尔字段。

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

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

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

在管理员中,你甚至可以为子模型公开内联is_favourite复选框(如果你每个父母只有几个孩子),否则管理员必须从孩子那边完成。

答案 9 :(得分:0)

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

答案 10 :(得分:-1)

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
    ]