はじめに
HandsontableというJavaScriptライブラリを使うと、WEBでExcelのようなスプレッドシートライクな入力が可能になります。が、更新方法が分からなかったので、いろいろ調べてみました。
なお、JavaScriptについては全くの初学者なので、おかしいところも多々あると思いますが、ご笑覧いただければ幸いです。
できたもの
データベース管理システム(PostgreSQL)からデータを取得し、Handsontableを使ってテーブルを表示。
ブラウザ上でデータを更新し(garlicをAPPLEへ)、上書きボタンをクリックすると…
Ajax(非同期通信)を使って、ポスグレ上のデータを更新します。
準備
概要は以下の通りです。
- django admin startproject conf .
- タイムゾーン、言語の設定
- STATICFILES_DIRSの設定
- TEMPLATESの設定
- DATABASE(PostgreSQL)の設定
アプリケーション作成
$ python manage.py startapp hot
アプリケーションを作成します。
INSTALLED_APPS = [
'hot.apps.HotConfig',
# 以下省略
]
次にsettings.pyにアプリを登録します。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('hot/', include('hot.urls')
# 以下省略
]
confフォルダの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なので、ページ遷移はしません。
モデルを作成
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
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
モデルを作成します。商品名と価格と生産地をデータとしました。管理サイトへの登録、マイグレーション、スーパーユーザーの作成を忘れずに。
テーブルを表示するページ(Top)を作成
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に渡すデータの形状は上記のような感じです。
{% 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)へのリンクもありますが、そちらは無視してください。
{% 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')としています。
ブラウザ上ではこのような画面が表示されます。この時点でブラウザ上でデータは編集可能ですが、データベースの更新はできないので、リロードすると元に戻ります。
データを上書きする
データを上書き更新できるようにしたいと思います。おおまかな手順としては、テーブルの下に設置した上書きボタンを押下すると、その時点のデータを取得し、データベースを更新するようにしたいと思います。
{% 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へデータを渡すのですが、いろいろ調べてなんとかこの形にたどり着きました。(もっと良い方法があったら知りたい…)
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という関数ビューを作ります。
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関数を使うことになりました。(もっと良い方法があったら知りたい…切実に…)
実行してみる
データの更新がうまくいけば、このようなウィンドウが開きます。ページ遷移はしません。
前回はリロードすると元に戻ってしまいましたが、今回は大丈夫です。
おわりに
なんとか動いてよかった…。あやふやなところがたくさんあるので、少しずつ勉強していきたいと思います。