问题
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:
- Override get_search_results of the ModelAdmin you are searching for and want to filter
- Use the request referer header to get the magical context you need to apply the appropriate filter based on the source of the relationship
- Grab the limit_choices_to from the appropriate ForeignKey's _meta
- 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