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', () => { // columnsの引数には、columnDefsで定義した列indexをセット 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() { // DataTableインスタンスを保持しておきたいため、クラスとして定義 this.instance = null; } addEventHandler() { $('#filter').on('click', () => { // columnsの引数には、columnDefsで定義した列indexをセット 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
メソッドのコードを読むと、カラムごとの検索において
# column specific filter if col['search.value']: qs = qs.filter(**{ '{0}__{1}'.format(column, filter_method): col['search.value']})
となっています。
また、その中の変数 filter_method
の値が FILTER_ISTARTSWITH = 'istartswith'
と定義されています。
そのため、常に前方一致での検索になってしまいます。
そこで今回は、 filter_queryset
メソッドをオーバーライドし、検索対象の列だけ特別な処理を追加します。
つまり、上記で示した部分を
# column specific filter if col['search.value']: # BooleanField対応のため、if col_no を追加し、指定した列だけ処理を変える 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': # Booleanな項目の場合な場合、返し方によりJSでの型が異なるので注意 # 暗黙的に返す -> JSではstring型 # 明示的に返す -> JSではboolean型 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: # get global search value search = self._querydict.get('search[value]', None) q = Q() filter_method = self.get_filter_method() for col_no, col in enumerate(self.columns_data): # col['data'] - https://datatables.net/reference/option/columns.data data_field = col['data'] try: data_field = int(data_field) except ValueError: pass if isinstance(data_field, int): column = columns[data_field] # by index so we need columns definition in self._columns else: column = data_field column = column.replace('.', '__') # apply global search to all searchable columns if search and col['searchable']: q |= Q(**{'{0}__{1}'.format(column, filter_method): search}) # column specific filter if col['search.value']: # BooleanField対応のため、if col_no を追加し、指定した列だけ処理を変える 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