How to filter choices in Django2's autocomplete_fields?
Asked Answered
T

6

33

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?

Twirl answered 8/1, 2018 at 15:4 Comment(0)
T
9

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.

Tighten answered 8/1, 2018 at 15:31 Comment(2)
Correct me if I'm wrong, but when overriding get_search_results every ForeignKey to the model is affected? In my case I have a FK parent to 'self' and I want to prevent selecting some model as it's own parent and only allow toplevel models (parent=None) as a parent. At the same time other models should be able to assign every entry as a FKLegwork
I haven't dug deeply into this, but off the cuff you might be able to do that by inspecting the request argument to get_search_results. It should have the PK of the object you're editing, so you can exclude that. And it'll have the URL, so if you only want autocompletes to limit to only top level instances from that admin view you could do that. If you want an autocomplete with such constraints for one field and one without them for a different field, it's harder - you might need to set up a subclass of the json view to handle it. Connecting that to the admin might be hard, though.Tighten
U
13

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
Uglify answered 20/5, 2019 at 19:6 Comment(2)
I use part of your answer, it works, thank you so much. BTW the request.path== can be found in admin page source code, search: data-ajax--url. I didn't use /admin/ as my admin URL path. So it didn't work at the beginning.Cedar
You make my dayBegird
T
9

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.

Tighten answered 8/1, 2018 at 15:31 Comment(2)
Correct me if I'm wrong, but when overriding get_search_results every ForeignKey to the model is affected? In my case I have a FK parent to 'self' and I want to prevent selecting some model as it's own parent and only allow toplevel models (parent=None) as a parent. At the same time other models should be able to assign every entry as a FKLegwork
I haven't dug deeply into this, but off the cuff you might be able to do that by inspecting the request argument to get_search_results. It should have the PK of the object you're editing, so you can exclude that. And it'll have the URL, so if you only want autocompletes to limit to only top level instances from that admin view you could do that. If you want an autocomplete with such constraints for one field and one without them for a different field, it's harder - you might need to set up a subclass of the json view to handle it. Connecting that to the admin might be hard, though.Tighten
M
3

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.

Mantic answered 27/1, 2020 at 15:59 Comment(0)
S
3

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
Surrey answered 14/6, 2020 at 12:5 Comment(1)
You need to include if 'autocomplete' in request.path: or you could experience strange results if you are trying link from the admin change form with the autocomplete you're filtering to the list view of the model you're filtering search results for. Look at @blakrul's answer in this thread to see it in context (bottom of second code block has this check and checking for substrings of the http referer). Hope this saves someone from feeling crazy like I just was!Hobble
H
2

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.

Handbill answered 24/1, 2023 at 11:50 Comment(0)
C
0

Another way to filter the options of an autocomplete field is through the limit_choices_to parameter of the relation field in the model.

Here is an example which limits the cities shown in the autocomplete field to cities in Europe.


class Location(models.Model):
    ...
    cities = models.ManyToManyField(City, limit_choices_to=Q(region="Europe"))
    ...

@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
    ...
    autocomplete_fields = ["cities"]

Chancemedley answered 5/10, 2023 at 17:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.