Djangoで、SILENCED_SYSTEM_CHECKSを定義してSystem check frameworkのメッセージ出力を抑制する

これは JSL(日本システム技研) Advent Calendar 2020 - Qiita 12/15分の記事です。

 
DjangoにはSystem check frameworkがあり、Djangoプロジェクトの正しさをチェックしてくれます。
System check framework | Django ドキュメント | Django

そんな中、特定のチェックで大量に引っかかってしまうことがありました。

そこで、特定のチェックのメッセージ出力を抑える方法を探した時のメモを残します。

 
目次

 

環境

 

事例

例えば、Djangoを1系からバージョンアップする中で、urls.pyに

urlpatterns = [
    path('warn$', TemplateView.as_view(template_name='silence_app/index.html')),
]

と、 $ を残してしまったとします。

 
この場合、開発用のサーバを起動すると、

Performing system checks...

System check identified some issues:

WARNINGS:
?: (2_0.W001) Your URL pattern 'warn$' has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().

System check identified 1 issue (0 silenced).

というメッセージが表示されます。

実際には他のメッセージも表示されているため、このメッセージだけを抑制したいとします。

 

対応

settings.pyに SILENCED_SYSTEM_CHECKS を定義します。
https://docs.djangoproject.com/en/3.1/ref/settings/#silenced-system-checks

今回は 2_0.W001 を抑制したいので、settings.pyに

SILENCED_SYSTEM_CHECKS = ['2_0.W001']

と定義します。

 
その後、開発サーバを起動すると

Performing system checks...

System check identified no issues (1 silenced).

へと表示が変わり、2_0.W001を抑制できました。

 
なお、この警告ですが、実際にアクセスしてみると

warn宛

$ curl localhost:8000/silence/warn -v
...
< HTTP/1.1 404 Not Found

warn$宛

$ curl localhost:8000/silence/warn$ -v
...
< HTTP/1.1 200 OK

となります。

 

ソースコード

Githubに上げました。 silence_app ディレクトリが今回のDjangoアプリです。
https://github.com/thinkAmi-sandbox/django_31-sample

pandoc & wkhtmltopdf のDockerイメージを作成し、複数マークダウンファイルを1つのpdfにする

この記事は、 JSL(日本システム技研) Advent Calendar 2020 - Qiita 12/8の記事です。

以前、markdownからpdfを作成する機会がありました。
GitLab CI + docker-reviewを使って、Markdownをtextlintしてからpdf化するCI環境を作ってみた - メモ的な思考的な

他の方法がないかを見たところ、 pandoc & wkhtmltopdfでも作成できそうでした。
pandoc + markdownでいい感じの執筆環境を作る - Qiita

そこで、pandoc & wkhtmltopdf のDockerイメージを作成し、複数マークダウンファイルを1つのpdfにしてみました。  
 

目次

 

環境

  • Docker
    • 以下のライブラリを入れる
      • pandoc 2.11.2
      • wkhtmltopdf 0.12.5
      • フォントは Google Noto

 

最終的なディレクトリ構成はこちら。

$ tree
.
├── Dockerfile
├── manuscript
│   ├── りんご.md
│   └── さつまいも.md
├── output
│   └── (merge.pdf)
└── settings
    ├── defaults.yaml
    └── style.css

 
マークダウンファイルは2つ用意しました。

なお、マークダウンファイル内で \ の後にトリプルバッククォートしている部分ですが、はてなブログに貼るために書いているため、本来 \ は不要です。

りんご.md

# りんごの種類

- シナノゴールド
- フジ

