Note that I investigated how to narrow down the list_filter items by the result of another list_filter.
For the model in which the one-to-many relationship is concatenated, specify the concatenation source model in list_filter. At this time, I want to reduce the items of list_filter according to the selected items.
Since it is difficult to understand in words, consider the following model as a sample.
Specify the store and staff as list_filter on the shift management screen. If you select a store here, you want to narrow down the selection items of the staff.
Override the field_choices
method of ʻadmin.RelatedFieldListFilterand Pass the field name and the created class to
list_filter` as a tuple.
When overriding, pass the limit_choices_to
argument when calling field.get_choices
.
The value to pass is a Q
object or a dictionary.
Narrow down once at the store, check what kind of query is issued, and narrow down by that value.
admin.py
from django.contrib import admin
from django.db.models import Q
from .models import Shift
class StaffFieldListFilter(admin.RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
shop_id = request.GET.get('staff__shop__id__exact')
limit_choices_to = Q(shop_id__exact=shop_id) if shop_id else None
return field.get_choices(include_blank=False, limit_choices_to=limit_choices_to)
class ShiftAdmin(admin.ModelAdmin):
list_display = (
'staff',
'start_at',
'end_at',
)
list_filter = (
'staff__shop',
('staff', StaffFieldListFilter),
)
admin.site.register(Shift, ShiftAdmin)
When selecting a store, the staff was narrowed down by the store to which they belong.
--
If you define list_filter
in the ModelAdmin
class, items will be created automatically, so check that part.
Filter by django / contrib / admin / templates / admin / change_list.html If you check the output method of Confirm that the template tag ʻadmin_list_filter` is used.
About line 66
{% block filters %}
{% if cl.has_filters %}
<div id="changelist-filter">
<h2>{% trans 'Filter' %}</h2>
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
</div>
{% endif %}
{% endblock %}
Defined from line 418 of django / contrib / admin / templatetags / admin_list.py Discovered.
The spec choices (cl)
seems to be the creation method.
Line 418
@register.simple_tag
def admin_list_filter(cl, spec):
tpl = get_template(spec.template)
return tpl.render({
'title': spec.title,
'choices': list(spec.choices(cl)),
'spec': spec,
})
Go back to changelist_view and see how cl.filter_specs is created.
Passed by near line 107 of django.contrib.admin.views.main Confirm that the list_filter is squeezed into the spec.
It seems that functions and tuples can be passed to list_filter in addition to strings.
In the case of tuple, it seems to be defined as (field, field_list_filter_class)
.
In the case of a character string, it seems that an appropriate filter is automatically created.
So, spec is an instance of field_list_filter_class
, so let's check the choices
method of this class.
If you put print (type (spec)) here, you can determine which class you are using.
python:django/contrib/admin/views/main.py_l.107
if self.list_filter:
for list_filter in self.list_filter:
if callable(list_filter):
# This is simply a custom list filter class.
spec = list_filter(request, lookup_params, self.model, self.model_admin)
else:
field_path = None
if isinstance(list_filter, (tuple, list)):
# This is a custom FieldListFilter class for a given field.
field, field_list_filter_class = list_filter
else:
# This is simply a field name, so use the default
# FieldListFilter class that has been registered for
# the type of the given field.
field, field_list_filter_class = list_filter, FieldListFilter.create
if not isinstance(field, models.Field):
field_path = field
field = get_fields_from_path(self.model, field_path)[-1]
lookup_params_count = len(lookup_params)
spec = field_list_filter_class(
field, request, lookup_params,
self.model, self.model_admin, field_path=field_path
)
The problematic method was found in line 197 of django / contrib / admin / filters.py.
It seems to be a generator method that issues each item after issuing the items for cancellation (all
).
The selection item is self.lookup_choices
, so next we will check this one.
python:django/contrib/admin/filters.py_l.197
def choices(self, changelist):
yield {
'selected': self.lookup_val is None and not self.lookup_val_isnull,
'query_string': changelist.get_query_string(
{},
[self.lookup_kwarg, self.lookup_kwarg_isnull]
),
'display': _('All'),
}
for pk_val, val in self.lookup_choices:
yield {
'selected': self.lookup_val == str(pk_val),
'query_string': changelist.get_query_string({
self.lookup_kwarg: pk_val,
}, [self.lookup_kwarg_isnull]),
'display': val,
}
if self.include_empty_choice:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': changelist.get_query_string({
self.lookup_kwarg_isnull: 'True',
}, [self.lookup_kwarg]),
'display': self.empty_value_display,
}
Assigned by init method in the same class. The contents seem to be the field_choices
method in the same class.
python:django/contrib/admin/filters.py_l.161
def __init__(self, field, request, params, model, model_admin, field_path):
other_model = get_model_from_relation(field)
self.lookup_kwarg = '%s__%s__exact' % (field_path, field.target_field.name)
self.lookup_kwarg_isnull = '%s__isnull' % field_path
self.lookup_val = request.GET.get(self.lookup_kwarg)
self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
super().__init__(field, request, params, model, model_admin, field_path)
self.lookup_choices = self.field_choices(field, request, model_admin)
This is also defined in the same class.
The get_choices
method of field is called.
python:django/contrib/admin/filters.py_l.194
def field_choices(self, field, request, model_admin):
return field.get_choices(include_blank=False)
Here, insert print (type (field))
again and check the class of field.
The output is as follows.
<class 'django.db.models.fields.related.ForeignKey'>
Check inside django / db / models / fields / related.py I couldn't confirm the definition of the get_choicesmethod. It seems that the method of the inherited
Field` class is used
Defined in line 783 of django / db / models / fields / __ init__.py Discovered.
python:django/db/models/fields/__init__.py_l.783
def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_choices_to=None):
"""
Return choices with a default blank choices included, for use
as <select> choices for this field.
"""
blank_defined = False
choices = list(self.choices) if self.choices else []
named_groups = choices and isinstance(choices[0][1], (list, tuple))
if not named_groups:
for choice, __ in choices:
if choice in ('', None):
blank_defined = True
break
first_choice = (blank_choice if include_blank and
not blank_defined else [])
if self.choices:
return first_choice + choices
rel_model = self.remote_field.model
limit_choices_to = limit_choices_to or self.get_limit_choices_to()
if hasattr(self.remote_field, 'get_related_field'):
lst = [(getattr(x, self.remote_field.get_related_field().attname),
smart_text(x))
for x in rel_model._default_manager.complex_filter(
limit_choices_to)]
else:
lst = [(x.pk, smart_text(x))
for x in rel_model._default_manager.complex_filter(
limit_choices_to)]
return first_choice + lst
Confirm that if limit_choices_to
is set, some sort of filter will be applied by the method complex_filter
.
Defined in near line 855 of django / db / models / query.py.
There is no definition in the Manager class, but the definition location is different because the Manager class has a process to call the method of the QuerySet class.
python:django/db/models/query.py_l.855
def complex_filter(self, filter_obj):
"""
Return a new QuerySet instance with filter_obj added to the filters.
filter_obj can be a Q object or a dictionary of keyword lookup
arguments.
This exists to support framework features such as 'limit_choices_to',
and usually it will be more natural to use other methods.
"""
if isinstance(filter_obj, Q):
clone = self._chain()
clone.query.add_q(filter_obj)
return clone
else:
return self._filter_or_exclude(None, **filter_obj)
As you can see in the comments, you can pass a Q object or a dictionary. So, I adjusted it to pass this one.
Recommended Posts