python + django + w2uiで検索機能付き一覧表示

djangoとW2uiを使って、検索画面を作ってみます。業務アプリの要件で、画面に検索条件をつけて、さらに検索結果に対して任意の項目でフィルタができるようにとのことでしたので、それを実現するためのサンプルです。

##1.環境

###(1)バージョン

| カテゴリ       | バージョンなど       |
|:-----------------|------------------:|
| os               |windows 10 home 64bit | 
| python           |            3.7.2 |
| django             |              2.1.5|
|psycopg2 |2.7.6.1|
| project             | myproject|

###(2)アプリケーション

```cmd.prompt
python manage.py startapp w2ui
```

###(3)ファイル構成

```tree.txt
(抜粋)
├─commons
│  │  dbutils.py
・・・
├─myproject
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
・・・
├─querysample
│  │  admin.py
│  │  apps.py
│  │  models.py
│  │  tests.py
│  │  urls.py
│  │  views.py
・・・
├─static
│  ├─css
・・・
│  │      w2ui.min.css
・・・
│  ├─js
・・・
│  │      exportcsv.js
・・・
│  │      jquery-3.2.1.min.js
・・・
│  │      w2ui.min.js
├─w2ui
│  │  admin.py
│  │  apps.py
│  │  models.py
│  │  tests.py
│  │  urls.py
│  │  views.py
・・・
│  ├─templates
│  │  └─w2ui
│  │          index.html
・・・
```

##2.w2ui

