iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Django Adminでフィルター機能(SimpleListFilter)をAND検索にカスタマイズする方法について

こんにちは、iimonでサーバーサイドエンジニアをしています。hoge1です。 本記事はiimonアドベントカレンダー18日目の記事となります。

はじめに

iimonではDjangoで作られたアプリケーションの管理画面としてDjango Adminを使っています。

皆さん会社はどうですか?

フレームワークに付属の管理画面を使われていないところも多いかと思います。

今回はDjango Adminのデフォルトのテキスト検索をOR検索からAND検索への変更について書きたいと思います。

Django Adminを使われていない人はデフォルトではできないだと知って頂き、使われている人は色々な方法の1つとして、こんな方法でも実現できるんだと知って頂けると幸いです。

デフォルトだと検索はどうなる?

実はDjango Adminはデフォルトでデータの検索はできません。 検索項目を出すとこからスタートで、例えばiimonは不動産テック企業ですので物件名と管理会社というデータがあったとしましょう。 するとデフォルトではこのような画面になります。

すみません、時間の都合上、弊社のコードを改変して作っているので機密情報の部分は赤で塗りつぶしています。

まずは下記コードを追加してフィルター機能を見てみましょう。 list_filterのプロパティを入れるだけです。

アプリケーション名/admin.py

from django.contrib import admin
from .models import Bukken

class BukkenAdmin(admin.ModelAdmin):
    list_filter = ("name", "management_company_name",)
    list_display: tuple[str, ...] = (
        "name",
        "management_company_name",
    )


admin.site.register(Bukken, BukkenAdmin)

これだけだと検索フォームではなく全てのデータの羅列がされます。

これではデータ数が多いと使い物になりません。

ということでadmin.SimpleListFilterクラスを継承して新しくクラスを作って検索フォームを使います。

AND検索ってこれでいけんじゃねぇ?

AND検索の仕方を検索していてこれでいけんじゃね?と思った方法が以下です。

アプリケーション名/admin.py

from django.contrib import admin
from .models import Bukken


class InputFilter(admin.SimpleListFilter):
    template = "admin/input_filter.html"

    def lookups(self, request, model_admin):
        # Dummy, required to show the filter.
        return ((),)

    def choices(self, changelist):
        # Grab only the "all" option.
        all_choice = next(super().choices(changelist))
        all_choice["query_parts"] = (
            (k, v)
            for k, v in changelist.get_filters_params().items()
            if k != self.parameter_name
        )
        yield all_choice


class BukkeNameFilter(InputFilter):
    parameter_name = "name"
    title = "物件名"

    def queryset(self, request, queryset):
        if self.value() is not None:
            name = self.value()

            return queryset.filter(name__icontains=name)


class ManagementNameFilter(InputFilter):
    parameter_name = "management_company_name"
    title = "管理会社名"

    def queryset(self, request, queryset):
        if self.value() is not None:
            management_company_name = self.value()

            return queryset.filter(
                management_company_name__icontains=management_company_name
            )


class BukkenAdmin(admin.ModelAdmin):
    list_filter = ("name", "management_company_name",)
    list_display: tuple[str, ...] = (
        "name",
        "management_company_name",
    )


admin.site.register(Bukken, BukkenAdmin)

hoge/hoge/アプリケーション名/templates/admin/input_filter.html

{% load i18n %}

<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<form name="andSearch" method="GET" action="">
    {% for k, v in all_choice.query_parts %}
        <input type="hidden" name="{{ k }}" value="{{ v }}" />
    {% endfor %}
    <input
        type="text"
        value="{{ spec.value|default_if_none:'' }}"
        name="{{ spec.parameter_name }}"/>
    <input type="submit" value="送信"">
</form>

色々と検索しているとSimpleListFilterクラスを継承して必要なメソッドをオーバーライドすることでテンプレートでhiddenの引数として格納すればできるよ!という話でしたので試してみましたが実際の挙動としてはall_choice.query_partsに何も渡されていなくできませんでした。

また、OR検索とはなるもののAND検索には至りませんでした。 しかし、

?name=マンション&management_company_name=イイモン

のようにクエリパラメータを直接いじるとAND検索ができることを確認しました。 よってどうにかしたらDjangoでできるのでは?と思って色々調べたのですが、探し方が悪いのかそれらしき情報には辿り付けませんでした。

