How do I restrict foreign keys choices to related objects only in django

Viewed 66085

I have a two way foreign relation similar to the following

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)

How do I restrict the choices for Parent.favoritechild to only children whose parent is itself? I tried

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

but that causes the admin interface to not list any children.

11 Answers

I just came across ForeignKey.limit_choices_to in the Django docs. Not sure yet how this works, but it might just be the right thing here.

Update: ForeignKey.limit_choices_to allows to specify either a constant, a callable or a Q object to restrict the allowable choices for the key. A constant obviously is of no use here, since it knows nothing about the objects involved.

Using a callable (function or class method or any callable object) seems more promising. However, the problem of how to access the necessary information from the HttpRequest object remains. Using thread local storage may be a solution.

2. Update: Here is what has worked for me:

I created a middleware as described in the link above. It extracts one or more arguments from the request's GET part, such as "product=1", and stores this information in the thread locals.

Next there is a class method in the model that reads the thread local variable and returns a list of ids to limit the choice of a foreign key field.

@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

It is important to return a query containing all possible ids if none was selected so that the normal admin pages work ok.

The foreign key field is then declared as:

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

The catch is that you have to provide the information to restrict the choices via the request. I don't see a way to access "self" here.

The new "right" way of doing this, at least since Django 1.1 is by overriding the AdminModel.formfield_for_foreignkey(self, db_field, request, **kwargs).

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

For those who don't want to follow the link below is an example function that is close for the above questions models.

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)

I'm only not sure about how to get the current object that is being edited. I expect it is actually on the self somewhere but I'm not sure.

This isn't how django works. You would only create the relation going one way.

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

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

And if you were trying to access the children from the parent you would do parent_object.child_set.all(). If you set a related_name in the myparent field, then that is what you would refer to it as. Ex: related_name='children', then you would do parent_object.children.all()

Read the docs http://docs.djangoproject.com/en/dev/topics/db/models/#many-to-one-relationships for more.

@Ber: I have added validation to the model similar to this

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)

which works exactly how I want, but it would be really nice if this validation could restrict choices in the dropdown in the admin interface rather than validating after the choice.

Do you want to restrict the choices available in the admin interface when creating/editing a model instance?

One way to do this is validation of the model. This lets you raise an error in the admin interface if the foreign field is not the right choice.

Of course, Eric's answer is correct: You only really need one foreign key, from child to parent here.

A much simpler variation of @s29's answer:

Instead of customising the form, You can simply restrict the choices available in form field from your view:

what worked for me was: in forms.py:

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

in views.py:

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