django-datatables-viewで、フォームの入力値に基づいてDataTableの絞り込みを行おうと考えた時に詰まったことがあったため、メモを残します。
目次
環境
なお、サンプルの見栄えを良くするため、Bootstrap 4.5.2 も使っています。
また、今回の実装ではDataTableのインスタンスをどこかに保持する必要がある & わかりやすくするために、JavaScriptの class
を使っています*1。
DataTableのSearchについて
DataTableには標準でSearch機能があります。
Searchはリアルタイムで行われます。例えば、
という元データに対し、 Search欄に入力すると
と絞り込みが行われます。
やりたいこと
状況によってはSearch欄以外にフォームを設けて、そこで絞り込みを行いたいこともあるかもしれません。
例えば、フォームに入力してFilterボタンを押すと、元データを
と絞り込みたいとします。
実装
フロントエンド
Server Side Processingの作りとほぼ変わりありません。
ただ、filterボタンを押した時に絞り込みを行うため、
addEventHandler() {
$('#filter').on('click', () => {
this.instance.columns(1).search($('#filterName').val());
this.instance.columns(3).search($('#filterIsBornInNagano').prop('checked'));
this.instance.draw();
})
}
と、DataTable APIの columns.search()
を使って、フォームの入力欄に対応した列でDataTableのSearchが動くようにしています。
https://datatables.net/reference/api/columns().search()
全体像はこちら
$(document).ready(function() {
const app = new SearchApp();
app.addEventHandler();
app.loadDataTable();
});
class SearchApp {
constructor() {
this.instance = null;
}
addEventHandler() {
$('#filter').on('click', () => {
this.instance.columns(1).search($('#filterName').val());
this.instance.columns(3).search($('#filterIsBornInNagano').prop('checked'));
this.instance.draw();
})
}
loadDataTable() {
this.instance = $('#demo').DataTable({
autoWidth: false,
serverSide: true,
processing: true,
responsive: true,
ajax: {
url: '/search-app/data/',
type: 'GET',
},
columnDefs: [
{targets: 0, data: 'id'},
{targets: 1, data: 'name'},
{targets: 2, data: 'color__name'},
{
targets: 3,
data: 'born_in_nagano',
render: data => {
const checked = data ? 'checked="checked"' : '';
return `<input type="checkbox" ${checked} />`;
}
},
]
});
}
}
バックエンド
もし、フォームで検索するのが文字列だけ (上記例では 品種名
だけ)であれば、django-datatables-view
は一般的な使い方で検索できるようになります。
しかし、今回のモデルは
class Apple(models.Model):
name = models.CharField('品種', max_length=50)
color = models.ForeignKey('search_app.Color', on_delete=models.CASCADE)
born_in_nagano = models.BooleanField('長野県生まれ')
と、 born_in_nagano
列がBooleanFieldとして定義されています。
BooleanFieldの何が問題かというと、 django-datatables-view
の BaseDatatableView
では、BooleanFieldが検索対象とならないことです。
というのも、 filter_queryset
メソッドのコードを読むと、カラムごとの検索において
if col['search.value']:
qs = qs.filter(**{
'{0}__{1}'.format(column, filter_method): col['search.value']})
となっています。
また、その中の変数 filter_method
の値が FILTER_ISTARTSWITH = 'istartswith'
と定義されています。
そのため、常に前方一致での検索になってしまいます。
そこで今回は、 filter_queryset
メソッドをオーバーライドし、検索対象の列だけ特別な処理を追加します。
つまり、上記で示した部分を
if col['search.value']:
if col_no == 3:
if col['search.value'] == 'true':
qs = qs.filter(**{f'{column}': True})
else:
qs = qs.filter(**{
'{0}__{1}'.format(column, filter_method): col['search.value']})
とします。
なお、今回のサンプルでは結果を分かりやすく確認するため、パフォーマンスのことを考えず、filter_methodは FILTER_ICONTAINS
にしておきます。
Viewの全体像はこんな感じです。
class AppleSearchDataTableView(BaseDatatableView):
model = Apple
columns = [
'id',
'name',
'color__name',
'born_in_nagano',
]
def render_column(self, row, col):
if col == 'color__name':
return row.color.name
if col == 'born_in_nagano':
return row.born_in_nagano
return super().render_column(row, col)
def get_filter_method(self):
""" 今回は結果をわかりやすくするために、部分一致(大文字小文字区別なし)にする
デフォルトは、FILTER_ISTARTSWITH
"""
return self.FILTER_ICONTAINS
def filter_queryset(self, qs):
""" Booleanフィールドの値はうまくSearchできない
そのため、オーバーライドして一部を書き換える
"""
columns = self._columns
if not self.pre_camel_case_notation:
search = self._querydict.get('search[value]', None)
q = Q()
filter_method = self.get_filter_method()
for col_no, col in enumerate(self.columns_data):
data_field = col['data']
try:
data_field = int(data_field)
except ValueError:
pass
if isinstance(data_field, int):
column = columns[data_field]
else:
column = data_field
column = column.replace('.', '__')
if search and col['searchable']:
q |= Q(**{'{0}__{1}'.format(column, filter_method): search})
if col['search.value']:
if col_no == 3:
if col['search.value'] == 'true':
qs = qs.filter(**{f'{column}': True})
else:
qs = qs.filter(**{
'{0}__{1}'.format(column, filter_method): col['search.value']})
qs = qs.filter(q)
return qs
以上より、フォームの入力値に基づいてDataTableの絞り込みが行えるようになりました。
参考:Search機能との連動について
JavaScript側で columns.search()
を使っているため、DataTableのSearch機能と追加したフォームを併用できます。
例えば、まずSearchで絞り込み、
続いて、フォームで更に絞り込む、ということができます。
GitHubに上げました。 search_app
アプリが今回のアプリです。
https://github.com/thinkAmi-sandbox/django-datatables-view-sample