How to filter ModelAdmin autocomplete_fields results with the context of limit_choices_to

佐手、 提交于 2020-05-15 08:04:34

问题


I have a situation where I wish to utilize Django's autocomplete admin widget, that respects a referencing models field limitation.

For example I have the following Collection model that has the attribute kind with specified choices.

class Collection(models.Model):
    ...
    COLLECTION_KINDS = (
        ('personal', 'Personal'),
        ('collaborative', 'Collaborative'),
    )

    name = models.CharField()
    kind = models.CharField(choices=COLLECTION_KINDS)
    ...

Another model ScheduledCollection references Collection with a ForeignKey field that implements limit_choices_to option. The purpose of this model is to associate meta data to a Collection for a specific use case.

class ScheduledCollection(models.Model):
    ...
    collection = models.ForeignKey(Collection, limit_choices_to={'kind': 'collaborative'})

    start_date = models.DateField()
    end_date = models.DateField()
    ...

Both models are registered with a ModelAdmin. The Collection model implements search_fields.

@register(models.Collection)
class CollectionAdmin(ModelAdmin):
    ...
    search_fields = ['name']
    ...

The ScheduledCollection model implements autocomplete_fields

@register(models.ScheduledCollection)
class ScheduledCollectionAdmin(ModelAdmin):
    ...
    autocomplete_fields = ['collection']
    ...

This works but not entirely as expected. The autocomplete retrieves results from a view generated by the Collection model. The limit_choices_to do not filter the results and are only enforced upon save.

It has been suggested to implement get_search_results or get_queryset on the CollectionAdmin model. I was able to do this and filter the results. However, this changes Collection search results across the board. I am unaware of how to attain more context within get_search_results or get_queryset to conditionally filter the results based upon a relationship.

In my case I would like to have several choices for Collection and several meta models with different limit_choices_to options and have the autocomplete feature respect these restrictions.

I don't expect this to work automagically and maybe this should be a feature request. At this point I am at a loss how to filter the results of a autocomplete with the respect to a choice limitation (or any condition).

Without using autocomplete_fields the Django admin's default <select> widget filters the results.


回答1:


Triggering off the http referer was ugly so I made a better version: subclass the AutocompleteSelect and send extra query parameters to allow get_search_results to lookup the correct limit_choices_to automagically. Simply include this mixin in your ModelAdmin (for both source and target models). As a bonus it also adds a delay to the ajax requests so you don't spam the server as you type in the filter, makes the select wider and sets the search_fields attribute (to 'translations__name' which is correct for my system, customise for yours or omit and set individually on the ModelAdmins as before):

from django.contrib.admin import widgets
from django.utils.http import urlencode


class AutocompleteSelect(widgets.AutocompleteSelect):
    """
    Improved version of django's autocomplete select that sends an extra query parameter with the model and field name
    it is editing, allowing the search function to apply the appropriate filter.
    Also wider by default, and adds a debounce to the ajax requests
    """

    def __init__(self, rel, admin_site, attrs=None, choices=(), using=None, for_field=None):
        super().__init__(rel, admin_site, attrs=attrs, choices=choices, using=using)
        self.for_field = for_field

    def build_attrs(self, base_attrs, extra_attrs=None):
        attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
        attrs.update({
            'data-ajax--delay': 250,
            'style': 'width: 50em;'
        })
        return attrs

    def get_url(self):
        url = super().get_url()
        url += '?' + urlencode({
            'app_label': self.for_field.model._meta.app_label,
            'model_name': self.for_field.model._meta.model_name,
            'field_name': self.for_field.name
        })
        return url


class UseAutocompleteSelectMixin:
    """
    To avoid ForeignKey fields to Event (such as on ReportColumn) in admin from pre-loading all events
    and thus being really slow, we turn them into autocomplete fields which load the events based on search text
    via an ajax call that goes through this method.
    Problem is this ignores the limit_choices_to of the original field as this ajax is a general 'search events'
    without knowing the context of what field it is populating. Someone else has exact same problem:
    https://stackoverflow.com/questions/55344987/how-to-filter-modeladmin-autocomplete-fields-results-with-the-context-of-limit-c
    So fix this by adding extra query parameters on the autocomplete request,
    and use these on the target ModelAdmin to lookup the correct limit_choices_to and filter with it.
    """

    # Overrides django.contrib.admin.options.BaseModelAdmin#formfield_for_foreignkey
    # Is identical except in case db_field.name is in autocomplete fields it constructs our improved AutocompleteSelect
    # instead of django's and passes it extra for_field parameter
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name in self.get_autocomplete_fields(request):
            db = kwargs.get('using')
            kwargs['widget'] = AutocompleteSelect(db_field.remote_field, self.admin_site, using=db, for_field=db_field)
            if 'queryset' not in kwargs:
                queryset = self.get_field_queryset(db, db_field, request)
                if queryset is not None:
                    kwargs['queryset'] = queryset

            return db_field.formfield(**kwargs)

        return super().formfield_for_foreignkey(db_field, request, **kwargs)

    # In principle we could add this override in a different mixin as adding the formfield override above is needed on
    # the source ModelAdmin, and this is needed on the target ModelAdmin, but there's do damage adding everywhere so combine them.
    def get_search_results(self, request, queryset, search_term):
        if 'app_label' in request.GET and 'model_name' in request.GET and 'field_name' in request.GET:
            from django.apps import apps
            model_class = apps.get_model(request.GET['app_label'], request.GET['model_name'])
            limit_choices_to = model_class._meta.get_field(request.GET['field_name']).get_limit_choices_to()
            if limit_choices_to:
                queryset = queryset.filter(**limit_choices_to)
        return super().get_search_results(request, queryset, search_term)

    search_fields = ['translations__name']




回答2:


I had the exact same problem. It's a bit hacky, but here's my solution:

  1. Override get_search_results of the ModelAdmin you are searching for and want to filter
  2. Use the request referer header to get the magical context you need to apply the appropriate filter based on the source of the relationship
  3. Grab the limit_choices_to from the appropriate ForeignKey's _meta
  4. Pre-filter the queryset and then pass to super method.

So for your models:

@register(models.Collection)
class CollectionAdmin(ModelAdmin):
    ...
    search_fields = ['name']

    def get_search_results(self, request, queryset, search_term):
        if '<app_name>/scheduledcollection/' in request.META.get('HTTP_REFERER', ''):
            limit_choices_to = ScheduledCollection._meta.get_field('collection').get_limit_choices_to()
            queryset = queryset.filter(**limit_choices_to)
        return super().get_search_results(request, queryset, search_term)

A shortcoming of this approach is the only context we have is the model being edited in admin, rather than which field of the model, so if your ScheduledCollection model has 2 collection autocomplete fields (say personal_collection and collaborative_collection) with different limit_choices_to we can't infer this from the referer header and treat them differently. Also inline admins will have the referer url based on the parent thing they are an inline for, rather than reflecting their own model. But it works in the basic cases.

Hopefully a new version of Django will have a cleaner solution, such as the autocomplete select widget sending an extra query parameter with the model and field name it is editing so that get_search_results can accurately look up the required filters instead of (potentially inaccurately) inferring from the referer header.



来源:https://stackoverflow.com/questions/55344987/how-to-filter-modeladmin-autocomplete-fields-results-with-the-context-of-limit-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!