ライブラリ django-datatables-view
を使ってjQuery DataTableのServer Side Processing を行った際、ソートを実装することがありました。
ただ、ソートを実装しようとしたところ詰まったことがあったため、メモを残します。
目次
環境
なお、サンプルの見栄えを良くするため、Bootstrap 4.5.2 も使っています。
ソートを意識しない時の実装
このようなモデルがあったとします。
class Color(models.Model):
name = models.CharField('色', max_length=10)
class Apple(models.Model):
name = models.CharField('品種', max_length=50)
color = models.ForeignKey('sort_app.Color', on_delete=models.CASCADE)
breeding = models.CharField('交配', max_length=100)
season = models.IntegerField('旬')
born_in_nagano = models.BooleanField('長野県生まれ')
このAppleモデルをDataTableに表示します。
その時の要件として、
season
(旬) が実行月と一致した場合は、チェックボックスにチェックを入れる
- 最初は、id列でソート
があった時に、以下のような表示をしたいとします。
この時の django-datatables-view
を使ったViewの実装はこんな感じです。
class AppleAllFieldDataTableView(BaseDatatableView):
model = Apple
columns = [
'id',
'name',
'color__name',
'breeding',
'season',
'born_in_nagano',
]
def render_column(self, row, col):
if col == 'color__name':
return row.color.name
if col == 'season':
if row.season == timezone.now().month:
return True
return False
if col == 'born_in_nagano':
return row.born_in_nagano
return super().render_column(row, col)
注意点として、Djangoのモデルで BooleanField
は、そのままJavaScript側に渡すと string
型になります。
それでは扱いづらいので、 render_column()
メソッドをオーバーライドし、明示的にPythonのTrue/Falseを返すようにします。これにより、JavaScriptには boolean
型の値が渡されます。
続いて、JavaScript側の実装です。
DataTableの中でチェックボックスを表示できるよう、render
属性を使って描画するHTMLを指定します。
$('#all-columns').DataTable({
autoWidth: false,
serverSide: true,
processing: true,
responsive: true,
pageLength: 25,
ajax: {
'url': '/sort-app/all-columns/data/',
'type': 'GET',
},
columnDefs: [
{targets: 0, data: 'id'},
{targets: 1, data: 'name'},
{targets: 2, data: 'color__name'},
{targets: 3, data: 'breeding'},
{
targets: 4,
data: 'season',
render: data => {
const checked = data ? 'checked="checked"' : '';
return `<input type="checkbox" ${checked} />`;
}
},
{
targets: 5,
data: 'born_in_nagano',
render: data => {
const checked = data ? 'checked="checked"' : '';
return `<input type="checkbox" ${checked} />`;
}
},
],
});
なお、JavaScript側の render()
やDjango側の render_column()
は、あくまで見た目を整えるだけであり、実際のデータとは異なります。
そのため、旬
列でソートしたとしても、DB上のデータは xx月であることから、チェックボックスがONの順番ではソートされません。
これをベースに、いろいろ試していきます。
複合ソートキーでの結果を初期表示する場合
JavaScript側で対応が必要になります。
jQuery DataTableでは、初期表示するソートキーの列は order
属性を使います。
DataTables example - Default ordering (sorting)
[ソートキー列のindex, ソート順文字列]
という要素を用意し、それをArrayで指定します。ソート順文字列は asc
・ desc
のどちらかを指定します。
$('#all-columns').DataTable({
serverSide: true,
processing: true,
columnDefs: [ ]
order: [
[3, 'asc'],
[5, 'desc'],
[0, 'asc'],
],
});
これにより、
- 交配
- 長野県生まれ
- ID
の複合キーで、指定した順(昇順/降順)にて、ソートされるようになりました。
ソート不可列がある時のソートについて
JavaScript側、Django側の双方で対応が必要になります。
DataTableでソート不可列を指定するには、columnDefsの中で該当の列に orderable: false
を指定します。
また、ソート不可列を order
属性に含めないよう、注意します。
$('#some-columns').DataTable({
columnDefs: [
{targets: 0, data: 'id'},
{
targets: 5,
data: 'born_in_nagano',
searchable: false,
orderable: false,
render: data => {
const checked = data ? 'checked="checked"' : '';
return `<input type="checkbox" ${checked} />`;
}
},
],
order: [
[3, 'asc'],
[0, 'asc'],
]
});
django-datatables-viewのREADMEにあるサンプルを読むと、
order_columns = ['number', 'user', 'state', '', '']
との記載があります。
そのため、 order_columns
を追加で定義し、ソート不可項目のIndexの要素には、空文字を指定します。
class AppleSomeFieldDataTableView(BaseDatatableView):
model = Apple
columns = [
'id',
'name',
'color__name',
'breeding',
'season',
'born_in_nagano',
]
order_columns = [
'id',
'name',
'color__name',
'breeding',
'season',
'',
]
def render_column(self, row, col):
...
結果
権限により、列の表示可否が変わる時のソートについて
例えば、「ログインしている場合のみ、 交配
列を表示する」という仕様が追加になったとします。
この場合、
- ログイン状態により、DataTableの列表示可否を変更
- ソート機能を変更
の2つの対応が必要になります。
そのため、順番に作業を行います。
ログイン状態により、DataTableの列表示可否を変更
テンプレートの変更
DataTableのヘッダの表示を動的に変更します。
<table id="perms-columns" class="table table-striped table-bordered dataTable" style="width:100%">
<thead>
<tr>
<th>ID</th>
<th>品種</th>
<th>色</th>
{# 交配は、ログインしていれば見える #}
{% if request.user.is_authenticated %}
<th>交配</th>
{% endif %}
<th>旬</th>
<th>長野県生まれ</th>
</tr>
</thead>
</table>
columnDefsを動的に制御するため、以下の関数を追加します。
function getColumnDefs() {
const results = [];
let colIndex = 0;
results.push({targets: colIndex++, data: 'id'});
results.push({targets: colIndex++, data: 'name'});
results.push({targets: colIndex++, data: 'color__name'});
if ($('#username').length) {
results.push({targets: colIndex++, data: 'breeding'});
}
results.push({
targets: colIndex++, data: 'season',
render: data => {
const checked = data ? 'checked="checked"' : '';
return `<input type="checkbox" ${checked} />`;
}
});
results.push({
targets: colIndex++, data: 'born_in_nagano',
render: data =>{
const checked = data ? 'checked="checked"' : '';
return `<input type="checkbox" ${checked} />`;
}
});
return results;
}
そして、この関数の戻り値を、columnDefsに指定します。
なお、 columnDefs: () => { /* .. */ }
のような定義では動作しませんでした。
columnDefs: getColumnDefs(),
get_columns
メソッドをオーバーライドし、ログインしている場合のみ 交配
列を追加します。
class ApplePermsFieldDataTableView(BaseDatatableView):
model = Apple
def get_columns(self):
results = [
'id',
'name',
'color__name',
]
if self.request.user.is_authenticated:
results.append('breeding')
results.extend([
'season',
'born_in_nagano',
])
return results
ソート機能を変更
列表示が動的になる場合、JavaScriptとDjango側で表示する列数を合わせる必要があります。具体的には以下の4項目です。
ここまでで対応していないのはJavaScript側の order
ですので、対応を行います。
まずは columnDefs同様、列を動的に変更する関数を用意します。
function getOrder() {
const results = [];
let bornIndex = 4;
if ($('#username').length) {
results.push([3, 'asc']);
bornIndex++;
}
results.push([bornIndex, 'desc']);
results.push([0, 'asc']);
return results;
}
続いて、 order属性にこの関数の戻り値を割り当てます。
$('#perms-columns').DataTable({
columnDefs: getColumnDefs(),
order: getOrder(),
});
結果
以上の対応により、動的に列数が変わったとしても、初期ソート状態やソート機能が利用できるようになります。
未ログイン時
交配
列が表示されず、 長野県生まれ
列の降順だけでのソートとなります。
ログイン済時
交配
列の昇順・ 長野県生まれ
列の降順でソートされています。
GitHubに上げました。 sort_app
アプリが今回のソースコードです。
https://github.com/thinkAmi-sandbox/django-datatables-view-sample