アイコン無し=区別ない jsTree での保存

編集可能 jsTree の編集結果を Pyhon で、JSON 保存 - Oboe吹きプログラマの黙示録 の続き、

アイコン無し、フォルダ等の区別がない、自由にツリーを組み立てるようにする jsTree
編集可能 jsTree の編集結果を Pyhon で、JSON 保存 - Oboe吹きプログラマの黙示録 に書いたのは、
ファイルシステムのように、ファイルの下にフォルダ、ファイルを作ることはできない当たり前の規則が
あるのではなく、自由にツリーを組み立てるということは、どのノードの下にも子を作れること。
f:id:posturan:20181118104430j:plain

これは、jsTreeの描画JSで以下のようにすることと、CSSを
編集可能 jsTree の編集結果を Pyhon で、JSON 保存 - Oboe吹きプログラマの黙示録 
から修正すれば良い。

CSSは、

.jstree-default-large .jstree-notIcon {
   display: none;
}

があれば良い。

jsTreeの描画JSの修正は、
'core' の中の 'check_callback'を

   'check_callback' : true,

コンテキストメニューの "createFile" : {..} を削除して、createFolder を ”新規作成" として、修正、
作成されるアイコンも、'icon':'jstree-notIcon' として任意にCSS定義するアイコン無し属性にして、
"text" も違うものにする。

   "createFolder":{
      "separator_before": false,
      "separator_after": false,
      "icon": "contextmenu-icon fa fa-plus",
      "label": "新規作成",
      "_disabled": false,
      "action": function(data){
         var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
         inst.create_node(obj, { text:'New Item', 'icon':'jstree-notIcon' }, "last", function(new_node){
            try{
               inst.edit(new_node);
            }catch(ex){
               setTimeout(function(){ inst.edit(new_node); },0);
            }
         });
      }
   },

全体は以下のとおり

$(function(){
   $.jstree.defaults.core.themes.variant = "large";
   $.jstree.defaults.core.themes.responsive = true;

   $('#tree').jstree({
      'plugins': [ 'contextmenu','dnd' ],
      'core':{
         'data':{
            "url":"./tree.json",
            "dataType":"json"
         },
         'check_callback' : true,
      },
      "contextmenu":{
         "items":function($node){
            return {
               "createFolder":{
                  "separator_before": false,
                  "separator_after": false,
                  "icon": "contextmenu-icon fa fa-plus",
                  "label": "新規作成",
                  "_disabled": false,
                  "action": function(data){
                     var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                     inst.create_node(obj, { text:'New Item', 'icon':'jstree-notIcon' }, "last", function(new_node){
                        try{
                           inst.edit(new_node);
                        }catch(ex){
                           setTimeout(function(){ inst.edit(new_node); },0);
                        }
                     });
                  }
               },
               "rename":{
                  "separator_before": true,
                  "separator_after": false,
                  "icon": "contextmenu-icon fa fa-edit",
                  "label": "名称の変更",
                  "_disabled": false,
                  "action": function(data){
                     var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                     inst.edit(obj);
                  }
               },
               "remove":{
                  "separator_before": false,
                  "separator_after": false,
                  "icon": "contextmenu-icon fa fa-trash-alt",
                  "label": "削除",
                  "_disabled": function(data){
                     return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                  },
                  "action": function(data){
                     var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                     if (inst.is_selected(obj)){
                        inst.delete_node(inst.get_selected());
                     }else{
                        inst.delete_node(obj);
                     }
                  }
               },
               "cut":{
                  "separator_before": true,
                  "separator_after": false,
                  "icon": "contextmenu-icon fa fa-cut",
                  "label": "切り取り",
                  "_disabled": function(data){
                     return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                  },
                  "action": function(data){
                     var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                     if (inst.is_selected(obj)){
                        inst.cut(inst.get_top_selected());
                     }else{
                        inst.cut(obj);
                     }
                  }
               },
               "copy":{
                  "separator_before": false,
                  "separator_after": false,
                  "icon": "contextmenu-icon fa fa-copy",
                  "label": "コピー",
                  "_disabled": function(data){
                     return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                  },
                  "action": function(data){
                     var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                     if (inst.is_selected(obj)){
                        inst.copy(inst.get_top_selected());
                     }else{
                        inst.copy(obj);
                     }
                  }
               },
               "paste":{
                  "separator_before": false,
                  "separator_after": false,
                  "icon": "contextmenu-icon fa fa-paste",
                  "label": "貼り付け",
                  "_disabled": function(data){
                     return !$.jstree.reference(data.reference).can_paste();
                  },
                  "action": function(data){
                     var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                     inst.paste(obj);
                     console.log( obj );

                  }
               }
            };
         }
      }
   }).on('loaded.jstree', function(){
      $(this).jstree('open_all');
   });
});

