Django ForeignKey limit_choices_to a different ForeignKey id

Question:

I’m trying to limit Django Admin choices of a ForeignKey using limit_choices_to, but I can’t figure out how to do it properly.

This code does what I want if the category id is 16, but I can’t figure out how to use the current category id rather than hard-coding it.

class MovieCategory(models.Model):    
    category = models.ForeignKey(Category)
    movie = models.ForeignKey(Movie)
    prefix = models.ForeignKey('Prefix', limit_choices_to={'category_id': '16'},
                               blank=True, null=True)
    number = models.DecimalField(verbose_name='Movie Number', max_digits=2,
                                 blank=True, null=True, decimal_places=0)

Is it possible to refer to the id of the category ForeignKey somehow?

Asked By: ti66

||

Answers:

After hours of reading semi related questions I finally figured this out.

You can’t self reference a Model the way I was trying to do so there is no way to make Django act the way I wanted using limit_choices_to because it can’t find the id of a different ForeignKey in the same model.

This can apparently be done if you change the way Django works, but a simpler way to solve this was to make changes to admin.py instead.

Here is what this looks like in my models.py now:

# models.py
class MovieCategory(models.Model):    
    category = models.ForeignKey(Category)
    movie = models.ForeignKey(Movie)
    prefix = models.ForeignKey('Prefix', blank=True, null=True)
    number = models.DecimalField(verbose_name='Movie Number', max_digits=2,
                                 blank=True, null=True, decimal_places=0)

I simply removed limit_choices_to entirely.
I found a similar problem here with the solution posted by Kyle Duncan. The difference though is that this uses ManyToMany and not ForeignKey. That means I had to remove filter_horizontal = ('prefix',) under my class MovieCategoryAdmin(admin.ModelAdmin): as that is only for ManyToMany fields.

In admin.py I had to add from django import forms at the top to create a form. This is how the form looks:

class MovieCategoryForm(forms.ModelForm):

    class Meta:
        model = MovieCategory
        fields = ['prefix']

    def __init__(self, *args, **kwargs):
        super(MovieCategoryForm, self).__init__(*args, **kwargs)
        self.fields['prefix'].queryset = Prefix.objects.filter(
                                        category_id=self.instance.category.id)

And my AdminModel:

class MovieCategoryAdmin(admin.ModelAdmin):
    """
    Admin Class for 'Movie Category'.
    """
    fieldsets = [
        ('Category',      {'fields': ['category']}),
        ('Movie',         {'fields': ['movie']}),
        ('Prefix',        {'fields': ['prefix']}),
        ('Number',        {'fields': ['number']}),
    ]
    list_display = ('category', 'movie', 'prefix', 'number')
    search_fields = ['category__category_name', 'movie__title', 'prefix__prefix']
    form = MovieCategoryForm

This is exactly how Kyle describes it in his answer, except I had to add fields = ['prefix'] to the Form or it wouldn’t run. If you follow his steps and remember to remove filter_horizontal and add the fields you’re using it should work.

Edit: This solution works fine when editing, but not when creating a new entry because it can’t search for the category id when one doesn’t exits. I am trying to figure out how to solve this.

Answered By: ti66

Another approach, if you don’t want to add a custom ModelForm, is to handle this in your ModelAdmin’s get_form() method. This was preferable for me because I needed easy access to the request object for my queryset.

class StoryAdmin(admin.ModelAdmin):

    def get_form(self, request, obj=None, **kwargs):
        form = super(StoryAdmin, self).get_form(request, obj, **kwargs)

        form.base_fields['local_categories'].queryset = LocalStoryCategory.
            objects.filter(office=request.user.profile.office)

        return form
Answered By: tated

I had the same question and your self-answer helped me get started. But I also found another post (question-12399803) that completed the answer, that is, how to filter when creating a new entry.

In views.py

form = CustomerForm(groupid=request.user.groups.first().id)

In forms.py

def __init__(self, *args, **kwargs):
    if 'groupid' in kwargs:
        groupid = kwargs.pop('groupid')
    else:
        groupid = None
    super(CustomerForm, self).__init__(*args, **kwargs)
    if not groupid:
        groupid = self.instance.group.id
    self.fields['address'].queryset = Address.objects.filter(group_id=groupid)

So, whether adding a new customer or updating an existing customer, I can click on a link to go add a new address that will be assigned to that customer.

This is my first answer on StackOverflow. I hope it helps.

Answered By: Greg

Keep in mind that limit_choices_to supports "Either a dictionary, a Q object, or a callable returning a dictionary or Q object" and should theoretically support any lookup that can be done using django’s queryset filtering. A potential solution would then be filtering based on some property of the category that you control such as a slug field.

class MovieCategory(models.Model):    
    category = models.ForeignKey(Category)
    movie = models.ForeignKey(Movie)
    prefix = models.ForeignKey('Prefix', blank=True, null=True,
                               limit_choices_to=Q(category__slug__startswith='movie'))
    number = models.DecimalField(verbose_name='Movie Number', max_digits=2,
                                 blank=True, null=True, decimal_places=0)
Answered By: DragonBobZ
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.