qtatsuの週報

初心者ですわぁ

サーバーサイド処理しているDataTablesをPDFで出力する

前書き

DtataTablesで表示するデータをサーバーサイドで用意し、またPDFを出力する機能をDjangoで作成することになりました。その際、 thinkAmiさんから助言をいただきました。本記事は、Datatables/django-datatables-view/WeasyPrint/Djangoを使用し、PDF出力を実装する方法 をまとめたものです。

また、使用しているコードの全体は以下のGitHubレポジトリにアップしました。

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ツール。

仕様と実装方針

以下の様な、シンプルな名簿を想定します。

f:id:Qtatsu:20200813162515j:plain
ER図

参加者 出身 年齢
太郎 東京 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サーバーサイド処理の実装

  • 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は避けます。
    $('#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が出力されたら成功です。

f:id:Qtatsu:20200813163252p:plain
PDF