編集可能 jsTree の編集結果を Pyhon で、JSON 保存

「jsTree 描画→Treeの編集操作→ 編集結果を次回表示の為に保存」
(ここでいうTreeの編集操作は、ツリーアイテムを移動したり新規作成・削除・名称変更をツリー図上で実行することです)
通常は、ブラウザで表示した jsTree → JavaScriptJSON 変換、→ サーバへ送信 → サーバ側の処理で、
jsTree 用の JSON に変換して次回の表示のデータにする。
ということをするのが一般的だと思います。
しかし、この為にサーバ側で処理を書くのがめんどくさい時など、
手元の開発用に、jsTree 描画→Treeの編集操作→ 編集結果を次回表示の為に保存、ということをするために、
Python の助けを借ります。
Python eel で、chrome 起動 → jsTree の HTML を表示、→ ツリーの編集操作
JavaScript で、ツリー図データを JSON にして、Python に渡す、
PythonJSONを解析して、jsTree がツリー表示できる形式の JSONに変換する
Python でローカルPCに保存する。→ 次回起動再読み込む JSON として保存する

f:id:posturan:20181117145940j:plain

HTMLソース
JavaScript  src="/eel.js" を指定するのを忘れないようにします。
↓ font-awesome は、右クリックコンテキストメニューのアイコンで利用します。ここでは CDNサイトを指定してます。
↓ BootStrap も CDNサイト利用です。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>develop.html</title>
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.5.0/css/all.min.css" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"/>
<link href="jstree/themes/default/style.min.css" rel="stylesheet" type="text/css"/>
<link href="css/develop.css" rel="stylesheet" type="text/css"/>
<script src="js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<script src="jstree/jstree-3.3.6.min.js" type="text/javascript"></script>
<script src="js/develop-tree.js" type="text/javascript"></script>
<script type="text/javascript" src="/eel.js"></script>
<script src="js/develop.js" type="text/javascript"></script>
</head>
<body>
<div class="container-fluid">
<div class="d-flex justify-content-center edit-action">
   <button id="create" type="button" class="btn btn-outline-primary">SAVE</button>
</div>
<div>
   <ul class="edit-container">
      <li class="edit-border">
         <div class="tree-container">
            <div id="tree" class="tree-div"></div>
         </div>
      </li>
   </ul>
</div>
</div>
</body>
</html>

CSS ソース

@charset "UTF-8";
/**
 * develop.css
 */
ul{ margin: 0; padding: 0; }
li{ list-style-type: none; }

ul.edit-container::after{ content: ""; display: block; clear: both; }
li.edit-border{
   padding: 2px 10px;
   float: left;
}
li.edit-border:nth-child(1){
   width: 100%;
   height: 400px;
   overflow: auto;
}
li.edit-border:nth-child(2){
   width: 60%;
}
.edit-action{ margin: 20px 0px; }
/* ツリー図エリア背景色 */
.tree-div{
   background-color: #ffffff;
}
.edit-border{
   border: 1px solid #a0c0b0;
}
.tree-container{
   max-height: 360px;
   overflow-y: auto;
}
/*------------- for jsTee ---------------*/
/* jsTreeアイコン画像無し */
.jstree-default-large .jstree-notIcon {
   display: none;
}
.contextmenu-icon{
   color: #0085c9;
}

jsTree を描画する JS ソース