(以下の \ は不要)
\```python
print('りんごです!')
\```


<div class="hidden">
# コメント

シナノゴールドはイタリアをはじめとした海外に進出してる

</div>

<div class="page-break"></div>

 

さつまいも.md

# さつまいもの種類

- 紅はるか
- 安納芋

(以下の \ は不要)
\```python
print('さつまいもです!')
\```

<div class="hidden">
# コメント

紅優甘は、紅はるかの商標登録名

</div>


<div class="page-break"></div>

実装

Dockerfile

同じようなDockerfileがないかを探したところ、以下のリポジトリがありました。
https://github.com/slurdge/docker-pandoc-wkhtmltopdf

 
ただ、docker buildしてみると

E: Package 'libssl1.0-dev' has no installation candidate

というエラーでビルドできませんでした。

 

他のDockerfileがないかを探したところ、 wkhtmltopdf を使っているDockerfileがありました。
Docker コンテナ上で wkhtmltopdf を動かす - Qiita

そこで、これらを組み合わせて作ってみることにしました。

 

まずはpandocのリポジトリを見たところ、 pandoc-2.11.2-1-amd64.deb がありました。
https://github.com/jgm/pandoc/releases/

次にwkhtmltopdfのリポジトリを見たところ、 wkhtmltox_0.12.5-1.buster_amd64.deb 等のdebファイルがありました。
https://github.com/wkhtmltopdf/wkhtmltopdf/releases

そこで、今回はDebianベースで作ることにしました。

 
ただ、上記だけでは日本語を含んだマークダウンが文字化けしてしまいました。

そこで「Docker コンテナ上で wkhtmltopdf を動かす」の記事に合わせてフォントを入れることにしました。

DebianでNotoフォントを入れる方法を探したところ、 fonts-noto-cjkfonts-noto-cjk-extra を使えば良さそうでした。
Linuxだって、綺麗にフォントが表示できるんだからねッ!

そのため、pandocとwkhtmltopdfはGithubから、それ以外はdebファイルからインストールすることにしました。

 
ただ、debファイルからインストールする際にいくつか依存関係が発生することから、 gdebi もインストールしておきます。

 
他に、 --no-install-recommends でインストールすると

ERROR: The certificate of 'github.com' is not trusted.
ERROR: The certificate of 'github.com' doesn't have a known issuer.

が発生することから、 ca-certificates も追加しています。
Ubuntu on Docker で SSL/TLS 通信するとエラーになる問題の対処 - Qiita

 

最終的なDockerfileはこちら

FROM debian:buster-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
  xorg \  
  libssl-dev \
  libxrender-dev \
  wget \
  gdebi \
  fonts-noto-cjk \
  fonts-noto-cjk-extra \
  ca-certificates \
  && rm -rf /var/lib/apt/lists/* \
  && apt-get autoremove \
  && apt-get clean

RUN wget https://github.com/jgm/pandoc/releases/download/2.11.2/pandoc-2.11.2-1-amd64.deb -O pandoc.deb \
    && dpkg -i ./pandoc.deb \
    && rm pandoc.deb

RUN wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.buster_amd64.deb -O wkhtmltox.deb \
    && dpkg -i ./wkhtmltox.deb \
    && rm wkhtmltox.deb

RUN mkdir /var/tmp/output

 

docker build

Dockerfileができたのでビルドします。

$ docker build ./ -t pandoc_wkhtmltopdf:1.0
...
Successfully built ae80e0763df1
Successfully tagged mydoc:1.0

 

ビルド後のサイズはこんな感じです。

$ docker image list pandoc*

REPOSITORY            TAG    IMAGE ID        CREATED          SIZE
pandoc_wkhtmltopdf    1.0    ae80e0763df1    2 minutes ago    998MB

 

docker run

Dockerイメージができたので、docker run します。

オプションとして以下を指定します。

オプション 内容
--mount type=bind,src=...,dst=... ホストとDockerでファイルを共有するため。生成したpdfのコピーを不要にする
-w /var/tmp/output 作業ディレクトリをホストと共有したディレクトリにすることで、cdとか不要に

 
docker run後、Dockerに入って作業ディレクトリにいればOKです。

$ docker run -it --mount type=bind,src="$(pwd)"/,dst=/var/tmp/output -w /var/tmp/output --name mypandoc pandoc_wkhtmltopdf:1.0
...
root@f190aac6d170:/var/tmp/output# 

 

pandocとwkhtmltopdfによる変換
Default filesファイルを作成

pandoc実行時にオプションを渡してpdfファイルへと変換します。

ただ、pandocにはオプションが多くあるため、実行時に指定漏れが発生しそうでした。

そこで、Default filesを使って、コマンド時のオプションは必要最低限とすることにしました。
https://pandoc.org/MANUAL.html#default-files

なお、今回複数マークダウンファイルを1つのpdfにまとめますが、 input-files ではワイルドカード指定ができなかったため、Default filesには記載しませんでした。
Wildcard for multiple input files in the defaults file variable "input-files" - Google グループ

from: markdown
to: html5

# 入力ファイルはコマンドラインから指定
# manuscript/*.md が指定できないため

# 出力ファイル (単一で指定)
output-file: output/merge.pdf

# コードブロックの背景色
highlight-style: tango

# 独自CSS
css:
- settings/style.css

 

独自CSSファイルの用意

今回は改ページと非表示のclassを用意しました。

.page-break {
    page-break-before: always;
}

.hidden {
    display: none;
}

 

pandocコマンドの実行

pandocコマンドで変換を行います。

# pandoc ./manuscript/*.md -d settings/defaults.yaml

[WARNING] This document format requires a nonempty <title> element.
  Defaulting to 'さつまいも' as the title.
  To specify a title, use 'title' in metadata or --metadata title="...".
Loading pages (1/6)
Counting pages (2/6)                                               
Resolving links (4/6)                                                       
Loading headers and footers (5/6)                                           
Printing pages (6/6)
Done 

 

pdfの確認

改ページされていること、不要な部分が表示されていないことが確認できました。

f:id:thinkAmi:20201207231228p:plain:w400

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/pandoc_wkhtmltopdf_docker

 

その他参考

aptまわり

Django3系のORMでSQLのEXISTS句を使う

この記事は、 JSL(日本システム技研) Advent Calendar 2020 - Qiita 12/3の記事です。

以前、SQLDjangoのQuerySet APIでどう実装するのかを書きました。

 
上記ではサブクエリについてはふれなかったのですが、当時EXISTS句を使うのは大変だった記憶があります。

そんな中、EXISTS句を使う機会があったため、Django3系ではどうなっているのかメモに残します。

 
目次

 

環境

 

やりたいこと

ColorとAppleモデルがあり、Color : Apple = 1 : 多 の関係とします。

class Color(models.Model):
    name = models.CharField('名前', max_length=20)


class Apple(models.Model):
    name = models.CharField('名前', max_length=20)
    color = models.ForeignKey('Color', on_delete=models.PROTECT)

 
この時、Appleが存在するColorを全件抽出したいとします。

SQL的にはこんな感じ。

SELECT
    "sql_exists_color"."id",
    "sql_exists_color"."name"
FROM
    "sql_exists_color"
WHERE
    EXISTS(
        SELECT
            U0."id",
            U0."name",
            U0."color_id"
        FROM
            "sql_exists_apple" U0
        WHERE
            U0."color_id" = "sql_exists_color"."id"
    ) 

 

Django3系でのやり方

改めて調べたところ、Django3系では以前より直感的に書けるようになっていました。

 
上記のように、「Appleが存在するColorを全件抽出したい」場合は

Color.objects.filter(
      Exists(Apple.objects.filter(color=OuterRef('pk')))
  )

SQLと似たような形で書けるようになっていました。

Exists() の中にサブクエリを書き、Exists中のfilterにて、 OuterRef でサブクエリの外のフィールドを参照・比較します。

 

テストコードでの確認

テストコードで確認してみます。

AppleFactoryではColorが緑のものは生成しないようにし、self.assertListEqual(list(colors), [yellow, red]) にて期待通りの結果になるかを確認します。

from django.conf import settings
from django.db import connection
from django.test import TestCase
from django.db.models import Exists, OuterRef

from sql_exists.factories import ColorFactory, AppleFactory
from sql_exists.models import Color, Apple


class TestM2MExists(TestCase):
    def test_exists(self):
        ColorFactory(name='緑')
        yellow = ColorFactory(name='黄')
        red = ColorFactory(name='赤')
        AppleFactory(name='シナノゴールド', color=yellow)
        AppleFactory(name='シナノスイート', color=red)
        AppleFactory(name='フジ', color=red)
        AppleFactory(name='シナノドルチェ', color=red)

        colors = Color.objects.filter(
            Exists(Apple.objects.filter(color=OuterRef('pk')))
        )

        # テストコードでは常にDEBUG=Falseになり、connection.queriesが取得できないことから強制書き換え
        # なお、今回の場合SQLが発行されるのは、list()のタイミング
        # ちなみに、Django1.11からはmanage.pyのオブションに `--debug-mode` もある
        # https://docs.djangoproject.com/en/3.1/ref/django-admin/#cmdoption-test-debug-mode
        settings.DEBUG = True

        self.assertListEqual(list(colors), [yellow, red])

        for query in connection.queries:
            print(query)

        settings.DEBUG = False

 
テストを実行すると、以下の通りパスしました。

$ python manage.py test sql_exists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
{'sql': 'SELECT "sql_exists_color"."id", "sql_exists_color"."name" FROM "sql_exists_color" WHERE EXISTS(SELECT U0."id", U0."name", U0."color_id" FROM "sql_exists_apple" U0 WHERE U0."color_id" = "sql_exists_color"."id")', 'time': '0.000'}
.
----------------------------------------------------------------------
Ran 1 test in 0.009s

OK
Destroying test database for alias 'default'...

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/django_31-sample

PHPのDOMDocumentを使って、日本語を含むHTMLの一部分を文字列として生成する

うまいタイトルが思い浮かばなかったのですが、

コメント
<hr>
<div>ハロー
  <p>ワールド</p>
</div>

のようなものを、PHPのDOMDocumentを使って実現する時に悩んだので、メモに残します。

もし、他に良い方法があれば教えて頂けるとありがたいです。

 

目次

 

環境

 

悩んだ点と対応

タグのないところにテキストやタグを入れる

タグに囲まれている部分にテキストを入れる場合

$dom = new DOMDocument();
$dom->appendChild($dom->createElement('div', 'ハロー'));

とすれば

<div>ハロー</div>

というHTMLが作れます。

ただ、今回は

コメント
<hr>

のように、hrタグの前にテキストを入れる必要がありました。

 
この場合、 createTextNode を使えば良さそうでした。

 
そのため、

$dom = new DOMDocument();

$comment = $dom->createTextNode('コメント');
$dom->appendChild($comment);

$dom->appendChild($dom->createElement('hr'));

とすることで、

コメント<hr>

となりました。

 

日本語を含むHTMLを文字列にする

DOMDocumentをHTMLの文字列にするには、 saveHTML() を使えば良さそうでした。
PHP: DOMDocument::saveHTML - Manual

 
しかし、

$dom = new DOMDocument();

$comment = $dom->createTextNode('コメント');
$dom->appendChild($comment);

$dom->appendChild($dom->createElement('hr'));

$div = $dom->appendChild($dom->createElement('div', 'ハロー'));

$div->appendChild($dom->createElement('p', 'ワールド'));
$dom->saveHTML();

echo $html;

のように、日本語を含むDOMDocumentに対して使ったところ

&#12467;&#12513;&#12531;&#12488;
<hr>
<div>&#12495;&#12525;&#12540;
  <p>&#12527;&#12540;&#12523;&#12489;</p>
</div>

と、日本語は数値文字参照での表現となりました。

 
この場合、 mb_convert_encoding() を使えば良さそうでした。

 

そのため、 mb_convert_encoding() を使って文字コードHTML-ENTITIES から UTF-8 へと変更するよう、

// ここまでは同じ
$div->appendChild($dom->createElement('p', 'ワールド'));

// 変更
$html = mb_convert_encoding($dom->saveHTML(), 'utf-8', 'HTML-ENTITIES');

echo $html;

としたところ、

コメント
<hr>
<div>ハロー
  <p>ワールド</p>
</div>

と、日本語のHTMLの文字列となりました。

 

ソースコード

Githubに上げました。 dom_document/create_html.php が今回のファイルです。
https://github.com/thinkAmi-sandbox/php-sample

bootstrap-selectの1.13.8以前では、val()でoptionを選択した時のpreviousValueが取得できなかった

Bootstrapを使っている環境にて、select要素の見栄えを良くしたい場合、 bootstrap-select を使うことがあります。
snapappointments/bootstrap-select: The jQuery plugin that brings select elements into the 21st century with intuitive multiselection, searching, and much more.

 
bootstrap-selectにはいくつかイベントがあり、例えば changed.bs.select を使うと選択した値が変更された時にイベントが発火します。
https://developer.snapappointments.com/bootstrap-select/options/#events

ドキュメントにもある通り previousValue には変更前の値が入ってきます。

ただ、1.13.8以前の古いバージョンを使っている場合、previousValueが取得できなかったため、メモを残します。

 
目次

 

環境

  • Bootstrap 4.3.1
  • bootstrap-select 1.13.8/1.13.9
  • jQuery 3.5.1
  • popper 1.14.7

 

原因

以下にある通り、バグがあったようです。

 

対応

bootstrap-selectを1.13.9以降にアップデートします。

 

再現

以下のようなHTMLがあるとします。

<div style="padding: 10px">
  <div>
    <h1>1.13.8</h1>

    <select id="ringo"  class="selectpicker">
      <option value="1">フジ</option>
      <option value="2">紅玉</option>
      <option value="3">シナノゴールド</option>
    </select>
  </div>

  <div style="padding-top: 10px">
    <button id="auto" class="btn btn-primary">自動選択</button>
  </div>
</div>

 
表示はこんな感じ。

f:id:thinkAmi:20201123135256p:plain:w400

 
 
ここで、

$(document).ready(function() {
  $('#ringo').on('changed.bs.select', (event, clickedIndex, isSelected, previousValue) => {
    console.log(previousValue);
  });

  $('#auto').on('click', () => {
    $('#ringo').selectpicker('val', 3);
  })
});

と、自動選択ボタンを押すと、 シナノゴールド が表示されるようにします。

また、 changed.bs.select の発火を確認するため、イベントが発生したら previousValue の値をコンソールに出力します。

 

bootstrap-selectのバージョンが1.13.8の場合

undefined になります。

f:id:thinkAmi:20201123135309p:plain:w400

 

bootstrap-selectのバージョンが1.13.9の場合

正しい値 1 (フジ) が取得できます。

f:id:thinkAmi:20201123135226p:plain:w400  
 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/bootstrap_select-sample

django-datatables-viewによるServer-side processingで、DataTable向けのクエリパラメータを追加する

django-datatables-viewによるServer-side processingで、DataTable向けのクエリパラメータを追加しようと考えた時に詰まったことがあったため、メモを残します。

 

目次

 

環境

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

 

やりたいこと

例えば以下のようなDataTableがあるとします。

f:id:thinkAmi:20201020214702p:plain

 
この状態で、もしHTMLのhidden input値があれば、その条件に従ってDataTableを絞り込んで表示したいとします。

例えば、

<input type="hidden" id="limitation" value="yellow">

と指定されている時は

f:id:thinkAmi:20201020215039p:plain

と絞り込まれた状態で表示します。

 

対応

フロントエンド

Server-side Processingで、バックエンドに渡すクエリパラメータを追加したい場合、 ajax オプションに data を追加します。
ajax - DataTables option

$('#demo').DataTable({
  autoWidth: false,
  serverSide: true,
  processing: true,
  responsive: true,
  ajax: {
    url: '/args-app/data/',
    type: 'GET',
    // 追加で渡すクエリパラメータを指定
    data: getLimitation(),
  },
  columnDefs: [
    {targets: 0, data: 'id'},
    {targets: 1, data: 'name'},
    {targets: 2, data: 'color__name'},
  ]
});


function getLimitation() {
  const limitation = $('#limitation');
  return limitation.length ? {limitation: limitation.val()} : {};
}

 

バックエンド

フロントからのクエリパラメータは、 _querydict の中に含まれています。

今回はクエリパラメータを元に絞り込みを行いたいため、 filter_queryset メソッドをオーバーライドします。

class AppleArgsDataTableView(BaseDatatableView):
    model = Apple

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

    def render_column(self, row, column):
        if column == 'color__name':
            return row.color.name

        return super().render_column(row, column)

    def filter_queryset(self, qs):
        qs = super().filter_queryset(qs)

        # 追加されたクエリパラメータによる絞り込み
        limitation = self._querydict.get('limitation')
        if limitation == 'yellow':
            qs = qs.filter(color__name='黄')

        return qs

 

以上より、HTMLのhidden input値を元に、DataTableを絞り込んで表示できるようになりました。

 

ソースコード

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

django-datatables-viewによるServer-side processingで、フォームの入力値に基づいてDataTableの絞り込みを行う

django-datatables-viewで、フォームの入力値に基づいてDataTableの絞り込みを行おうと考えた時に詰まったことがあったため、メモを残します。

 

目次

 

環境

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

また、今回の実装ではDataTableのインスタンスをどこかに保持する必要がある & わかりやすくするために、JavaScriptclass を使っています*1

 

DataTableのSearchについて

DataTableには標準でSearch機能があります。

Searchはリアルタイムで行われます。例えば、

f:id:thinkAmi:20201016001229p:plain

という元データに対し、 Search欄に入力すると

f:id:thinkAmi:20201016001242p:plain

と絞り込みが行われます。

 

やりたいこと

状況によってはSearch欄以外にフォームを設けて、そこで絞り込みを行いたいこともあるかもしれません。

例えば、フォームに入力してFilterボタンを押すと、元データを

f:id:thinkAmi:20201016001450p:plain

と絞り込みたいとします。

 

実装

フロントエンド

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 APIcolumns.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-viewBaseDatatableView では、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で絞り込み、

f:id:thinkAmi:20201016003720p:plain

 

続いて、フォームで更に絞り込む、ということができます。

f:id:thinkAmi:20201016003743p:plain

 

ソースコード

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

*1:インスタンスグローバル変数に保持すれば、classを使わなくて済みそうですが...