2
2

More than 3 years have passed since last update.

【Python】DjangoでHandsontableをAjaxしてみる 【JaveScript】

Last updated at Posted at 2021-06-23

はじめに

HandsontableというJavaScriptライブラリを使うと、WEBでExcelのようなスプレッドシートライクな入力が可能になります。が、更新方法が分からなかったので、いろいろ調べてみました。

なお、JavaScriptについては全くの初学者なので、おかしいところも多々あると思いますが、ご笑覧いただければ幸いです。

できたもの

2.PNG
データベース管理システム(PostgreSQL)からデータを取得し、Handsontableを使ってテーブルを表示。

3.PNG
ブラウザ上でデータを更新し(garlicをAPPLEへ)、上書きボタンをクリックすると…

4.PNG
Ajax(非同期通信)を使って、ポスグレ上のデータを更新します。

5.PNG
念のため、管理サイトでも更新を確認。

準備

概要は以下の通りです。

  • django admin startproject conf .
  • タイムゾーン、言語の設定
  • STATICFILES_DIRSの設定
  • TEMPLATESの設定
  • DATABASE(PostgreSQL)の設定

アプリケーション作成

$ python manage.py startapp hot

アプリケーションを作成します。

conf/settings.py
INSTALLED_APPS = [
    'hot.apps.HotConfig',
    # 以下省略
]

次にsettings.pyにアプリを登録します。

conf/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('hot/', include('hot.urls')
    # 以下省略
]

confフォルダのurls.pyを修正します。

hot/urls.py
from django.urls import path
from  . import views


app_name = 'hot'

urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    # path('ajax_update/', views.ajax_update, name='ajax_update')
]

hotフォルダ内にurls.pyを作成し、上記のように書きますクラスベースビューのTopはテーブルを表示するページで、関数ビューのajax_updateはデータを上書きするためのページです。Ajaxなので、ページ遷移はしません。

モデルを作成

hot/models.py
from django.db import models


class Merchandise(models.Model):
    merchandise = models.CharField('merchandise', max_length=255)
    price = models.IntegerField('price')
    origin = models.CharField('origin', max_length=255)

    def __str__(self):
        return self.merchandise
hot/admin.py
from django.contrib import admin
from .models import Merchandise

admin.site.register(Merchandise)
$ python manage.py makemigrations hot
$ python manage.py migrate
$ python manage.py createsuperuser

モデルを作成します。商品名と価格と生産地をデータとしました。管理サイトへの登録、マイグレーション、スーパーユーザーの作成を忘れずに。

1.PNG
適当に三つほどデータを入れておきます。

テーブルを表示するページ(Top)を作成

hot/views.py
import psycopg2
from django.http.response import JsonResponse
from django.shortcuts import render
from django.views import generic
from .models import Merchandise


class Top(generic.TemplateView):
    template_name = 'table/top.html'

    # contextをテンプレートファイルに渡す。
    def get_context_data(self, **kwargs):
        # はじめに継承元のメソッドを呼び出す
        context = super().get_context_data(**kwargs) 

        # データベースに接続→データ取得→切断
        connect_str  = ''
        connect_str += "dbname=test1 "
        connect_str += "user=postgres "
        connect_str += "password=password"
        conn = psycopg2.connect(connect_str)
        cur = conn.cursor()
        cur.execute('select * from hot_merchandise order by id')
        table = cur.fetchall()
        cur.close()
        conn.close()

        # handsontable用のデータを作成
        key = ('id', 'marchandise', 'price', 'origin')
        data = []
        for record in table:
            data.append(dict(zip(key, record)))
        columns = []
        for k in key:
            columns.append(dict(data=k))
        colHeaders = list(key)

        context['data'] = data
        context['columns'] = columns
        context['colHeaders'] = colHeaders
        return context

Topページのビューを作成します。データベースからデータを取得し、Handsontable用にかたちを整えて、テンプレートファイルに渡します。テーブルの名称はhot_merchandiseとなっています。アプリ名小文字_モデル名小文字になるみたいです。

