How to filter choices in Django2's autocomplete_fields?

Question:

In Django 2.0, autocomplete_fields was added, which is great.

Without autocomplete_fields, I can change the queryset of a ForeignKeyField using formfield_for_foreignkey.

But combining the two together doesn’t work – it looks like the list of options for autocomplete is dynamic and coming from a different url, instead of from the current form.

So the question is –

How can I change the queryset in the autocomplete widget?

Asked By: Oren Shpigel

||

Answers:

Override the ModelAdmin’s get_search_results method to use the query you want. You can see in the get_queryset method for the view providing the data for autocomplete fields that it’s used to get the queryset – the source as of this answer is https://github.com/django/django/blob/03dbdfd9bbbbd0b0172aad648c6bbe3f39541137/django/contrib/admin/views/autocomplete.py#L42.

Answered By: Peter DeGlopper

If you are using autocomplete_fields for a ManyToManyField on ‘self’,
this example will exclude the current object.

Get the current object’s id by overriding get_form:

field_for_autocomplete = None

def get_form(self, request, obj=None, **kwargs):
    if obj:
        self.field_for_autocomplete = obj.pk

    return super(MyAdmin, self).get_form(request, obj, **kwargs)

Next, override get_search_results. Modify the queryset only for your model’s autocomplete URI:

def get_search_results(self, request, queryset, search_term):
    queryset, use_distinct = super().get_search_results(request, queryset, search_term)

    # Exclude only for autocomplete
    if request.path == '/admin/myapp/mymodel/autocomplete/':
        queryset = queryset.exclude(field=self.field_for_autocomplete)

    return queryset, use_distinct
Answered By: Mike

Short: You can try my solution in django-admin-autocomlete-all or make something similar.

Long answer:

One pain is: limit_choices_to-.. of source foreign key is not implemented too 🙁

I was able to implement filter into get_search_results() of the target ModelAdmin.
But here we have another serious pain.
We can check request.is_ajax and '/autocomplete/' in request.path.

In addition we only have request.headers[‘Referer’]. With help of this we can limit affected foreign keys to 1 model.
But if we have 2+ foreign keys into same target (lets say: two user roles inside the same model instance), we don’t know which one of them calls the ajax.

My idea was modify the url’s. With Request url I was not successfull (after long attempts to find in DOM and in js the select2 elements and extend the url).

But I have some success with modifying of the Referer url (ie. source admin page url) using window.history.replaceState(). I can temporary modify the url like /?key=author – which run always if you will use django-admin-autocomplete-all and I am able to add almost everything into Referer url with additional custom javascript. Especially adding of current values of other form fields could be useful to implement dynamic filtering (dependencies of fields).

So, it is a hack, sure. But you can give try to django-admin-autocomplete-all. – More in docs of it.

Answered By: mirek

I had somehow the same problem, when using autocomplete_fields the limit_choices_to was not taking affect, and then I found a solution for my case which may help others too.
this an idea and a solution for my case, anybody should change the code for his/her use.

imagine we have two models model_A and modle_B:
we are going to override the "get_search_results" of
model-admin of model_A(because model_B has a foreign_key(or m2m) to it)
in my case I just want to limit choices to all model_A objects which
currentlly dont have a model_B connected object(s)
or in case of updating an object of model_B limit to just the previous model_A object(s).
so we go

# moodels.py
class model_A(models.Model):
    name = models.CharField()

class model_B(models.Model):
    name = models.CharField()
    fk_field = models.OneToOneField( #ManyToManyField or ForeignKey
    model_A,
    related_name='fk_reverse',
    on_delete=models.CASCADE)

# admin.py
class model_A_Admin(admin.ModelAdmin):
   search_fields = ('name', )

 def get_search_results(self, request, queryset, search_term):
        import re
        queryset, use_distinct = super().get_search_results(request, queryset, search_term)
    # note: str(request.META.get('HTTP_REFERER')) is the url from which the request had come/previous url.
        if "model_b/add/" in str(request.META.get('HTTP_REFERER')):
        # if we were in creating new model_B instanse page
        # note: the url is somehow containing model_Bs calss name then / then "add"
        # so there is no related object(of model_A) for non exsisting object(of model_B)
            queryset = self.model.objects.filter(fk_reverse=None)
        elif re.search(r"model_b/d/change/", str(request.META.get('HTTP_REFERER'))):
        # if we were in updatineg page of an exsisting model_B instanse
        # the calling page url contains the id of the model_B instanse
        # we are extracting the id and use it for limitaion proccess
            pk = int(re.findall(r'd+', str(str(request.META.get('HTTP_REFERER')).split('/')[-3: ]))[-1])
            queryset = self.model.objects.filter(fk_reverse=pk)
        return queryset, use_distinct
Answered By: mh-firouzjah

You can modify autocomplete queryset just by overriding get_search_results like this:

def get_search_results(self, request, queryset, search_term):
    queryset, use_distinct = super().get_search_results(request, queryset, search_term)
    if 'autocomplete' in request.path:
        queryset = queryset.exclude(field_name='foo')
    return queryset, use_distinct

This function is inside admin.py and use it with model you refer to, if model used in autocomplete is Bar, then get_search_results should be in class BarAdmin(admin.ModelAdmin)

Also if you use same Model in multiple autocomplete_fields you can change queryset depending on where it’s called. Example:

@admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
    fields = (
        'the_field',
    )
    
    autocomplete_fields = ('the_field',)


@admin.register(Bar)
class BarAdmin(admin.ModelAdmin):
    fields = (
        'the_same_field',
    )

    autocomplete_fields = ('the_same_field',)


@admin.register(Key)
class KeyAdmin(admin.ModelAdmin):
    fields = (
        'name',
    )
    ordering = [
        '-id',
    ]
    search_fields = [
        'name',
    ]

    def get_search_results(self, request, queryset, search_term):
        queryset, use_distinct = super().get_search_results(request, queryset, search_term)
        if 'autocomplete' in request.path:
            if 'foo' in request.headers['referer']:
                queryset = queryset.exclude(name='foo')
            elif 'bar' in request.headers['referer']:
                queryset = queryset.exclude(name='bar')
        return queryset, use_distinct

We got Foo and Bar models with ForeingKeys to Key Model and used with autocomplete.
Now when we open autocomplete changing Foo, we will only see queryset with name equal to ‘foo’, also when we open autocomplete in Bar, we will only see queryset with name equal to ‘bar’, this way you can modify queryset depending on where is autocomplete called.

Answered By: blakrul