$(function(){
    $.jstree.defaults.core.themes.variant = "large";
    $.jstree.defaults.core.themes.responsive = true;

    $('#tree').jstree({
        'plugins': [ 'contextmenu','dnd' ],
        'core':{
            'data':{
                "url":"./tree.json",
                "dataType":"json"
            },
            "check_callback" : function(operation, node, node_parent, node_position, more){
                    if (operation=="move_node"){
                        if (node_parent.icon != "jstree-folder" && node_parent.id != "#") return false;
                    }
            },
        },
        "contextmenu":{
            "items":function($node){
                return {
                    "createFolder":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-folder-plus",
                        "label": "新規フォルダ作成",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).icon != "jstree-folder";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.create_node(obj, { text:'New Folder', 'icon':'jstree-folder' }, "last", function(new_node){
                                try{
                                    inst.edit(new_node);
                                }catch(ex){
                                    setTimeout(function(){ inst.edit(new_node); },0);
                                }
                            });
                        }
                    },
                    "createFile":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon far fa-file",
                        "label": "新規ファイル作成",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).icon != "jstree-folder";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.create_node(obj, { text:'New File', 'icon':'jstree-file' }, "last", function(new_node){
                                try{
                                    inst.edit(new_node);
                                }catch(ex){
                                    setTimeout(function(){ inst.edit(new_node); },0);
                                }
                            });
                        }
                    },
                    "rename":{
                        "separator_before": true,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-edit",
                        "label": "名称の変更",
                        "_disabled": false,
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.edit(obj);
                        }
                    },
                    "remove":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-trash-alt",
                        "label": "削除",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            if (inst.is_selected(obj)){
                                inst.delete_node(inst.get_selected());
                            }else{
                                inst.delete_node(obj);
                            }
                        }
                    },
                    "cut":{
                        "separator_before": true,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-cut",
                        "label": "切り取り",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            if (inst.is_selected(obj)){
                                inst.cut(inst.get_top_selected());
                            }else{
                                inst.cut(obj);
                            }
                        }
                    },
                    "copy":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-copy",
                        "label": "コピー",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            if (inst.is_selected(obj)){
                                inst.copy(inst.get_top_selected());
                            }else{
                                inst.copy(obj);
                            }
                        }
                    },
                    "paste":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-paste",
                        "label": "貼り付け",
                        "_disabled": function(data){
                            return !$.jstree.reference(data.reference).can_paste();
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.paste(obj);
                        }
                    }
                };
            }
        }
    }).on('loaded.jstree', function(){
        $(this).jstree('open_all');
    });
});

ツリー表示の JSON データです。 tree.json の内容

[
  {
    "id": "1",
    "icon": "jstree-folder",
    "text": "ルート",
    "children": [
      {
        "id": "11",
        "icon": "jstree-file",
        "text": "A",
        "children": []
      }
    ]
  }
]

注目のPython コードです。

# -*- coding: utf-8 -*-
import eel
import json
import codecs

@eel.expose
def writeFile(str):
    # JavaScript から受け取ったテキスト→JSON parse → jsTree用 JSON作成
    trees = []
    for node in json.loads(str):
        if node['parent'] == '#':
            trees.append(makeNode(node))
        else:
            setLeaf(node, trees)
    print(json.dumps(trees, ensure_ascii=False, indent=2))
    # ファイル保存
    with codecs.open('WebContent/tree.json', 'w+', 'utf-8') as fp:
        json.dump(trees, fp, ensure_ascii=False, indent=2)

def makeNode(node):
    return dict(id=node['id'], icon=node['icon'], text=node['text'], children=[])

def setLeaf(node, trees):
    for n in trees:
        if (n['id']==node['parent']):
            n['children'].append(makeNode(node))
            break
        else:
            setLeaf(node, n['children'])

if __name__ == '__main__':
    web_app_options = {
        'mode': "chrome-app",
        'port': 8000,
        'chromeFlags': ["--browser-startup-dialog"]
    }
    eel.init("WebContent")
    eel.start("develop.html", size=(800, 600), options=web_app_options)

「SAVE」ボタンをクリックした時に Python eel でエクスポーズしたメソッドを呼び出す
JavaScript です。「eel. + (Python で @expose したメソッド名)」で記述します。

/**
 * develop.js
 */