参考
# table(整形前のデータ)
[(1, 'carrot', 150, 'kanagawa'), (2, 'garlic', 200, 'aomori'), (3, 'onion', 300, 'hyogo')]

# data
[
    {'id': 1, 'merchandise': 'carrot', 'price': 150, 'origin': 'kanagawa'}, 
    {'id': 2, 'merchandise': 'garlic', 'price': 200, 'origin': 'aomori'}, 
    {'id': 3, 'merchandise': 'onion', 'price': 300, 'origin': 'hyogo'}
]

# columns
[
    {'data': 'id'}, 
    {'data': 'merchandise'}, 
    {'data': 'price'}, 
    {'data': 'origin'}
]

# colHeaders
['id', 'merchandise', 'price', 'origin']

ちなみに、handsontableに渡すデータの形状は上記のような感じです。

base.html
{% load static %}
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

    <!-- handsontable -->
    <script src="{% static 'hot/jquery.min.js' %}"></script>
    <script src="{% static 'hot/handsontable.full.min.js' %}"></script>
    <link rel="stylesheet" href="{% static 'hot/handsontable.full.min.css' %}">

    <title>HandsontableでAjax</title>
  </head>
  <body>
    <!--  ナビゲーションバー -->
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="{% url 'home:top' %}">Test</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>

      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item">
            <a class="nav-link" href="{% url 'home:top' %}">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="{% url 'table:top' %}">Table</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="{% url 'hot:top' %}">HOT</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
          </li>
        </ul>
      </div>
    </nav>

    <div class="container">
        {% block content %}{% endblock %}
    </div>


    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <!-- jQueryはslim版じゃないやつを使う。slimはajaxに対応していないため。 -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    {% block extrajs %}{% endblock %}
  </body>
</html>

ベースとなるテンプレートファイルを作成します。handsontableのjsやcssはCDNも公開されています。この記事では触れていないアプリ(home, table)へのリンクもありますが、そちらは無視してください。

hot/top.html
{% extends 'base.html' %}
{% load static %}

{% block content %}
    <h3 class="mt-3">Table</h3>
    <div id="grid"></div>
    <div class="mt-1">
        <button type="button" id="btn_update_data" class="btn btn-primary">上書き</button>
    </div>
{% endblock %}

{% block extrajs %}
    <script>
        'use strict';

        // 変数の定義
        var data = {{ data | safe }};
        var columns = {{ columns | safe }};
        var colHeaders = {{ colHeaders | safe }};

        // テーブルを表示
        var container = document.getElementById('grid');
        var hot = new Handsontable(container, {
            data: data,
            columns: columns,
            columnSorting: true,
            rowHeaders: true,
            colHeaders: colHeaders,
            filters: true,
            dropdownMenu: true
        });
    </script>
{% endblock %}

top.htmlはbase.htmlをextendsします。block contentには見出しとテーブルと上書きボタンを設置しています。テーブルを書き込むdiv要素のid属性を'grid'としています。それを受けてextrajsブロックでgetElementById('grid')としています。

2.PNG

ブラウザ上ではこのような画面が表示されます。この時点でブラウザ上でデータは編集可能ですが、データベースの更新はできないので、リロードすると元に戻ります。

3.PNG
↑ブラウザ上では編集できますが、更新はできません↓

2.PNG
↑リロードすると元に戻ってしまいます。

データを上書きする

データを上書き更新できるようにしたいと思います。おおまかな手順としては、テーブルの下に設置した上書きボタンを押下すると、その時点のデータを取得し、データベースを更新するようにしたいと思います。

hot/top.html
{% block extrajs %}
    <script>
        // 省略

        // Ajaxでデータベースを更新
        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({
            beforeSend: function (xhr, settings) {
                if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                    xhr.setRequestHeader("X-CSRFToken", csrftoken);
                }
            }
        });

        // ↑ ここまではおまじない

        $('#btn_update_data').on('click', function(event){
            event.preventDefault();

            // ボタンをクリックした時点のデータを取得
            var data_hot = hot.getData();
            console.log(data_hot);

            // Ajax実行
            $.ajax({
                'url': '{% url "hot:ajax_update" %}',
                'type': 'POST',
                'data': {
                    'temp_data': 'aaa',
                    'str_hot': JSON.stringify(data_hot),
                    'str_col': JSON.stringify(columns)
                },
                'dataType': 'json'
            })
            .done(function(data) {
                window.alert("done!!");
            })
            .fail(function(){
                window.alert("error!!");
            });
        });
    </script>
{% endblock %}

