django-datatables-viewによるServer-side processingで、色々なソートを試してみた

ライブラリ 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列でソート

があった時に、以下のような表示をしたいとします。

f:id:thinkAmi:20201010212004p:plain

 

この時の 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':
            # Booleanな項目の場合な場合、返し方によりJSでの型が異なるので注意
            # 暗黙的に返す -> JSではstring型
            # 明示的に返す -> JSではboolean型
            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の順番ではソートされません。

f:id:thinkAmi:20201010214057p:plain

 
これをベースに、いろいろ試していきます。  
 

複合ソートキーでの結果を初期表示する場合

JavaScript側で対応が必要になります。

jQuery DataTableでは、初期表示するソートキーの列は order 属性を使います。
DataTables example - Default ordering (sorting)

[ソートキー列のindex, ソート順文字列] という要素を用意し、それをArrayで指定します。ソート順文字列は ascdesc のどちらかを指定します。

$('#all-columns').DataTable({
  serverSide: true,
  processing: true,
  // ...
  columnDefs: [ /* 略 */ ]
  order: [
    [3, 'asc'],
    [5, 'desc'],
    // ソートキーが重複した場合にもソートが常に一定となるよう、idをソートキーに加えておく
    [0, 'asc'],
  ],
});

 
これにより、

  1. 交配
  2. 長野県生まれ
  3. ID

の複合キーで、指定した順(昇順/降順)にて、ソートされるようになりました。

f:id:thinkAmi:20201010212933p:plain

 

ソート不可列がある時のソートについて

JavaScript側、Django側の双方で対応が必要になります。

JavaScript側の修正

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側の修正

django-datatables-viewのREADMEにあるサンプルを読むと、

# https://bitbucket.org/pigletto/django-datatables-view/src/master/

# define column names that will be used in sorting
# order is important and should be same as order of columns
# displayed by datatables. For non sortable columns use empty
# value like ''
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',
        '',  # born_in_nagano部分はソート不可にする
    ]

    def render_column(self, row, col):
        ...

 

結果

f:id:thinkAmi:20201010214131p:plain

 

権限により、列の表示可否が変わる時のソートについて

例えば、「ログインしている場合のみ、 交配 列を表示する」という仕様が追加になったとします。

この場合、

  • ログイン状態により、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>

 

JavaScript側の変更

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(),

 

Django側の修正

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

 

ソート機能を変更

列表示が動的になる場合、JavaScriptDjango側で表示する列数を合わせる必要があります。具体的には以下の4項目です。

 
ここまでで対応していないのはJavaScript側の order ですので、対応を行います。

まずは columnDefs同様、列を動的に変更する関数を用意します。

function getOrder() {
  const results = [];
  let bornIndex = 4;  // 非表示時の "born_in_nagano" 位置

  if ($('#username').length) {
    results.push([3, 'asc']);
    bornIndex++;  // 列が増えるため、"born_in_nagano" 位置を修正
  }
  results.push([bornIndex, 'desc']);

  // ソートキーが重複した場合にもソートが常に一定となるよう、idをソートキーに加えておく
  results.push([0, 'asc']);

  return results;
}

 

続いて、 order属性にこの関数の戻り値を割り当てます。

$('#perms-columns').DataTable({
  // ...
  columnDefs: getColumnDefs(),

  // 変更
  order: getOrder(),
});

 

結果

以上の対応により、動的に列数が変わったとしても、初期ソート状態やソート機能が利用できるようになります。

 

未ログイン時

交配 列が表示されず、 長野県生まれ 列の降順だけでのソートとなります。

f:id:thinkAmi:20201010215509p:plain

 

ログイン済時

交配 列の昇順・ 長野県生まれ 列の降順でソートされています。

f:id:thinkAmi:20201010215716p:plain

 
 

ソースコード

GitHubに上げました。 sort_app アプリが今回のソースコードです。
https://github.com/thinkAmi-sandbox/django-datatables-view-sample