前書き
DtataTablesで表示するデータをサーバーサイドで用意し、またPDFを出力する機能をDjangoで作成することになりました。その際、 thinkAmiさんから助言をいただきました。本記事は、Datatables/django-datatables-view/WeasyPrint/Djangoを使用し、PDF出力を実装する方法 をまとめたものです。
また、使用しているコードの全体は以下のGitHubレポジトリにアップしました。
環境,ツールのバージョン
MacOS Catalina 10.15.6
バージョン | |
---|---|
Python3 | 3.8.2 |
Django | 3.0.7 |
WeasyPrint | 15 |
django-datatables-view | 1.19.1 |
使用するツール
- DataTables
- テーブル要素に検索やソート、ページネーション機能を持たせるjQueryライブラリ
- django-datatables-view
- 上記のDataTablesをサーバーサイド処理する際に、Djangoでバックエンドを簡単に作成するためのViewを使うことができる。
- WeasyPrint
- HTMLテンプレートを用いてPDFを作成するためのPythonツール。
仕様と実装方針
以下の様な、シンプルな名簿を想定します。
表
参加者 | 出身 | 年齢 |
---|---|---|
太郎 | 東京 | 10 |
二郎 | 大阪 | 15 |
三郎 | 北海道 | 22 |
こちらをテーブルとして表示し、ソートや検索機能を実装します。 ただし、名簿は数万件規模となるためページネーション可能とし、処理速度を確保するためデータは表示するもののみサーバーサイドで提供することにします。
またPDFとして出力する機能を実装します。 PDFはデータテーブルで表示している内容を、ページネーション含め全件出力します。ただし、テーブルをソートしたり検索して絞り込んだ結果を反映可能にします。
プロジェクトの作成とインストール
仮想環境をvenvで作成し、必要なライブラリをpipでインストールします。
# 仮想環境構築 $ mkdir project $ cd project $ python3 -m venv env $ source env/bin/activate # ライブラリのインストール $ pip install Django $ pip install django-datatables-view
WeasyPrintは依存ライブラリがあるので、homebrewでインストールしました。 詳しくは公式ドキュメントInstalling — WeasyPrint 51 documentationをご覧ください。 Amazon Linuxでの環境構築は、手前味噌ですがWeasyPrintでPDF出力するまでの環境構築(Django/Amazon Linux)をご覧ください。
$ pip install WeasyPrint $ brew install cairo pango gdk-pixbuf libffi
Djnagoのプロジェクトを作成します。cmsというappを作成します。 詳しくはGitHubにあげたこちらのリンクを参考にしてください。
DataTablesでの表示(Server-side processing)
DataTablesは通常、HTMLに表示済みのテーブルに対してページネーションや検索の機能を付与します。つまり実際には、ページネーションで表示されていない部分はdisplayをnoneとしただけで、レンダリングされたままです。
しかしデータ数が非常に多い場合には、大量のデータをHTML上に表示しているため動作が遅くなり、最悪の場合はサーバー側でタイムアウトしてしまいます。 そのため、1ページ分のデータのみをサーバー側から返す様にします(Server-side Processing)。
- DataTablesの公式ドキュメントにもPHPでバックエンドを作成する例が示されています。
- DataTables example - Server-side processing
DataTablesサーバーサイド処理の実装
cms/templates/cms/index.html
- 省略。こちらのGitHubのリンクを参照してください。
- こちらの表示は、
cms/urls.py
にてTemplateViewを用いて行っています。
- こちらの表示は、
cms/static/js/index.js
- DataTableを表示するためのjsコードです。該当部分のみ示します。
- "serverSide"をtrueとし、"ajax"でDjangoによって作っているURLを指定します。URLはGitHubのリンクを参照してください。
$('#table_id').DataTable({ "processing": true, "serverSide": true, "ajax": { 'url': '/datatable-view/', 'type': 'GET' } });
cms/views.py
- DataTablesにデータを返すバックエンド側です。
- DataTablesはソートなどの情報を複雑なクエリストリングを渡してきますが、django-datatables-viewを使えば適切に解析してくれます。
from django_datatables_view.base_datatable_view import BaseDatatableView from cms.models import Person class IndexDatatableView(BaseDatatableView): model = Person # この順番で表示される。 columns = ['name', 'prefecture__name', 'age'] def render_column(self, row, col): if col == 'prefecture__name': return row.prefecture.name return super().render_column(row, col)
- 補足: render_columnのオーバーライド
- django-datatables-view · PyPI
- 県の名称(prefectureテーブルのname属性)は外部キーの値です。そのため、render_columnメソッドをオーバーライドして、表示したい値を返す必要があります。
- モデルの構造については、仕様と実装方針をご確認ください。
PDF出力
余談: DataTablesのPDF出力機能
DtataTablesにもPDF出力機能はあります。もしサーバーサイドでデータを作らず、全件HTMLに表示する様な使い方をしているなら、こちらを使用します。
DataTables example - PDF - open in new window
サーバーサイド: WeasyPrintを使ったPDF出力
サーバーサイドでPDFを作る方法を以下に示します。 PDFを作成するPythonライブラリとしてはReportLabが有名な様です。Djangoの公式ドキュメントでは、こちらを利用した例が乗っています。
Open Source - ReportLab.com Outputting PDFs with Django | Django documentation | Django
ただしReportLabは表示のためのコードが複雑です。今回はWeasyPrintを用います。こちらはCSSを当てることで、スタイルを簡単に適応することができます。
今回の方針としてDataTablesでソートや絞り込みを行ったデータを、全ページネーション分PDFとして出力することにします。 そのため、django-datatables-viewのクラスを利用することにします。
イメージとしては、DataTables表示のためのソートや検索の処理後、DataTables表示用JSONレスポンスを作る代わりに、PDF出力用レスポンスを返す様に します。
フロント: PDF印刷のためのボタンとリクエストを作る
URLはDataTablesのサーバーサイド処理と共通で使います。Viewも共通です。そのため、「PDFの出力リクエストである」ことを示すための目標を付加する必要があります。
今回は、クエリストリングにoutput=pdf
を付加します。output=pdf
がクエリストリングに付加されている時に、PDF印刷処理に分岐する様にします。
cms/static/js/index.js
- 補足です。
- DataTableオブジェクトの
table.ajax.params
メソッドによって、画面上での操作によるソートや検索の条件をオブジェクトとして取得することができます。 $.param
メソッドによって、オブジェクトをクエリストリングにすることができます。- 最後に、前述した
output=pdf
を付加しています。ただの目印なので、わかればなんでもOKですが、datatablesが利用しているKeyは避けます。
- DataTableオブジェクトの
$('#pdf_button').on('click', function () { const table = $('#table_id').DataTable(); const query_params = table.ajax.params(); // 1ページあたりのデータ取得件数: -1は全件。 query_params.length = -1; query_string = $.param(query_params); const url = '/datatable-view/' + '?' + query_string + '&' + 'output=pdf'; window.open(url, '_blank'); });
バックエンド: リクエストを元にPDFを組み立てる
cms/views.py
- 先ほどの、DataTablesのサーバーサイドのviewに記述を付加します。
prepare_results
関数はBaseDatatableViewのメソッドです。ソートやフィルター済みの、Djangoのクエリセットオブジェクトを引数として受け取ります。この関数に割り込み、クエリセットをself.query_set_for_pdf
と命名したインスタンス変数に保持します。後に、これを用いてHTMLをレンダリングし、それをもとにPDFを作成します。render_to_response
関数は、BaseDatatableViewがレスポンスを返す本当に最後の部分の関数です。この部分でクエリストリングのoutput=pdf
を判定し、元の処理(DataTabelsに描画用のjsonを作る)のか、PDFを出力するのかを分岐する様にしています。- なお、
self._querydict
はBaseDatatableViewが処理の上流で作成している、queryパラメータの一覧です。
- なお、
その他、CSSは以下を参考に当てておきます。
cms/static/css/pdf.css
を作成しました。
class IndexDatatableView(BaseDatatableView): model = Person # この順番で表示される。 columns = ['name', 'prefecture__name', 'age'] def render_column(self, row, col): if col == 'prefecture__name': return row.prefecture.name return super().render_column(row, col) def prepare_results(self, qs): """PDF出力様に、ソートやフィルター後のquerysetを保持""" self.query_set_for_pdf = qs return super().prepare_results(qs) def render_to_response(self, context): """ output=pdfのクエリパラメータが存在するときPDFを出力""" if self._querydict.get('output', 0) == 'pdf': return self.create_pdf() return super().render_to_response(context) def get_dataset_for_pdf(self): return { 'dataset': [ [person.name, person.prefecture.name, person.age] for person in self.query_set_for_pdf ] } def create_pdf(self): context = self.get_dataset_for_pdf() html_string = render_to_string( template_name='cms/pdf_template.html', context=context, request=self.request) pdf_file = HTML(string=html_string).write_pdf( # ここでCSSを指定可能.projectのrootからのパスを書く。 stylesheets=[CSS('cms/static/css/pdf.css')]) stream = io.BytesIO(pdf_file) return FileResponse( stream, as_attachment=True, filename='test.pdf')
cms/templates/cms/pdf_template.html
- PDF出力のためのtemplateです。データを埋める部分のみ表示しています。全体は、GitHubをご参照ください。
- Djangoのテンプレートシステムを使って動的にデータをテーブル要素に埋めています。
<tbody> {% for row in dataset %} <tr> {% for cell in row %} <td>{{ cell }}</td> {% endfor %} </tr> {% endfor %} </tbody>
動作確認
localhost:8000/idnex/
にアクセスし、DataTablesを操作した後にPDFボタンを押します。以下の様なPDFが出力されたら成功です。