$(function(){
    $('#create').click(function(){
        var v = $('#tree').jstree(true).get_json('#', {flat:true});
        eel.writeFile(JSON.stringify(v))
    });
});

これらのコードの配置は、WebContent フォルダの下に HTML と tree.json
そして、Javascriptcss ファイルを直下または相対で配置します。
Python コードの eel initメソッドが、"WebContent" としているので、Python コードは、
WebContent フォルダと同じ階層に置きます。

Python 3.x からの JSON ファイル読込みと書込み

Python の嫌なところは、2系と3系のバージョンの差が思わぬところにあるところだ。
2系は無視して、3系、

codecs を使えば文字コード問題に悩まなくて済む

読込み

import json
import codecs

with codecs.open("test.json", 'r', 'utf-8') as f:
    data = json.load(f)
    for key, value in data.items():
        print("%s : %s" % (key, value) )

書込み

import json
import codecs

with codecs.open('test.json', 'w+', 'utf-8') as fp:
    json.dump(dict, fp, ensure_ascii=False, indent=2)

Python eelで jsTree の AJAX load を確かめる

jsTree の AJAX による URL指定の描画は、わざわざ Webサイトを立てないと
確認できません。
f:id:posturan:20181115121149j:plain
↑のGIF画像になってしまいます。
サーバを立てなくても描画させるのに、
Python eel パッケージを使って、手元のWindows PC上で、HTML と JS + Pythonスクリプトで確かめます。
WebContent というフォルダに、HTML (develop.html) と読込ませるJSONファイル(tree.json)と jsTree描画に必要なJSやCSS
中に配置します。
develop.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>develop.html</title>
<link href="jstree/themes/default/style.min.css" rel="stylesheet" type="text/css"/>
<link href="css/develop.css" rel="stylesheet" type="text/css"/>
<script src="js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="jstree/jstree-3.3.6.min.js" type="text/javascript"></script>
<script src="js/develop.js" type="text/javascript"></script>
</head>
<body>
<div class="container-fluid">
   <div class="develop">
      <ul>
         <li>
            <div class="tree-container">
               <div id="tree" class="tree-div"></div>
            </div>
         </li>
         <li></li>
      </ul>
   </div>
</div>
</body>
</html>

develop.js の コード

$(function(){
   $.jstree.defaults.core.themes.variant = "large";
   $.jstree.defaults.core.themes.responsive = true;
   $('#tree').jstree({
      'core':{
         'data':{
            "url":"./tree.json",
            "dataType":"json"
         }
      }
   }).on('loaded.jstree', function(){
      $(this).jstree('open_all');
   });
});

