Python + Django Rest framework + igGrid で REST API を利用したCRUDの実装

こんにちは。インフラジスティックス・ジャパン株式会社 テクニカルコンサルティングチームの中江です。
普段はお客様へ技術サポートやトレーニングコンテンツの提供などを主な業務として行っています。

[Infragistics（以下IG）](https://jp.infragistics.com/) は 開発ツールのソフトウェアベンダーです。主に UI コンポーネントの開発・提供を様々なプラットフォームに対して行っています。今回はバックエンドの言語にとらわれず IG の豊富な UI コンポーネントを Web アプリケーションに導入できる Ignite UI for jQuery という製品を取りあげます。

Ignite UI for jQuery
https://jp.infragistics.com/products/ignite-ui

IG の UI コンポーネント郡の中でも特に利用頻度の高いものとして、グリッドコンポーネントがあります。グリッドコンポーネントはデータを表組みで表示するコンポーネントのことです。Ignite UI for jQuery の igGrid は以下のような機能が組み込まれています。

* 列の集計
* グループ化
* ソート
* フィルタリング
* ページング
* 列の固定
* 列の移動
* 行の新規追加、更新（編集）、削除
* セルの結合
* 複数行レイアウト
* 仮想化

本記事では Python + Django で構築したウェブアプリケーションと Ignite UI for jQuery を組み合わせて、CRUD の機能を持った igGrid を実装する方法をご紹介します。

Django には Django REST framework という Web API の実装がかなり簡単にできるフレームワークがあります。

Django REST framework
https://www.django-rest-framework.org/

また、Ignite UI for jQuery の igGrid には データソースとして REST で取得したデータをバインディングすることが可能です。グリッド上の新規行追加、更新、削除の内容に基づいて適切な処理を REST に対して行います。

REST の更新 (igGrid)
https://jp.igniteui.com/help/iggrid-rest-updating

この２つの機能（Django REST framework と REST 連携可能な igGrid）を組み合わせることでとても簡単に CRUD の機能を持った igGrid を実装することが可能です。

最終的な実現イメージは以下のようになります。
[![Image from Gyazo](https://i.gyazo.com/b0d480bb1fc9fb6544064d84678bb307.gif)](https://gyazo.com/b0d480bb1fc9fb6544064d84678bb307)

データをグリッドコンポーネントを使って一覧表示し、新規追加、更新、削除の処理をUIで操作しています。Saveボタンをクリックするとそれぞれの処理がAPIによって実行され、データベースが更新されます。

## Python + Django で REST を実装する

Django において、データベースとアプリの連携までは実装できているという前提で進めていきます。
もし今回はじめて Django を始めてみようという方がいらっしゃいましたら、以下のドキュメントを参考に、「はじめての Django アプリ作成、その2」までの内容を進めたうえで以降の内容をご覧ください。

Django 2.2 ドキュメント
https://docs.djangoproject.com/ja/2.2/contents/

また今回は、
- python 3.7
- Django 2.2.14
- Ignite UI for jQuery 20.1

にて実装を行なっています。

### Django REST framework のインストールと初期設定

django-filter も合わせてインストールします。

``` bash
$ pip install djangorestframework
$ pip install django-filter
```

```settting.py
INSTALLED_APPS = [
    ...
    'rest_framework',
]
```

### API 用のアプリを作成

```
$ python manage.py startapp api
```

### モデルの設定

今回は以下ような注文管理用のモデルを作成しました。

```api/models.py
from django.db import models
# Create your models here.
class Ordering(models.Model):
    Shop = models.CharField(max_length=100) #出荷名
    Address = models.CharField(max_length=200) #配送先住所
    TotalNumber = models.IntegerField(default=0) #項目の合計数
    TotalPrice = models.FloatField(default=0) #総額
```

また、すでにデモ用のデータは以下のようなものを入れ込んでいる前提で進めます。

|  id  |  Shop  |  Address  |  TotalNumber  |  TotalPrice  |
| ---- | ---- | ---- | ---- | ---- |
|  1  |  小料理ひろ  |  味美白山町 1-9-X  |  27  |  440  |
|  2  |  食所あんどう  |  志免町御手洗 51-X  |  49  |  1863.4  |
|  ...  |  ...  |  ...  |  ...  |  ...  |

### Serializer の定義

新規に serializer.py というファイルを作成して、以下のように設定します。

```api/serializer.py
# coding: utf-8
from rest_framework import serializers
from .models import Ordering
class orderingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Ordering
        fields = ('id', 'Shop', 'Address', 'TotalNumber', 'TotalPrice')
```

### View の設定

View ではモデルと先ほど用意した Serializer を組み合わせた ViewSet を定義します。

```api/views.py
# coding: utf-8
import django_filters
from rest_framework import viewsets, filters
from .models import Ordering
from .serializer import orderingSerializer
class orderingViewSet(viewsets.ModelViewSet):
    queryset = Ordering.objects.all()
    serializer_class = orderingSerializer
```

### URL の設定

URL の設定をします。
まず api/urls.py を新規作成し、以下のように記述します。

```api/urls.py
# coding: utf-8
from rest_framework import routers
from .views import orderingViewSet
router = routers.DefaultRouter()
router.register(r'order', orderingViewSet)
```

ルートの urls.py には以下のようなルーティング設定を行います。

```urls.py
from django.contrib import admin
from django.conf.urls import url, include
from django.urls import include, path
from api.urls import router as api_router
urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^api/', include(api_router.urls)),
]
```

以上で API を利用したデータのやり取りが出来るようになりました。サーバーを起動して以下の URL にアクセスしてみます。
http://127.0.0.1:8000/api/order/
[![Image from Gyazo](https://i.gyazo.com/0a277afbe3998eff02883a2e987f0162.png)](https://gyazo.com/0a277afbe3998eff02883a2e987f0162)

JSON 形式でデータの一括取得が出来ていることを確認できます。
同じ要領で、以下のように ID を指定した URL にアクセスすると、
http://127.0.0.1:8000/api/order/1
[![Image from Gyazo](https://i.gyazo.com/ae3a160be4b74bd41e4e035dce472821.png)](https://gyazo.com/ae3a160be4b74bd41e4e035dce472821)

該当のデータのみ表示することが出来ますし、PUT による情報の更新や DELETE による削除の処理もこの画面で行うことが可能で、データベースとの連携も対応してくれます。

これで Django 側の準備が整いましたので igGrid の実装に移っていきます。

## igGrid の実装と、REST との連携

バックエンドの受け入れ態勢は整いましたので、フロントエンドの組み込みを行なっていきます。
まず igGrid 用のアプリを新たに追加します。

### igGrid 用のアプリを作成

```
$ python manage.py startapp grid
```

### igGrid 用のテンプレートを作成

grid/templates/grid ディレクトリにテンプレート用の html を新規作成し、編集していきます。今回は <head> 部分やスクリプトをパーツ分けするなどの処理はせずに、全て１枚の index.html に収める形で記述していきます。

まず必要なライブラリの読み込みは以下のように CDN から読み込む形としました。

```grid/templates/grid/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ig-grid on Django</title>
    <link href="https://cdn-na.infragistics.com/igniteui/2020.1/latest/css/themes/infragistics/infragistics.theme.css" rel="stylesheet">
    <link href="https://cdn-na.infragistics.com/igniteui/2020.1/latest/css/structure/infragistics.css" rel="stylesheet">
    <link href="https://igniteui.github.io/help-samples/css/apiviewer.css" rel="stylesheet" type="text/css">
    <style type="text/css">
        input.button-style
        {
            margin-top: 10px;
        }
    </style>
  </head>
  <body>
  ...
    <script src="https://ajax.aspnetcdn.com/ajax/modernizr/modernizr-2.8.3.js"></script>
    <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
    <script src="https://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
    <script src="https://cdn-na.infragistics.com/igniteui/2020.1/latest/js/i18n/infragistics-ja.js"></script>
    <script src="https://cdn-na.infragistics.com/igniteui/2020.1/latest/js/infragistics.core.js"></script>
    <script src="https://cdn-na.infragistics.com/igniteui/2020.1/latest/js/infragistics.lob.js"></script>
    <script src="https://igniteui.github.io/help-samples/js/apiviewer.js"></script>
    <script src="https://cdn-na.infragistics.com/igniteui/2020.1/latest/js/modules/i18n/regional/infragistics.ui.regional-ja.js"></script>
    <!-- Used to add modal loading indicator for igGrid -->
    <script src="https://www.igniteui.com/js/grid-modal-loading-inicator.js"></script>
  </body>
</html>
```

また、今回の実装方法として、グリッドの各行への変更内容を一旦保持して、保存ボタンのクリックによって全ての変更をまとめてデータベースにコミットするような形としております。
本記事では一括でのコミットに関しての説明は省略いたします。以下のドキュメントに詳細が書かれておりますので参考にしていただければ幸いです。

グリッド - 編集
https://jp.igniteui.com/grid/basic-editing

更新の概要 (igGrid)
https://jp.igniteui.com/help/iggrid-updating

### REST データのデータバインディング

REST をサポートするために DataSource から拡張された RESTDataSource を用いてグリッドにデータバインディングを行います。

ig.RESTDataSource
https://jp.igniteui.com/help/api/2020.1/ig.restdatasource

```grid/templates/grid/index.html
<script type="text/javascript">
    $(function () {
        var ds = new $.ig.RESTDataSource({
            dataSource : "/api/order/",
            restSettings: {
                create: {
                    url: "/api/order/", //APIのエンドポイントを指定
                },
            }
        });
        var grid = $("#grid").igGrid({
            dataSource: ds, //RESTDataSource をバインド
            type: "json",
            columns: [
                ...
            ],
            primaryKey: "id",
            autoGenerateColumns: false,
```

上記の例では restSettings の create の エンドポイントに /api/order/ を指定していますので、グリッドで新規に行を作成した際に /api/order/ に対して POST を行なってくれます。
また、その他のエンドポイントを特に指定しなくても、DELETE や PUT なども適宜エンドポイントを /api/order/1 のように解釈してくれます。

ルーティングの調整をして、こちらの状態で一度テストしてみます。
[![Image from Gyazo](https://i.gyazo.com/0c031bef5e734a983b8081dddf02efd2.png)](https://gyazo.com/0c031bef5e734a983b8081dddf02efd2)

グリッドにデータの一覧を表示することが出来ました。これはREST によって GET 処理が成功したことを意味しています。

では POST の処理はどうでしょうか。新規行の追加処理のテストをしてみます。
[![Image from Gyazo](https://i.gyazo.com/158bbb77510ff35f1e5b208dbc8aa59f.png)](https://gyazo.com/158bbb77510ff35f1e5b208dbc8aa59f)

POST の処理はエラーとなります。
これは ajax を通して POST の処理をした際に Django 側でセキュリティの保護処理が行われたためのエラーです。クロスサイトリクエストフォージェリ（CSRF）対策のために ajax を利用する場合にはトークンを生成して付与する必要があります。

しかしながら ig.RESTDataSource にはトークンを指定する組み込みのオプションはないため、別の方法での実装が必要です。

### CSRF トークンの設定

jQuery の ajaxSetup() メソッド の beforeSend パラメーターを利用することで、ajax 通信が発生する前の処理を設定することが可能です。以下のようにリクエストヘッダーの X-CSRFToken に対してトークンを指定します。

Django には CSRF 対策用のトークンが簡単に取得できるテンプレートタグがはじめから用意されているため、以下の記述をスクリプトに追加します。

```grid/templates/grid/index.html
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
    }
});
```

もう一度テストしてみましょう。
[![Image from Gyazo](https://i.gyazo.com/fb7837d612887c0df941580abca9b836.png)](https://gyazo.com/fb7837d612887c0df941580abca9b836)

無事新しいデータが POST されました。
データベースでも新しいデータの存在が確認できます。

```
% python manage.py shell
>>> from api.models import Ordering
>>> Ordering.objects.filter(id=21)
<QuerySet [<Ordering: Ordering object (21)>]>
```

続けて PUT と DELETE の動作テストをします。
先ほど新規に追加したデータを削除し、ID20 のデータの総額を変更します。
[![Image from Gyazo](https://i.gyazo.com/f2db6859a3203c6c7df4bb4bae776274.png)](https://gyazo.com/f2db6859a3203c6c7df4bb4bae776274)

Save ボタンをクリックしコミットすると以下のエラー文とともに、PUT の処理にエラーが発生していることが確認できます。

`You called this URL via PUT, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining PUT data. Change your form to point to 127.0.0.1:8000/api/order/20/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.`

Django は PUT のエンドポイントの URL がスラッシュで終わっていない形の場合、エラーを返します。また、ig.RESTDataSource のデフォルトの PUT のエンドポイントは最後にスラッシュがつかない /api/order/20 のような形になるため、このエラーが発生します。

Django 側の設定変更によってスラッシュなしを受け入れることも可能ですが、ig.RESTDataSource もエンドポイントのテンプレートをカスタマイズ可能ですので、ig.RESTDataSource を修正していきます。

### Django の仕様に合わせて ig.RESTDataSource の restSettings を変更

```grid/templates/grid/index.html
var ds = new $.ig.RESTDataSource({
    dataSource : "/api/order/",
    type: "json",
    restSettings: {
        create: {
            url: "/api/order/",
            template: "/api/order/"
        },
        update: {
            template: "/api/order/${id}/" //スラッシュで終わる形に変更
        },
        remove: {
            url: "/api/order/"
        }
    }
});
```

この時、update に対して行なったテンプレート設定が create にも及ぶため、create にも元々与えているエンドポイントと同じ URL を template の URL としても指定しておきます。

最後に、保存ボタンを押した時の処理として以下の2行を加えます。

```grid/templates/grid/index.html
grid.igGrid("commit");
grid.igGrid("dataBind");
```

グリッドの ID 列は、データベースのテーブル上のプライマリキーである id と対応していますが、フロントエンドで割り振られた id が、データベース上で割り振られるべき id と一致するとは限りません。そのためデータベースへのコミットを行った後に再度データバインドを行うことによって、データベース上で割り当てられた id をグリッドで反映するという処理を行っています。

改めて、今回作成したデモの最終的な振る舞いを見てみましょう。
[![Image from Gyazo](https://i.gyazo.com/b0d480bb1fc9fb6544064d84678bb307.gif)](https://gyazo.com/b0d480bb1fc9fb6544064d84678bb307)

GET, POST, PUT, DELETE の処理を REST と igGrid によってスムーズに一括で行えるようになりました。また、今回 Django REST framework を利用することによってバックエンドとフロントエンドの役割分担が出来ている点が特徴かと思います。

今回作成したデモアプリケーションは以下にアップロードしておりますのでご興味のある方は触ってみていただければと思います。
https://github.com/igjp-kb/Ignite-UI-for-JavaScript/tree/master/igGrid/rest_iggrid