[w2uiを使ってみる](https://qiita.com/t-iguchi/items/dd344683995d5b5fa773)を参照。

##3.model

[Python+Django+psycopg2で内部結合クエリを試す](https://qiita.com/t-iguchi/items/827865481e82bb32ad04)を参照。

##4.ルーティング

###(1)アプリケーションレベル

```w2ui\urls.py
from django.urls import path, include
from . import views

app_name = 'w2ui'
urlpatterns = [
    path('', views.index, name='index'),   # 初期表示
    path('search/', views.search, name='search'),   # 検索ボタン
]
```
###(2)プロジェクトレベル

```
urlpatterns = [
・・・
    path('w2ui/', include('w2ui.urls')),   # ←ここを追加    
    path('admin/', admin.site.urls),
]
```

##5.views.py

```w2ui\views.py
from django.shortcuts import render
from commons import dbutils #追加
import json
from django.http import HttpResponse

sqltext="""
  SELECT
    a.id
  , a.empid
  , a.empname
  , a.deptid
  , a.mailaddress
  , b.deptname
  FROM
    public.querysample_employee a
  INNER JOIN
    public.querysample_department b
  on
    a.deptid=b.deptid
      """

def getJoiner(joiner):
    """
    検索条件の結合子を判定する。joinerが文字列でなければ where そうでない場合 and を返す
    """
    ret =''
    if not joiner:
        ret = ' where '
    else:
        ret = ' and '
    return ret

def index(request):
    """
    初期表示
    """
    emplist=dbutils.exec_query(sqltext);
    form_name='w2ui sample'
    query = sqltext + ' ORDER BY a.id '
    return render(request, 'w2ui/index.html', {'searchdata':'[]', 'emplist':emplist,'form_name':form_name})

def search(request):
    """
    検索ボタン
    """
    query_columns = [ key for key in request.POST.keys() if not request.POST.get(key) == ['',None] ]
    query = sqltext
    joinstr = ''
    queryparams =[]
    for q_column in query_columns:
        # print(q_column+'='+request.POST.get(q_column))
        # リクエストパラメータ毎に、検索条件が指定された場合にSQLに条件を追加する。
        if q_column == 'empid' and request.POST.get(q_column):
            joinstr = getJoiner(joinstr)
            query += joinstr
            query += "a.empid = %s"
            queryparams.append(request.POST.get(q_column))
        elif q_column == 'empname' and request.POST.get(q_column):
            joinstr = getJoiner(joinstr)
            query += joinstr
            query += "a.empname like %s"
            # like検索の場合、クエリパラメータの前後に「%」をつける
            queryparams.append('%' + request.POST.get(q_column) + '%')
        elif q_column == 'deptname' and request.POST.get(q_column):
            joinstr = getJoiner(joinstr)
            query += joinstr
            query += "b.deptname like %s"
            # like検索の場合、クエリパラメータの前後に「%」をつける
            queryparams.append('%' + request.POST.get(q_column) + '%')
    # 並び順をクエリに追加する。
    query += ' ORDER BY a.id '

    emplist=dbutils.exec_query2(query, queryparams)
    jsondata = json.dumps(emplist, ensure_ascii=False)
    print(jsondata)
    response = HttpResponse(jsondata, content_type='application/json; charset=UTF-8')
    return response
```

##6.dbutils.py

パラメータクエリに対応しました。

```commons\dbutils.py
from django.db import connection

def exec_query(sqltext):
  """
  SQL(SELECT)を実行してカーソルを辞書に変換します
  """
  with connection.cursor() as c:
      c.execute(sqltext)
      "Return all rows from a cursor as a dict"
      columns = [col[0] for col in c.description]
      return [
          dict(zip(columns, row))
          for row in c.fetchall()
      ]

def exec_query2(sqltext, queryparams):
  """
  パラメータクエリを実行してカーソルを辞書に変換します
  """
  with connection.cursor() as c:
      c.execute(sqltext, queryparams)
      "Return all rows from a cursor as a dict"
      columns = [col[0] for col in c.description]
      return [
          dict(zip(columns, row))
          for row in c.fetchall()
      ]
```

##7.html

```w2ui\templates\w2ui\index.html
{% extends "commons/base.html" %}

{% load static %}

{% block title %}
{% endblock title %}

{% block links %}

<link rel="stylesheet" type="text/css" href="{% static 'css/w2ui.min.css' %}">
<style>
  /* w2ui grid toolbar の更新ボタンは非表示にする。 */
  #tb_grid_toolbar_item_w2ui-reload, .w2ui-icon-reload {display:none !important;}

  /* w2ui cssを少し調整する */
  .w2ui-form .w2ui-buttons {padding: 0 !important;}

  /* 見出しを少し調整する */
  p {font-size: 1.2em;}
</style>
{% endblock links %}

{% block headertitle %}
　　従業員一覧（w2ui grid版）
{% endblock %}

{% block content %}
<div id="w2uiform" style="width: 100%; height: 560px;position: relative;">
  <div class="w2ui-page page-0">
    <p>検索条件</p>
    <form id="searchform">
      {% csrf_token %}
      <label>従業員Code：</label><input name="empid" style="width: 90px;"/>
      <label>氏名：</label><input name="empname" style="width: 200px;"/>
      <label>所属名：</label><input name="deptname" style="width: 200px;"/>
    </form>
    <div style="width: 100%;text-align: right;">
      <button class="w2ui-btn" name="search">検索</button>
    </div>
    <p style="margin-top: 8px;">検索結果</p>
    <div id="main" style="width: 100%; height: 400px;position: relative;"></div>
    <div style="width:100px; margin-left: auto !important;">
      <button class="w2ui-btn" style="margin-right: 5px;" onclick="csv(); return false;">CSV出力</button>
    </div>
  </div>
</div>

{% endblock content %}

{% block scripts %}

<script src="{% static 'js/w2ui.min.js' %}"></script>
<script src="{% static 'js/exportcsv.js' %}"></script>
<script type="text/javascript">
  var searchdata = {{searchdata|safe}}
  var searchresult = {{emplist|safe}}

  function csv(){
    console.log(w2ui.grid.records);
    exportCSV(w2ui.grid.records);
  }

  function search(){
    // 画面をロックする。
    w2ui.w2uiform.lock('検索中・・・', true);
    $.ajax("{% url 'w2ui:search' %}",
      {
        type: 'post',
        data: $("#searchform").serialize(),
        'dataType': 'json'
      }
    )
    // gridをサーバから受信したデータで置き換える
    .done(function(jsondata) {
        w2ui.grid.clear(true);
        w2ui.grid.records = jsondata;
        w2ui.grid.refresh();
    })
    // 検索失敗時には、その旨をダイアログ表示
    .fail(function( jqXHR, textStatus, errorThrown ) {
      // alert('通信エラーが発生しました。 status=' + textStatus);
      w2ui.grid.error('通信エラーが発生しました。 status=' + textStatus);
    })
    .always(function( jqXHR, textStatus ) {
      // ロックを外す
      w2ui.w2uiform.unlock();
    });
  }

  $(function(){
    $('#w2uiform').w2form({
      name   : 'w2uiform',
      fields: [
          { field: 'empid', type: 'text', },
          { field: 'empname', type: 'text', },
          { field: 'deptname', type: 'text', },
      ],
      record : searchdata,
      actions: {
          'search': function () {
              search();
          },
      }
    });
    $('#main').w2grid({
      name: 'grid',
      recid: 'id',
      show: {
          footer    : true,
          toolbar    : true,
          lineNumbers    : true,
      },
      columns: [
          { field: 'empid', caption: '従業員Code', size: '100px', sortable: true, searchable: 'text', resizable: true },
          { field: 'empname', caption: '氏名', size: '100%', sortable: true, searchable: 'text', resizable: true },
          { field: 'deptid', caption: '所属Code', size: '100px', resizable: true, sortable: true, searchable: 'text' },
          { field: 'deptname', caption: '所属名', size: '240px', resizable: true, sortable: true, searchable: 'text' },
          { field: 'mailaddress', caption: 'メールアドレス', size: '160px', resizable: true, sortable: true, searchable: 'text' }
      ],
    });
    w2ui.grid.records = searchresult;
    w2ui.grid.refresh();
  })

</script>
{% endblock %}
```
csvのボタンについては、[JSONデータをCSVに出力する](https://qiita.com/t-iguchi/items/b15a0d592cb485d53f9a)を参照

##8.プロジェクトにアプリを追加する

```myproject\settings.py
INSTALLED_APPS = [
    'accounts.apps.AccountsConfig', #追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    ・・・・
    'w2ui', #追加
]
```

##9.動作確認

![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/907a92a8-8bc7-0213-2b12-dc3e82bd2f9a.png)

w2ui grid のフィルタ機能は簡単に実装できます。データもJSONデータなので他のgridライブラリと同じような感じで記述することができます。