19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Django管理画面 list_filterのカスタマイズ

Posted at

list_filterの項目を別のlist_filterの結果で絞り込む方法を調べたのでメモ.

やりたいこと

1対多の関係が連結しているモデルに,list_filterに連結元のモデルを指定する.
この時に,選んだ項目によってlist_filterの項目を減らしたい.

言葉だとわかりづらいのでサンプルとして以下のようなモデルを考える.

Kobito.R4Pvai.png

シフトの管理画面のlist_filterとして店とスタッフを指定する.
ここで店を選択すると,スタッフの選択項目を絞り込みたい.

Kobito.me0mLo.png

結論

admin.RelatedFieldListFilterfield_choices メソッドをオーバーライドし,
list_filter にfield名と作ったクラスをタプルで渡す.

オーバーライドする時に,field.get_choices の呼び出し時に limit_choices_to 引数を渡す.
渡す値は Q オブジェクトか辞書.

店で一度絞り込みをかけ,どういうクエリが発行されるかを確認し,その値で絞り込みをかける.

コード

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)

結果

Kobito.4xD8xK.png

Kobito.lOpw7b.png

店を選択した時にスタッフが所属店で絞り込まれた.

--

調べたこと

ModelAdmin クラスで list_filter を定義すると自動で項目が作成されるので,その部分を確認.

表示用テンプレートの確認

django/contrib/admin/templates/admin/change_list.htmlでフィルターの出力方法を調べると,
admin_list_filter というtemplateタグが使用されていることを確認.

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

出力用テンプレートタグの確認

django/contrib/admin/templatetags/admin_list.pyの418行目から定義は発見.
specの choices(cl) が作成メソッドらしい.

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の正体確認

changelist_viewのほうに戻ってcl.filter_specsをどうやって作ってるかを確認.

django.contrib.admin.views.mainの107行目付近で渡されたlist_filterをごにょごにょしてspecに突っ込んでいるのを確認.
list_filterには文字列以外に関数やタプルも渡せるらしい.
タプルの場合は (field, field_list_filter_class) として定義するらしい.
文字列の場合は適切なfilterを自動作成するらしい.
ということで,specは field_list_filter_class のインスタンスなので,このクラスの choices メソッドを確認しにいく.

ここで print(type(spec)) を挟むとどのクラスを使っているかまで確定する.

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メソッド

django/contrib/admin/filters.pyの197行目に問題のメソッド発見.
解除用の項目(全て)を出した後に,各項目を出すgeneratorメソッドらしい.
選択項目は self.lookup_choices なので,次はこいつを調べる.

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

self.lookup_choicesの確認

同クラス内の __init__メソッドで代入.中身は同クラス内の field_choices メソッドらしい.

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)

field_choicesメソッドの確認

これも同クラス内に定義あり.
fieldのget_choicesメソッドを呼び出している.

django/contrib/admin/filters.py_l.194
    def field_choices(self, field, request, model_admin):
        return field.get_choices(include_blank=False)

ここで再度 print(type(field)) を挟み,fieldのクラスを確認.
出力は以下の通り.

<class 'django.db.models.fields.related.ForeignKey'>

ForeignKey/get_choicesメソッドの確認

django/db/models/fields/related.pyの中を確認するも,get_choicesメソッドの定義は確認できず.
継承元の Field クラスのメソッドを使用している模様

Field/get_choicesメソッドの確認

django/db/models/fields/init.pyの783行目で定義発見.

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

limit_choices_to が設定されていれば complex_filter というメソッドで何かしらのfilterがかかるらしいことを確認.

complex_filterの確認

django/db/models/query.pyの855行目付近で定義あり.

Managerクラスには定義はないが,ManagerクラスはQuerySetクラスのメソッドを呼び出す処理があるので定義場所が異なる.

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)

コメントの通り,Qオブジェクトか辞書を渡せ,とのこと.
というわけで,こいつを渡すように調整しておしまい.

19
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?