18

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?

Dave Mackey
  • 4,306
  • 21
  • 78
  • 136
ti66
  • 439
  • 1
  • 4
  • 14
  • If you're happy with just raising a validation error, see http://stackoverflow.com/q/37946885/247696 – Flimm Jun 21 '16 at 14:15

5 Answers5

25

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.

Dave Mackey
  • 4,306
  • 21
  • 78
  • 136
ti66
  • 439
  • 1
  • 4
  • 14
  • 1
    Because I've been pretty busy I've coded around the problem of creating new items. I currently create new items within the category model rather than this one and since Prefix has a foreignkey linking it to that the problem is eliminated for me. If I do find a way to solve this I will update my solution, but as it stands this does what it needs to for my application. – ti66 Jul 25 '15 at 06:01
  • where did you defined Prefix model? or what is this? `self.fields['prefix'].queryset = Prefix.objects.filter( category_id=self.instance.category.id)` – khashashin Apr 10 '18 at 12:42
6

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
tated
  • 621
  • 7
  • 11
3

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)
DragonBobZ
  • 2,194
  • 18
  • 31
  • How can i use the limit_choices_to referencing to a parent id? – Jcc.Sanabria Jul 10 '21 at 21:31
  • @Jcc.Sanabria If you had a `Book` belongs to `Author` relationship with a foreignkey set on `Book` (`Book.author_id`) and you wanted to filter your book choices to those belonging to a particular author, you should be able to do `limit_choices_to=Q(author_id=1)`. If you instead have a `ManyToMany('myapp.Book')` field on `Author`, it would be something like `limit_choices_to=Q(author__id=1)` because you are using a reverse accessor. For more, read about the [ManyToMany](https://docs.djangoproject.com/en/3.2/topics/db/examples/many_to_many/) field. But I dont recommend hard-coding filters by id. – DragonBobZ Jul 12 '21 at 16:14
2

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.

Greg
  • 31
  • 3
0

Well, I'm not sure why are you doing this. But I think there's easier way to set category for "MovieCategory" in views.py.

For example if you have urls:

path('category/<int:prefix_pk>/create', views.MovieCategoryCreateView.as_view(), name='movie_category_create'),

And then you can use prefix_pk in views.py:

def post(self, request, **kwargs):
    prefix = get_object_or_404(Prefix, id=self.kwargs['prefix_pk'])
    form = MovieCategoryForm(request.POST)

        if not form.is_valid():
            ctx = {'form': form, 'prefix ': prefix }
            return render(request, self.template_name, ctx)

        movie_category = form.save(commit=False)
        movie_category.prefix = prefix 
        movie_category.save()
    return redirect(reverse...)
Nursultan
  • 1
  • 1