読込ませる JSON(tree.json

[
   { "id":1, "text":"フォルダ",
      "children":[
         { "id":11, "icon":"jstree-file","text":"ノート" }
      ]
   }
]

WebContent と同じ階層に、Python スクリプト(develop.py )を置きます。
f:id:posturan:20181115121622j:plain
develop.py

# -*- coding: utf-8 -*-
import eel

web_app_options = {
   'mode': "chrome-app", #or "chrome"
   'port': 8080,
   'chromeFlags': ["--start-fullscreen", "--browser-startup-dialog"]
}

eel.init("WebContent")
eel.start("develop.html", options=web_app_options)

8000 ポートでなくて、8080 ポートで起動します。Windows PCで手元でよく利用するからです。
CSSの方は、

@charset "UTF-8";
/**
 * develop.css
 */
.develop ul{ margin: 0; padding: 0; }
.develop ul::after{ content: ""; display: block; clear: both; }
.develop li{
   list-style-type: none;
   padding: 2px 10px;
   float: left;
}
.develop li:nth-child(1){
   width: 50%;
}
.tree-container{
   overflow-y: auto;
}
/* ツリー図エリア背景色 */
.tree-div{
   background-color: #ffffff;
}

です。
予め、eel は pip install でインストールしておきます
develop.py をクリックして起動すると、
f:id:posturan:20181115122631j:plain

無事、サーバ立てなくても jsTree を url 指定で描画できます。
WebContent の直下にtree.json を置くから、URL指定  "./tree.json" になります。

メソッド参照の否定形

先日、Java11 の String の strip isBlank が便利で、Wicket を使う時に
楽になることを書きました。
oboe2uran.hatenablog.com

でもこのラムダの記述をメソッド参照で書けないかと再興しました。

元ソース

final TextField<String> itemField = new TextField<>("item", new Model<>());
String s = Optional.ofNullable(itemField.getModelObject())
      .map(e->e.strip())
      .filter(e->!e.isBlank())
      .orElseThrow(()->new Exception("input must requre !"));

まず、strip の部分、、、

String s = Optional.ofNullable(itemField.getModelObject())
      .map(String::strip)
      .filter(e->!e.isBlank())
      .orElseThrow(()->new Exception("input must requre !"));

isBlank をメソッド参照で書いた時の否定の書き方で、

      .filter( !String::sBlank )

という書き方はできません。
Predicate の not() スタティックメソッドで被せるしか方法がないようです。

String s = Optional.ofNullable(itemField.getModelObject())
      .map(String::strip)
      .filter(Predicate.not(String::isBlank))
      .orElseThrow(()->new Exception("input must requre !"));

なぜそこまで、メソッド参照にすることに拘るかというと、ラムダ式の中でこれを書くときにラムダ変数の記述が
増えるのを避けたいです。

実際、新しいWicket を使っていると

queue(new Button("submit").add(AjaxFormSubmitBehavior.onSubmit("click", t->{
     // submit 時の処理
})));

を良く書くわけで、このラムダの中にまたラムダ文を書いてラムダ変数が増えてるのがカッコ悪いかもしれないのです。

Python で、Excel を読み込むメモ xlrd 使用

PythonExcel を読み込む xlrd の解説はネットでたくさん見つかるのだが、やはり書いてみないと習得できない。

Excel ファイルのサンプル、 sample.xlsx が以下の様になっているとする。

Name value 開始日
あいう 242 2018/11/06
ABCD 108 2018/11/07
XYZ 47 2018/11/08

まずは、これを読み込むスタンダードな簡単なスクリプト

# -*- coding: utf-8 -*-
import xlrd

book = xlrd.open_workbook('sample.xlsx')
sheet = book.sheet_by_index(0)
for row in range(1, sheet.nrows):
    print("%s %d %s" % (sheet.cell_value(row, 0), sheet.cell_value(row, 1), sheet.cell_value(row, 2) ) )

1行目はヘッダだから読み飛ばしてる。
cell_value(row, col) は、cell(row, col).value でも同じ。
上の結果は、

あいう 242 43410.0
ABCD 108 43411.0
XYZ 47 43412.0

と、日付が小数点になってしまう。
セルのタイプをチェックする。
sheet.cell_type(row, col) もしくは、sheet.cell(row, col).ctype で取得できる値は、
API Reference — xlrd 1.1.0 documentation を参照すると、

0 空セル
1 テキスト文字列(Unicode string)
2 数値(float)
3 日付(float)

これに従いセルをチェックすると日付のセルは、 3=日付(float) である。
日付をなんとか、yyyy/MM/dd の書式にしたいので、
https://qiita.com/nezumi/items/23c301c661f5e9653f19
を参考にさせてもらい、、

# -*- coding: utf-8 -*-
import xlrd

def excel_date(num):
    from datetime import datetime, timedelta
    return(datetime(1899, 12, 30) + timedelta(days=num)).strftime("%Y/%m/%d")

book = xlrd.open_workbook('sample.xlsx')
sheet = book.sheet_by_index(0)
for row in range(1, sheet.nrows):
    str = "'%s',  %d, TO_DATE('%s','YYYY/MM/DD')" % (
        sheet.cell(row, 0).value,
        sheet.cell(row, 1).value,
        excel_date(sheet.cell(row, 2).value)
        )
    print(str)

結果、

'あいう',  242, TO_DATE('2018/11/06','YYYY/MM/DD')
'ABCD',  108, TO_DATE('2018/11/07','YYYY/MM/DD')
'XYZ',  47, TO_DATE('2018/11/08','YYYY/MM/DD')

と、まあ、とりあえず、めでたし、めでたし。