[PYTHON] Django admin screen list_filter customization

Note that I investigated how to narrow down the list_filter items by the result of another list_filter.

Thing you want to do

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.

Kobito.R4Pvai.png

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.

Kobito.me0mLo.png

Conclusion

Override the field_choices method of ʻadmin.RelatedFieldListFilterand Pass the field name and the created class tolist_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.

code

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)

result

Kobito.4xD8xK.png

Kobito.lOpw7b.png

When selecting a store, the staff was narrowed down by the store to which they belong.

--

What I looked up

If you define list_filter in the ModelAdmin class, items will be created automatically, so check that part.

Check the display template

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 %}

Check the output template tag

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,
    })

Spec identity confirmation

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
                    )

RelatedFieldListFilter / choices method

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,
            }

Check self.lookup_choices

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)

Check the field_choices method

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 the ForeignKey / get_choices method

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 inheritedField` class is used

Check the Field / get_choices method

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.

Check 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

Django admin screen list_filter customization
Django admin screen customization first step
Django admin screen reverse lookup memo
Django2 screen addition flow
Tuning your Django admin site
When you forget your admin screen username / password in Django
The story of failing to update "calendar.day_abbr" on the admin screen of django
Django desired shift batch input screen
How to make only one data register on the Django admin screen