実際にAND検索を行った実装方法

1つのSimpleListFilterのテンプレートにinputタグを増やしたりとか色々試したのですが結果できませんでした。 で、どうしたかと言いますとこのようにしました。

アプリケーション名/admin.py

from django.contrib import admin
from .models import Bukken


class InputFilter(admin.SimpleListFilter):

    def lookups(self, request, model_admin):
        # Dummy, required to show the filter.
        return ((),)

    def choices(self, changelist):
        # Grab only the "all" option.
        all_choice = next(super().choices(changelist))
        all_choice["query_parts"] = (
            (k, v)
            for k, v in changelist.get_filters_params().items()
            if k != self.parameter_name
        )
        yield all_choice


class BukkeNameFilter(InputFilter):
    template = "admin/input_filter.html"
    parameter_name = "name"
    title = "物件名"

    def queryset(self, request, queryset):
        if self.value() is not None:
            name = self.value()

            return queryset.filter(name__icontains=name)


class ManagementNameFilter(InputFilter):
    template = "admin/and_search_filter.html"
    parameter_name = "management_company_name"
    title = "管理会社名"

    def queryset(self, request, queryset):
        if self.value() is not None:
            management_company_name = self.value()

            return queryset.filter(
                management_company_name__icontains=management_company_name
            )


class BukkenAdmin(admin.ModelAdmin):
    list_filter = ("name", "management_company_name",)
    list_display: tuple[str, ...] = (
        "name",
        "management_company_name",
    )


admin.site.register(Bukken, BukkenAdmin)

hoge/hoge/アプリケーション名/templates/admin/input_filter.html

{% load i18n %}

<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<form name="andSearch2" method="GET" action="">
    <input type="text" name="dummy" style="display:none;">
    <input
        type="text"
        value="{{ spec.value|default_if_none:'' }}"
        name="{{ spec.parameter_name }}"/>
</form>

hoge/hoge/アプリケーション名/templates/admin/and_search_filter.html

{% load i18n %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<form name="andSearch" method="GET" action="" onsubmit="return submitSearch()">
    <input type="text" name="dummy" style="display:none;">
    <input
        type="text"
        value="{{ spec.value|default_if_none:'' }}"
        name="{{ spec.parameter_name }}"/>
    <input type="button" value="送信" onclick="submitAndSearch();">
</form>
<script>
    let params = new URLSearchParams(document.location.search);
    if(params.get('name') != null) {
        document.querySelector('input[name="name"]').value = params.get('name');
    }
    if (params.get('management_company_name') != null) {
        document.querySelector('input[name="management_company_name"]').value = params.get('management_company_name');
    }
    function submitAndSearch() {
        let params = new URLSearchParams();;
        if (document.querySelector('input[name="name"]').value != "") {
            params.set("name", document.querySelector('input[name="name"]').value);
        }
        if (document.querySelector('input[name="management_company_name"]').value != "") {
            params.set("management_company_name", document.querySelector('input[name="management_company_name"]').value);
        }
        window.location.href = "?" + params.toString();
    }
</script>

このようにしました。 要はjavascriptで検索の送受信部分を書いちゃったわけです。 Djangoの標準機能でのやり方が分からなかったのでこういう形に行き着きました。 これのミソは各テンプレートで

<input type="text" name="dummy" style="display:none;">

これを入れている所です。 これはフォームに入力後にエンターキーを押して検索がかからないようにする為のものです。 今回は検索ボタンを押してAND検索させるコードになっていますので間違って検索できないようにしています。

あとはSimpleListFilterは1つで1つのform要素を作ってしまうのでテンプレートを分けている所です。検索に関わるsubmitは最後のSimpleListFilterのテンプレートに書いています。(JSは全部読んでから実行しないといけないので)

最後に

色々検索して思考錯誤したのですがDjangoの標準機能での実現の仕方が分からず、くしくもJavascriptで実装することになりました。

他にいい方法があればどなたか教えて下さいw

今後はpythonを書き始めて1年くらいになるのですがコミュニティに属さずやってきたので来年は積極的に参加していきたいです。

是非、おすすめのコミュニティがあったら教えてください。

この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします! Wantedly / Green

次回はフロントエンドエンジニアの山Pさんです!どんな記事か楽しみです!

参考