この記事では、DjangoのAdmin内リストページで入力テキストでデータを絞るフィルターを実装する方法を説明します。
この記事の内容です。
対象読者
- DjangoのAdmin内リストページで入力テキストでデータを絞るフィルターが必要な方
環境
- Django (version 3.2.16)
前提条件
なし
何故、参考にしたページの内容ではダメだったのか?
参考にしたページの内容だと動作が以下の通りだった。
- 複数項目を並べても単一項目でしかデータを絞り込めなかった。
- 1の動作のためか、複数項目入力すると最後に入力したデータのみしか復元されなかった。
- フィルタリングを行うにはテキストボックスでEnterを押すしかなく、普段からPC操作に慣れていないユーザにはフレンドリーではないと感じたため。
上記から、それらの問題を解決するために参考にしたページで紹介されていたコードを改造し独自のフィルターを実装した。
DjangoのAdmin内リストページで入力テキストでデータを絞るフィルターを実装
1. Admin内リストページのテンプレート作成
今回は複数条件を元にANDでフィルタリングを行いたかったため、全項目をformタグで囲っている。
また、各項目の横に [適用] ボタンを配置するとくどいと感じたため、フィルターリストの一番上と一番下に [適用] ボタンを配置し、それをクリックすることで入力されたテキストを元にフィルタリングを行うようにしている。
勿論、テキストボックスで入力をした後にEnterを押してもformのsubmit処理が走るため、フィルタリングが行われる。
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_list %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}">
{% if cl.formset %}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">
{% endif %}
{% if cl.formset or action_form %}
<script src="{% url 'admin:jsi18n' %}"></script>
{% endif %}
{{ media.css }}
{% if not actions_on_top and not actions_on_bottom %}
<style>
#changelist table thead th:first-child {width: inherit}
</style>
{% endif %}
{% endblock %}
{% block extrahead %}
{{ block.super }}
{{ media.js }}
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
› {{ cl.opts.verbose_name_plural|capfirst }}
</div>
{% endblock %}
{% endif %}
{% block coltype %}{% endblock %}
{% block content %}
<div id="content-main">
{% block object-tools %}
<ul class="object-tools">
{% block object-tools-items %}
{% change_list_object_tools %}
{% endblock %}
</ul>
{% endblock %}
{% if cl.formset and cl.formset.errors %}
<p class="errornote">
{% if cl.formset.total_error_count == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %}
</p>
{{ cl.formset.non_form_errors }}
{% endif %}
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
<div class="changelist-form-container">
{% block search %}{% search_form cl %}{% endblock %}
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
<form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
{% if cl.formset %}
<div>{{ cl.formset.management_form }}</div>
{% endif %}
{% block result_list %}
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
{% result_list cl %}
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
{% endblock %}
{% block pagination %}{% pagination cl %}{% endblock %}
</form>
</div>
{% block filters %}
{% if cl.has_filters %}
<div id="changelist-filter">
<h2>{% translate 'Filter' %}</h2>
<form method="GET">
<input class="btn btn-info" type="submit" value="{% trans '適用' %}">
{% if cl.has_active_filters %}<h3 id="changelist-filter-clear">
<a href="{{ cl.clear_all_filters_qs }}">✖ {% translate "Clear all filters" %}</a>
</h3>{% endif %}
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
<input class="btn btn-info" type="submit" value="{% trans '適用' %}">
</form>
</div>
{% endif %}
{% endblock %}
</div>
</div>
{% endblock %}
2. Filterで使用するテンプレート作成
以下のファイル名、内容でテンプレートファイルを作成する。
既に適用済みテキストの復元も併せて対応している。
{% load i18n %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
<li>
{% with choices.0 as all_choice %}
<input type="text" name="{{ spec.parameter_name }}" value="{{ spec.used_parameters|get_item:spec.parameter_name|default_if_none:"" }}"/>
{% if spec.used_parameters|get_item:spec.parameter_name|default_if_none:"" %}
<button
type="button"
class="btn btn-info clear-button button"
onclick="location.href='{{ all_choice.query_string }}'"
>
Clear
</button>
{% endif %}
{% endwith %}
</li>
</ul>
3. フィルタークラス作成
アプリのルートディレクトリに、以下のフィルタークラスを実装するためのファイルを作成する。
from django.contrib import admin
from django.db.models import Q
from django.template.defaulttags import register
from .models import Hoge
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)
# ベースクラス
# このクラスを元に各フォーム項目のフィルタークラスを作成する。
class InputTextFilter(admin.SimpleListFilter):
template = 'admin/input_filter.html'
def lookups(self, request, model_admin):
# Dummy, required to show the filter.
return ((),)
def choices(self, changelist):
query_params = changelist.get_filters_params()
query_params.pop(self.parameter_name, None)
all_choice = next(super().choices(changelist))
all_choice['query_params'] = query_params
yield all_choice
def queryset(self, request, queryset):
value = self.value()
if value:
q = Q(**{"%s__icontains" % self.parameter_name: value})
return queryset.filter(q)
class HogeFilters:
class NameFilter(SchoolFilters):
parameter_name = Hoge.name.field.name
title = Hoge.name.field.verbose_name
# 以降、上記のNameFilterの形で対象の項目数フィルタークラスを実装する。
4. Adminに対しフィルター適用
from django.contrib import admin
from .filters import HogeFilters
from .models import Hoge
@admin.register(User)
class HogeAdmin(admin.ModelAdmin):
change_list_template = 'testapp_hoge_change_list.html'
# 省略
list_filter = [
HogeFilters.NameFilter,
# 以降、使用するフィルターを設定する。
]
# 省略
参考にしたページ
codelab.website