Python + Django + ag-grid で一括処理

[「Python + Django + SlickGrid で一覧表示」](https://qiita.com/t-iguchi/items/89376151eb73a25d8529)で、gridライブラリを使った一覧表示を試してみましたが、Slick Gridより、簡単に実装できる「ag-grid」というライブラリに出会ったので、Djangoと連携する例を投稿しておきます。

##1.インストール
[ag-grid](https://www.ag-grid.com/javascript-grid-getting-started/)は、javascript, angular, react, vueなどで使用できると書いてありますが、今回は、javascript版を使用します。


###(1)ダウンロード

[GitHUB](https://github.com/ag-grid/ag-grid)からダウンロードします。必要なcss, jsファイルは、distフォルダの中にあります。

###(2)アプリケーションを作成する。

Djangoなので、サンプルアプリケーションを作成します。

- 今回のメインとなるアプリケーションを作成する

```
python manage.py startapp hellojson2
```

- db用のアプリケーションを使います。

[Python+DjangoでDBの検索処理をモジュール化してみる](https://qiita.com/t-iguchi/items/ec39cbdfba3f678a57f9)

```
python manage.py startapp commons
```

- model用のアプリケーション

modelはすべてのアプリケーションで使うので、別でアプリケーションを作成しておきます。

```
python manage.py startapp entities
```

- アプリケーションの構成

以下のようなファイル構成になります。

staticフォルダの下のcss, jsフォルダに、ダウンロードしたag-gridのcss, jsを配置します。
※ ag-gridのほかに、JQueryも使います。bootstrapは好みに合わせて配置してください。

```
myproject
│  db.sqlite3
│  manage.py
│
├─commons
・・・
│  │  dbutils.py
・・・
│          
├─entities
│  │  admin.py
│  │  models.py
・・・
├─hellojson2
│  │  admin.py
│  │  apps.py
│  │  models.py
│  │  tests.py
│  │  urls.py
│  │  views.py
・・・
├─myproject
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
・・・
├─static
・・・
│  ├─css
│  │      ag-grid.css
│  │      ag-theme-balham.css
│  │      bootstrap.min.css
│  │      bootstrap.min.css.map
・・・
│  ├─js
│  │      ag-grid-community.min.noStyle.js
│  │      bootstrap.bundle.min.js
│  │      bootstrap.bundle.min.js.map
│  │      jquery-3.3.1.min.js
・・・
```

##2.アプリケーションの概要

- 従業員マスタのメールアドレスを一括更新するアプリケーションです。
- 画面に、従業員マスタの一覧を表示し、ユーザは、更新対象とする行をチェックして更新します。
- 初期表示時は、一覧は表示されません。検索ボタンで表示します。
- 更新ボタンを押すと、従業員マスタのメールアドレスを「bbb@localhost.com」に変更します。

##3.実装

###(1)model

- modelの作成

```entities\models.py
from django.db import models

# Register your models here
class Department(models.Model):
    """
    所属マスタ
    """
    deptid = models.CharField(max_length=16)
    deptname = models.CharField(max_length=64)
    upperdeptid = models.CharField(max_length=16)
    def __str__(self):
        return self.deptname

class Employee(models.Model):
    """
    従業員マスタ
    """
    empid = models.CharField(max_length=16)
    empname = models.CharField(max_length=64)
    deptid = models.CharField(max_length=16)
    mailaddress = models.CharField(max_length=128)
    def __str__(self):
        return self.empname
```

- プロジェクトにアプリケーションを追加する

```myproject\settings.py
INSTALLED_APPS = [
・・・
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bootstrap4', #追加
    'entities', #追加
    'hellojson2', #追加
]
```

- migrate

```
python manage.py makemigrations entities
python manage.py migrate entities
```

###(2)HTML

htmlのコメントにポイントを入れています。

```hellojson2\templates\hellojson2\index.html
{% extends 'commons/base.html' %}

{% load static %}

{% block links %}
<link rel="stylesheet" href="{% static 'css/ag-grid.css' %}" type="text/css"/>
<link rel="stylesheet" href="{% static 'css/ag-theme-balham.css' %}" type="text/css"/>
<style media="screen">
  .form-control{border: none !important;}
</style>
<script src="{% static 'js/ag-grid-community.min.noStyle.js' %}"></script>
{% endblock %}

{% block headertitle %}
一括更新 サンプル
{% endblock %}

{% block content %}
<br/>
<div class="form-control">
  <p>選択した行のメールアドレスを一括更新します。</p>
</div>
<div class="form-control text-right">
  <button class="btn btn-sm btn-primary" onclick="search(); return false;">検索</button>
  <button class="btn btn-sm btn-primary" onclick="sendData(); return false;">更新</button>
</div>
<div id="myGrid" style="height: 600px; width:100%; margin-top:10px;" class="ag-theme-balham"></div>

<script type="text/javascript">

  // ※DjangoとAjax通信するときは、CSRF対策をしておく必要がある。
  // django CSRF対策 ここから
  function getCookie(name) {
      var cookieValue = null;
      if (document.cookie && document.cookie != '') {
          var cookies = document.cookie.split(';');
          for (var i = 0; i < cookies.length; i++) {
              var cookie = jQuery.trim(cookies[i]);
              // Does this cookie string begin with the name we want?
              if (cookie.substring(0, name.length + 1) == (name + '=')) {
                  cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                  break;
              }
          }
      }
      return cookieValue;
  }

  var csrftoken = getCookie('csrftoken');

  function csrfSafeMethod(method) {
      // these HTTP methods do not require CSRF protection
      return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
  }

  $.ajaxSetup({
      crossDomain: false, // obviates need for sameOrigin test
      beforeSend: function(xhr, settings) {
          if (!csrfSafeMethod(settings.type)) {
              xhr.setRequestHeader("X-CSRFToken", csrftoken);
          }
      }
  });
  // django CSRF対策 ここまで

  //検索ボタン
  function search(){
    //fetchを
    fetch("{% url 'hellojson2:getlist' %}").then(function(response) {
      return response.json();
    }).then(function(data) {
      gridOptions.api.setRowData(data);
    })
  }

  //更新ボタン
  function sendData() {

    //ag-grid:選択した行の一覧を取得する。
    var selectedNodes = gridOptions.api.getSelectedNodes()
    var selectedData = selectedNodes.map( function(node) { return node.data })

    if (selectedData.length == 0) {
      alert("選択してください。")
      return;
    }
    //fetchではなく、JQuery Ajaxを使う例。
    $.ajax('/hellojson2/senddata/',
      {
        type: 'post',
        data: { query: JSON.stringify(selectedData) },
        'dataType': 'json'
      }
    )
    // 更新成功時に{"status": "OK"}が返ってくる
    .done(function(data) {
      alert(JSON.stringify(data));
    })
    // 検索失敗時には、その旨をダイアログ表示
    .fail(function( jqXHR, textStatus, errorThrown ) {
      window.alert('通信エラーが発生しました。 status=' + textStatus);
    })
    .always(function( jqXHR, textStatus ) {
    });

  }

  // ag-grid: 列定義
  // empidにcheckboxSelectionをセットすることで、checkboxを表示
  // headerCheckboxSelectionは、ヘッダにcheckboxを表示
  // empidは、リンクボタンで表示するようにしています。
  var columns = [
    {headerName: "従業員コード", field: "empid", headerCheckboxSelection: true, checkboxSelection: true, filter: true, editable: true,
      cellRenderer: function(params) {
        //将来的には、詳細表示などのURLをセットする。ひとまずgoogleのサイトを開く
        return '<a href="https://www.google.com" target="_blank">'+ params.value+'</a>'
      },
    },
    {headerName: "氏名", field: "empname", filter: true},
    {headerName: "メールアドレス", field: "mailaddress", filter: true},
    {headerName: "所属コード", field: "deptid", filter: true},
    {headerName: "所属名", field: "deptname", filter: true}
  ];

  //ag-grid: データ（JSON）
  var data = {{data|safe}}

  //ag-grid: Options
  //複数行選択にしています。
  var gridOptions = {
    columnDefs: columns,
    rowSelection: 'multiple'
//    rowData: data
  };

  // ag-grid: lookup the container we want the Grid to use
  var eGridDiv = document.querySelector('#myGrid');

  // ag-grid: create the grid passing in the div to use together with the columns & data we want to use
  new agGrid.Grid(eGridDiv, gridOptions);

  // ag-grid: 初期表示時のデータをセットしておく。
  gridOptions.api.setRowData(data);

</script>
{% endblock %}

```

###(3)ルーティング

```hellojson2\urls.py
from django.urls import path
from . import views

app_name = 'hellojson2'
urlpatterns = [
    path('', views.index, name='index'),                    # 初期表示
    path('getlist/', views.getlist, name='getlist'),        # 検索
    path('senddata/', views.senddata, name='senddata'),     # 更新
]
```

###(4)view.py

```hellojson2\views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse
import json
from commons.dbutils import exec_query
from django.views.decorators.csrf import ensure_csrf_cookie
from entities.models import Employee

#list
def index(request):
    data = []
    jsondata = json.dumps(data, ensure_ascii=False, indent=2)
    return render(request,
                  'hellojson2/index.html',
                  {'form_name': 'hellojson2', 'data': jsondata})

def getlist(request):
    sqltext="""SELECT
        a.id
      , a.empid
      , a.empname
      , a.deptid
      , a.mailaddress
      , b.deptname
    FROM
      public.entities_employee a
    INNER JOIN
      public.entities_department b
      on a.deptid=b.deptid
    ORDER BY
      a.id
        ;  """
    emplist=exec_query(sqltext);

    jsondata = json.dumps(emplist, ensure_ascii=False);

    return HttpResponse(jsondata)

@ensure_csrf_cookie
def senddata(request):
    txt = request.POST['query']
    datas = json.loads(txt)

    for element in datas:
#        print(element)
        entity = Employee.objects.filter(id=element['id']).first()
        entity.mailaddress = 'bbb@localhost.com'
        entity.save()

    data = {'status': 'OK'}

    json_str = json.dumps(data, ensure_ascii=False, indent=2)

    return HttpResponse(json_str)

```

###(5)プロジェクトのルーティング

最後に、myproject\urls.pyに、アプリのルーティングを追加します。

```myproject\urls.py
urlpatterns = [
    path('hellojson2/', include('hellojson2.urls')),   # ←ここを追加
    path('admin/', admin.site.urls),
]
```

##4.動作確認

- webサーバ起動

```
python manage.py runserver
```
- アプリにアクセスしてみます。

http://localhost:8000/hellojson2/

![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/90907bd2-4f52-152d-a466-d8313dd091bf.png)


##5.最後に

Slick Gridでここまでやろうと思ったら、pluginの追加やjavascriptの実装が複雑になりがちですが、ag-gridは実装も少なくてとってもいい感じです。大量データの表示も高速に表示できるので、私的にはag-gridかなと思いました。