2

How can I filter a queryset inside the Admin page of an object that has a ManyToManyField relation with a manually defined through model?

Given models.py

class Foo(models.Model):
    foo_field1 = models.CharField(max_length=50)

class Main(models.Model):
    main_field1 = models.CharField(max_length=50)
    m2mfield = models.ManyToManyField(Foo, through="FooBar")

class FooBar(models.Model):
    main = models.ForeignKey(Main, on_delete=models.CASCADE)
    foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
    new_field = models.CharField(max_length=50)

Inside admin.py

class M2MInlineAdmin(admin.TabularInline):
    model = Main.m2mfield.through
    extra = 1

class MainAdmin(admin.ModelAdmin):
   inlines = [M2MInlineAdmin,]
   ...

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        print('called formfield_for_manytomany')
        return super().formfield_for_manytomany(db_field, request, **kwargs)

    def get_field_queryset(self, db, db_field, request):
        print('called get_field_queryset')
        return super().get_field_queryset(db, db_field, request)

I try to access both of these methods, but none of them are called if I specify a through table. However, they do get called if the ManyToMany relation is simply defined as like this:

class Main(models.Model):
    main_field1 = models.CharField(max_length=50)
    m2mfield = models.ManyToManyField(Foo)

Is there a method to filter the queryset when a through table is specified (while being able to access the request context)?

EDIT:

The methods are indeed called when the ManyToManyField has a through model specified, only if there are no fieldsets specified inside the modelAdmin class.

How to access these methods when fieldsets are defined?

drec4s
  • 7,946
  • 8
  • 33
  • 54
  • I am using django==2.2.3 – drec4s Jul 30 '19 at 15:30
  • @ChillarAnand I want to filter in create/change view. – drec4s Jul 30 '19 at 16:16
  • Upgraded to 2.2.3 and tested on it. On change/create view, it is always calling formfield_for_manytomany. Not able to reproduce the mentioned scenario. https://github.com/ChillarAnand/library/blob/master/trash/admin.py Can you paste entire model admin code? Is there something else which is missing from the above snippet? – Chillar Anand Jul 30 '19 at 16:27
  • @ChillarAnand well, I investigated a bit and noticed that the methods are indeed called, but I am specifying `fieldsets` and for some reason if this variable is defined, the methods are not called... – drec4s Jul 30 '19 at 17:47

2 Answers2

2

formfield_for_manytomany method seems to be called only when default form is used. When fieldsets is defined, it is using a different form which is why above method is not getting called.

Since you are using tabular admin for many to many field, you can override get_queryset to filter with field.

class M2MInlineAdmin(admin.TabularInline):
    model = Main.fruits.through
    extra = 1

    def get_queryset(self, request):
        qs = super(M2MInlineAdmin, self).get_queryset(request)
        qs = qs.filter(some_arg=some_value)
        return qs

Alternatively, you can write a custom model form and use it in admin instead of default form.

class MainAdminForm(forms.ModelForm):

    class Meta:
        model = Main
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # custom setup


class MainAdmin(admin.ModelAdmin):
    form = MainAdminForm
Chillar Anand
  • 27,936
  • 9
  • 119
  • 136
1

You can use the formfield_for_foreignkey() method on the inline class.

class M2MInlineAdmin(admin.TabularInline):
    model = Main.m2mfield.through
    extra = 1

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
bromosapien
  • 234
  • 1
  • 5
  • I need to access the request context while doing this filtering (`object.id` and `request.user`) – drec4s Jul 25 '19 at 20:26
  • I'm getting `inlineformset_factory() got an unexpected keyword argument 'user'` – drec4s Jul 25 '19 at 20:55
  • @drec4s did you add the init method above to the custom formset? The user line above `super()` is important. It allows you to use `self.user` in your filtering. – bromosapien Jul 25 '19 at 21:36
  • yes, copied exactly as you posted, I am setting `self.user` above the `super()` method inside `__init__` – drec4s Jul 25 '19 at 21:42
  • Wow, I was definitely thinking too hard.. see above for a much easier solution. – bromosapien Jul 25 '19 at 22:23