django-datatables-viewによるServer-side processingで、モデルの複数列を結合して表示する

django-datatables-viewで、モデルの複数列を結合して表示しようと考えた時に詰まったことがあったため、メモを残します。

 
目次

 

環境

なお、サンプルの見栄えを良くするため、Bootstrap 4.5.2 も使っています。

 

やりたいこと

以下のような2つのモデルがあったとします。

class Color(models.Model):
    name = models.CharField('色', max_length=10)


class Apple(models.Model):
    name = models.CharField('品種', max_length=50)
    color = models.ForeignKey('concat_col_app.Color', on_delete=models.CASCADE)
    breeding = models.CharField('交配', max_length=100)

 
DataTableの表示では、 colorname の値を組み合わせ、以下のように タイトル 列として表示したいとします。

f:id:thinkAmi:20201011105435p:plain

 

エラーとなる方法

画面の表示に関するものなので、Django側は render_column を使い

class AppleConcatDataTableView(BaseDatatableView):
    model = Apple

    columns = [
        'id',
        'title',  # 組み合わせて表示したい列を追加
        'breeding',
    ]

    def render_column(self, row, col):
        # title列が入ってきたら、colorとnameを組み合わせて表示
        if col == 'title':
            return f'[{row.color.name}] {row.name}'

        return super().render_column(row, col)

としてみました。

また、JavaScript側も、 title 列を受け取れるようにします。

$('#demo').DataTable({
  autoWidth: false,
  serverSide: true,
  processing: true,
  responsive: true,
  ajax: {
    url: '/concat-col-app/data/',
    type: 'GET',
  },
  columnDefs: [
    {targets: 0, data: 'id'},
    {targets: 1, data: 'title'},
    {targets: 2, data: 'breeding'},
  ]
});

 
しかし、初期表示はうまくいくものの、Searchやタイトル列をクリックするとエラーが表示されてしまいます。

f:id:thinkAmi:20201011110126p:plain

 
また、Djangoのログにも以下が記録されています。

...
    raise FieldError("Cannot resolve keyword '%s' into field. "
django.core.exceptions.FieldError: Cannot resolve keyword 'title' into field. Choices are: breeding, color, color_id, id, name

 

動作する方法

Djangoのログを見ると、Djangoのmodelに title が見当たらないのが原因のようです。

READMEにある Another example of views.py customisation のうち、 get_initial_queryset メソッドの説明を読むと

def get_initial_queryset(self):
    # return queryset used as base for futher sorting/filtering
    # these are simply objects displayed in datatable
    # You should not filter data returned here by any filter values entered by user. This is because
    # we need some base queryset to count total number of records.
    return MyModel.objects.filter(something=self.kwargs['something'])

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

と書かれています。

そのため、django-datatables-viewの get_initial_queryset メソッドをオーバーライドしてQuerySetに title 属性を追加することで、filterやsortにも対応できそうです。

 
そこで、Django側のViewを修正します。

今回はレコードごとに列を追加するため、Djangoの annotate句と

を使うこと、出力したい [色] 品種名 という形式の値を持つ title 属性を追加します。

from django.db.models import Value
from django.db.models.functions import Concat
from django_datatables_view.base_datatable_view import BaseDatatableView

from concat_col_app.models import Apple


class AppleConcatDataTableView(BaseDatatableView):
    model = Apple

    columns = [
        'id',
        'title',
        'breeding',
    ]

    def get_initial_queryset(self):
        # title列を追加
        return super().get_initial_queryset().annotate(
            title=Concat(Value('['), 'color__name', Value('] '), 'name')
        )

 
なお、表示内容については、get_initial_querysetで作成した title 列の値そのもので良いため、今回は render_column のオーバーライドは不要です。

 
これにより、Searchやタイトル列クリックによるソートを行っても動作するようになりました。

以下の画像でも、タイトル列の記号にある通り、タイトル列でのソートができていることが分かります。

f:id:thinkAmi:20201011111402p:plain

 

ソースコード

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