hot/top.htmlのextrajsブロックにスクリプトを追記します。前半はおまじないらしく、よくわかりません…。

$('#btn_update_data').on('click', function(event)以降がデータの上書きです。上書きボタンがクリックされたら、データを取得します。次にhot:ajax_upで指定した関数を実行するのですが、その時にdataを渡します。dataの中身は'temp_data'と'str_hot'と'str_col'です。'temp_data'は適当なデータです。'str_hot'はテーブルのデータをJSON形式に変換したものです。今回はJavaScript(Handsontable)からPythonへデータを渡すのですが、いろいろ調べてなんとかこの形にたどり着きました。(もっと良い方法があったら知りたい…)

hot/urls.py
app_name = 'hot'

urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    path('ajax_update/', views.ajax_update, name='ajax_update')  # 追加
]

top.htmlにhot:ajax_updateというURLを指定しましたが、その様なURLは無いので作ります。次にviews.pyにajax_updateという関数ビューを作ります。

hot/views.py
def ajax_update(request):
    # POSTされたデータを受け取る。この時点では文字列。
    str_hot = request.POST.get('str_hot')  # リストっぽい文字列[[1, "apple", 200, "aomori"], ...]
    str_col = request.POST.get('str_col')  # リストっぽい文字列[{'data':'id'}, {'data':'merchandise}, ...]

    # 文字列をリストに変換
    exec('data_hot={}'.format(str_hot), globals())
    exec('data_col={}'.format(str_col), globals())
    lis_col = []
    for elm in data_col:
        lis_col.append(elm['data'])

    # 辞書を作成
    # [{'id':1, 'marchandise'='apple', 'price':100, 'origin':"aomori"}, {...}, ...]
    lis_dic = []
    for row in data_hot:
        lis_dic.append(dict(zip(lis_col, row)))


    # データベースに接続
    connect_str  = ''
    connect_str += "dbname=test1 "
    connect_str += "user=postgres "
    connect_str += "password=password"
    conn = psycopg2.connect(connect_str)
    cur = conn.cursor()

    # データ更新
    for dic in lis_dic:
        id = dic['id']
        sql = 'UPDATE hot_merchandise SET'
        for k,v in dic.items():
            if k != 'id':
                if type(v) is str:
                    sql += f" {k}='{v}',"
                elif type(v) is int or type(v) is float:
                    sql += f" {k}={v},"
        sql = sql[:-1]  # 末尾のカンマを削除
        sql += f" where id={id};"
        print(sql)
        # 参考: "UPDATE hot_merchandise SET marchandise='orange', price=200, origin='aomori' WHERE id=1"
        cur.execute(sql)
        conn.commit()

    # データベース切断
    cur.close()
    conn.close()

    # レスポンスを返さないといけないらしいので、適当に…
    return JsonResponse({'ret': 'return'})

いよいよデータベースの更新です。いろいろ書いてありますが、やっていることはデータを受け取り、形を整えて、SQL文を生成し、実行の繰り返しです。ポイントは、ajaxで送られてきたデータは一見リストの様な形式をしていますが文字列です。なので、それをリストに戻してやる必要があります。いろいろ試した結果exec関数を使うことになりました。(もっと良い方法があったら知りたい…切実に…)

実行してみる

3.PNG
ブラウザ上でデータを更新し、上書きボタンをクリックします。

4.PNG
データの更新がうまくいけば、このようなウィンドウが開きます。ページ遷移はしません。

3.PNG
前回はリロードすると元に戻ってしまいましたが、今回は大丈夫です。

5.PNG
管理サイト上でもデータベースが更新されています!

おわりに

なんとか動いてよかった…。あやふやなところがたくさんあるので、少しずつ勉強していきたいと思います。

2
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2