77

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.

Jeff Mc
  • 3,723
  • 1
  • 22
  • 27
  • you should not use "null=True", I think. Look it up in the django doc – Ber Oct 24 '08 at 10:02
  • The null=True refers to CharFields. Here, it is perfectly ok to have null=True (otherwise the parents cannot be saved without a child) – Apollo Data Jun 03 '16 at 23:08

11 Answers11

48

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

Update: ForeignKey.limit_choices_to allows one 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.

Dave Mackey
  • 4,306
  • 21
  • 78
  • 136
Ber
  • 40,356
  • 16
  • 72
  • 88
  • @Cerin: Thread are being used anyway in Django. threadlocals is just a way to pass information form the request to a thread in safe way for cases where `self` is not avalable to refer to the request data. – Ber Dec 05 '12 at 08:17
  • 3
    No actual threads are used in the above example, it just uses threadlocals to emulate a wider scope. You could equivalently do it with a value stored on, say, the Model class or anywhere else which will have wide enough scope to be accessed in both places. – jbg Aug 27 '13 at 23:46
  • 1
    note that passing a callable to limit_choices_to is only allowed from Django 1.7 onwards (which was released Sept 3, 2014 -- me wonders how this very relevant information got in this answer when SO claims the last edit was on Nov 16 2008 (!), possibly a hick-up in the time continuum? :-)) – miraculixx Oct 30 '14 at 10:49
  • 3
    @miraculixx I actually found this in the documentation of the time (must have been around version 1.0). And now I have returned to the future :) – Ber Oct 30 '14 at 16:27
  • 2
    There is a feature request to pass an instance argument to a `limit_choices_to` callable, but it seems this will be pretty much impossible to implement, unfortunately. See https://code.djangoproject.com/ticket/25306 – Matthijs Kooijman Nov 07 '19 at 21:57
41

The 'right' way to do it is to use a custom form. From there, you can access self.instance, which is the current object. Example --

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
chris Frisina
  • 19,086
  • 22
  • 87
  • 167
s29
  • 2,027
  • 25
  • 20
  • Hey @s29 I am following your suggestion, but I am getting an `global name 'instance' is not defined`, Can you assist me, please? – slackmart Jan 16 '14 at 19:58
  • @sgmart, presumably you've omitted "self" out of self.instance? – s29 Aug 07 '14 at 01:08
  • This solution works very well for cases where you need the `self.instance` to make the selection. – Bartvds Oct 09 '14 at 14:21
  • 4
    A good idea and works on already created objects, but I can't see how it can be made to work with NEW objects that do not yet have a PK or any data set. Especially inside Inline admin forms, where you might want to filter the data based on the main object. – zeraien Jan 21 '16 at 12:38
  • For Django 1.8 and above you need to declare the fields. For example, after Meta: fields = '__ all __ ' (I've had to put some spaces between the udnerscores and "all" so that they'd show) – S_alj May 17 '17 at 22:37
  • @zeraien What would be the right solution that works also with NEW objects? – VMMF Oct 16 '20 at 01:55
18

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.

White Box Dev
  • 219
  • 2
  • 6
  • 1
    a probably dirty hack way can be parsing pk from url like this: `desired_id = request.META['PATH_INFO'].strip('/').split('/')[-1]` I would love to see any better way to do this in a clean way. – andilabs Jan 08 '15 at 13:36
  • 3
    There is a typo in the last line: `formfield_for_manytomany` -> `formfield_for_foreignkey` – sebastibe Jul 02 '17 at 13:30
13

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.

S.Lott
  • 384,516
  • 81
  • 508
  • 779
Eric Holscher
  • 259
  • 1
  • 7
12

If you only need the limitations in the Django admin interface, this might work. I based it on this answer from another forum - although it's for ManyToMany relationships, you should be able to replace formfield_for_foreignkey for it to work. In 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)
wasabigeek
  • 2,923
  • 1
  • 21
  • 30
  • so... how do you change the `favouritechild`? you have limited the queryset to a single row – Anentropic Jul 29 '15 at 16:38
  • 1
    Good spot. I edited the query, guess this should work. – wasabigeek Jul 29 '15 at 17:54
  • Although this is working, please note that this is not an optimal solution: Django's model admin is not thread-safe. Storing state on the model is not a good idea. See https://code.djangoproject.com/ticket/13659#comment:14 – jnns Jan 12 '23 at 11:34
4

I'm trying to do something similar. It seems like everyone saying 'you should only have a foreign key one way' has maybe misunderstood what you're trying do.

It's a shame the limit_choices_to={"myparent": "self"} you wanted to do doesn't work... that would have been clean and simple. Unfortunately the 'self' doesn't get evaluated and goes through as a plain string.

I thought maybe I could do:

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

But alas that gives an error because the function doesn't get passed a self arg :(

It seems like the only way is to put the logic into all the forms that use this model (ie pass a queryset in to the choices for your formfield). Which is easily done, but it'd be more DRY to have this at the model level. Your overriding the save method of the model seems a good way to prevent invalid choices getting through.

Update
See my later answer for another way https://stackoverflow.com/a/3753916/202168

Arpit Solanki
  • 9,567
  • 3
  • 41
  • 57
Anentropic
  • 32,188
  • 12
  • 99
  • 147
  • There is a feature request to pass an instance argument to a limit_choices_to callable, but it seems this will be pretty much impossible to implement, unfortunately. See code.djangoproject.com/ticket/25306 – Matthijs Kooijman Nov 07 '19 at 21:58
4

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

Jeff Mc
  • 3,723
  • 1
  • 22
  • 27
3

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.

Ber
  • 40,356
  • 16
  • 72
  • 88
2

An alternative approach would be not to have 'favouritechild' fk as a field on the Parent model.

Instead you could have an is_favourite boolean field on the Child.

This may help: https://github.com/anentropic/django-exclusivebooleanfield

That way you'd sidestep the whole problem of ensuring Children could only be made the favourite of the Parent they belong to.

The view code would be slightly different but the filtering logic would be straightforward.

In the admin you could even have an inline for Child models that exposed the is_favourite checkbox (if you only have a few children per parent) otherwise the admin would have to be done from the Child's side.

Anentropic
  • 32,188
  • 12
  • 99
  • 147
  • But it's logical that each Parent has his/her own set of favorite children. Having a Boolean would mean that all Parents have the same set of favorites - which I don't think Jeff wants. – wasabigeek Apr 04 '15 at 13:54
  • no, because each `Child` only belongs to a single `Parent`. If it was a many-to-many relation it would be as you say, in which case the boolean would go on the 'through' model – Anentropic Apr 04 '15 at 17:41
  • That's true. How would you have it displayed in the admin using this method? Based on the question it should be visible when editing a Parent. I don't think limit_choices_to works with through models or foreign keys by default. – wasabigeek Apr 04 '15 at 23:48
  • as I said, you could use an inline admin to edit the related `Child` instances in the `Parent`... all the children would have the `is_favourite` checkbox... there is a bit of a validation issue, scope for a custom widget, but for example if you ticked several of the children - in this case (using `django-exclusivebooleanfield`) the last child would win – Anentropic Jul 29 '15 at 16:36
1

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)
Dcode22
  • 97
  • 4
-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
    ]
  • 1
    Add some explanations to your answer. Posting just code - doesn't help much and doesn't teach a lot too. – admix Feb 14 '16 at 18:17