簡介:
無限極分類是一種比較常見的數據格式,生成組織結構,生成商品分類信息,權限管理當中的細節權限設置,都離不開無限極分類的管理。
常見的有鏈表式,即有一個Pid指向上級的ID,以此來設置結構。寫的時候簡單,用的時候效果一班,比如說,同一級沒有辦法手動重新排序,查詢所有子孫的時候不方便。
所以有了預排序樹,即左右值樹形管理。
優點還是挺多的。
可以快速確定關系,最短路徑,同級排序,查找所有子孫(最好的地方)
一:主要包
-
sqlalchemy_mptt
- https://github.com/uralbash/sqlalchemy_mptt
- pip install sqlalchemy_mptt
-
sqlalchemy-orm-tree
- https://github.com/monetizeio/sqlalchemy-orm-tree/
- pip install sqlalchemy-orm-tree
- 最后更新2015年8月27日,還沒有完整的示例代碼,僅有的幾行,還………………
- 放棄
-
ztree
- http://www.treejs.cn/
- 這個是前台JS顯示用的,希望我能整合到flask-admin中去。
二:sqlalchemy_mptt
1.快速指南
源碼:
# !/usr/bin/python3 # -*- coding: utf-8 -*- # @Time : 2018-09-12 16:32 # @Author : Jackadam # @Email :jackadam@sina.com # @File : mptt.py # @Software: PyCharm from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy_mptt import mptt_sessionmaker from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base from sqlalchemy_mptt.mixins import BaseNestedSets Base = declarative_base() class Tree(Base, BaseNestedSets): __tablename__ = "tree" id = Column(Integer, primary_key=True) name = Column(String(8)) def __repr__(self): return "<Node (%s)>" % self.id engine = create_engine('sqlite:///mptt.db', echo=False) mptt_ession = mptt_sessionmaker(sessionmaker(bind=engine)) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) def print_tree(group_name, tab=1): """ :param str group_name:要查找的樹的根的名稱 :param int tab: 格式化用的-數量 """ group = db_session.query(Tree).filter_by(name=group_name).one_or_none() # type: TreeGroup if not group: return # group found - print name and find children print('- ' * tab + group.name) for child_group in group.children: # type: TreeGroup # new tabulation value for child record print_tree(child_group.name, tab * 2) if __name__ == '__main__': # Base.metadata.create_all(bind=engine) # nodes=[] # node=Tree(name='中國') # nodes.append(node) # db_session.add_all(nodes) # db_session.commit() # nodes = [] # ref_id=db_session.query(Tree.id).filter_by(name='中國').first()[0] # print(ref_id) # new_name=['河南','河北','山東','山西','陝西'] # for i in new_name: # print(i) # node=Tree(name=i,parent_id=ref_id) # nodes.append(node) # db_session.add_all(nodes) # db_session.commit() # nodes = [] # ref_id = db_session.query(Tree.id).filter_by(name='河南').first()[0] # print(ref_id) # new_name = ['鄭州', '洛陽', '開封', '新鄉', '新鄭'] # for i in new_name: # print(i) # node = Tree(name=i, parent_id=ref_id) # nodes.append(node) # db_session.add_all(nodes) # db_session.commit() print_tree('中國') print_tree('河南')
2.結果樣式:
print_tree('中國')
- 中國
- - 河南
- - - - 鄭州
- - - - 洛陽
- - - - 開封
- - - - 新鄉
- - - - 新鄭
- - 河北
- - 山東
- - 山西
- - 陝西
print_tree('河南')
- 河南
- - 鄭州
- - 洛陽
- - 開封
- - 新鄉
- - 新鄭
3.優勢
我加入的順序可不是這個順序,但是數據結構,和sqlalchemy_mptt幫我們處理了層級數據結構,就顯示為樹形結構了。
再試試下面的代碼,可以列出所有的數。
def print_all_tree(tab=1): """ :param int tab: 格式化用的-數量 """ group = db_session.query(Tree).filter_by(parent_id=None).all() # type: TreeGroup print(group) if not group: return # group found - print name and find children for i in group: print('- ' * tab + i.name) for child_group in i.children: # type: TreeGroup # new tabulation value for child record print_tree(child_group.name, tab * 2) # nodes=[] # new_name = ['美國', '英國', '法國', '英國', '德國'] # for i in new_name: # print(i) # node = Tree(name=i) # nodes.append(node) # db_session.add_all(nodes) # db_session.commit() # ref_id = db_session.query(Tree.id).filter_by(name='美國').first()[0] # nodes = [] # new_name = ['亞拉巴馬', '阿拉斯加', '亞利桑那', '阿肯色', '加利福尼亞'] # for i in new_name: # print(i) # node = Tree(name=i,parent_id=ref_id) # nodes.append(node) # db_session.add_all(nodes) # db_session.commit() print_all_tree(1)
三:數據結構分析
數據庫結構
數據庫里面的內容為
映射定義結構
分析:
我們只定義了主鍵id,name名字。但是數據庫里面多了幾列:lft,rgt,level,tree_id,parent_id
這些結構就是左右值樹用的東西,參照最上面的圖,每個名字都有左值和右值,可以很方便的查到結構。
比如說:
找到所有的子孫:查找Food的子孫,Food作為參考點,左值為1,右值為18,所有的子孫,就是數據庫中左值大於1,小於18的。
查找Fruit的子孫,Fruit作為參考點,左值為2,右值為11,所有的子孫,就是數據庫中左值大於2,小於11的。
找到所有的子節點(不包括孫節點):查找Food的子節點,Food作為參考點,level=1,tree_id=1。
那么所有的子節點為 tree_id=1,level=1+1 層級為2的。
查找最短路徑:一般用在導航中,也有用在組合顯示上,因為需要知道上一級,上N級的路徑結構:
查找Banana的上級路徑,Banana作為參考點,左值為8,右值為9,那么路徑就是數據庫中左值小於8,並且右值大於9的。排序按左值或右值的升序降序,就隨便你了。
結果是 Food--Fruit--Yellow--Banana
四:算法
1.簡介:
關於預排序樹的算法,有很多,我也寫不好,所以我用了sqlalchemy_mptt,還是大概介紹一下吧。
2.增加
按sqlalchemy_mptt的用法,
如果沒有parent_id,那么就創建為一個新樹的根節點,parent_id是空的,level是1,tree_id根據數據庫的情況順序向上加。
如果有提供parent_id,那么久創建為parent的子節點,parent_id是提供的,level是parent的level+1,tree_id和parent一致。
同時要更新受影響的其他節點。
左值處理一遍。
大於parent_id右值的所有左值,+2
右值處理一遍。
大於等於parent_id右值的所有右值,+2
3.刪除
和增加差不多,刪除一個節點以后,也要更新受影響的左值和右值。
4.移動
這個其實就是刪除一個老節點,再創建一個新節點。
5.完全不用擔心
因為這些功能,sqlalchem_mptt全都實現了,根本不需要你操心。具體怎么用,我還在學習中。
下面的東西,隨着我的學習,逐步更新。
五:ztree
1.ztree基礎顯示(帶編輯功能)
下面的代碼,保存為html,直接就可以了。

<!DOCTYPE html> <HTML> <HEAD> <TITLE> ZTREE DEMO - beforeEditName / beforeRemove / onRemove / beforeRename / onRename</TITLE> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="../../../css/demo.css" type="text/css"> <link rel="stylesheet" href="../../../css/zTreeStyle/zTreeStyle.css" type="text/css"> <script type="text/javascript" src="../../../js/jquery-1.4.4.min.js"></script> <script type="text/javascript" src="../../../js/jquery.ztree.core.js"></script> <script type="text/javascript" src="../../../js/jquery.ztree.excheck.js"></script> <script type="text/javascript" src="../../../js/jquery.ztree.exedit.js"></script> <SCRIPT type="text/javascript"> <!-- var setting = { view: { addHoverDom: addHoverDom, removeHoverDom: removeHoverDom, selectedMulti: false }, edit: { enable: true, editNameSelectAll: true, showRemoveBtn: showRemoveBtn, showRenameBtn: showRenameBtn }, data: { simpleData: { enable: true } }, callback: { beforeDrag: beforeDrag, beforeEditName: beforeEditName, beforeRemove: beforeRemove, beforeRename: beforeRename, onRemove: onRemove, onRename: onRename } }; var zNodes = [ {id: 1, pId: 0, name: "父節點 1", open: true}, {id: 11, pId: 1, name: "葉子節點 1-1"}, {id: 12, pId: 1, name: "葉子節點 1-2"}, {id: 13, pId: 1, name: "葉子節點 1-3"}, {id: 2, pId: 0, name: "父節點 2", open: true}, {id: 21, pId: 2, name: "葉子節點 2-1"}, {id: 22, pId: 2, name: "葉子節點 2-2"}, {id: 23, pId: 2, name: "葉子節點 2-3"}, {id: 3, pId: 0, name: "父節點 3", open: true}, {id: 31, pId: 3, name: "葉子節點 3-1"}, {id: 32, pId: 3, name: "葉子節點 3-2"}, {id: 33, pId: 3, name: "葉子節點 3-3"} ]; var log, className = "dark"; function beforeDrag(treeId, treeNodes) { return false; } function beforeEditName(treeId, treeNode) { className = (className === "dark" ? "" : "dark"); showLog("[ " + getTime() + " beforeEditName ] " + treeNode.name); var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.selectNode(treeNode); setTimeout(function () { if (confirm("進入節點 -- " + treeNode.name + " 的編輯狀態嗎?")) { setTimeout(function () { zTree.editName(treeNode); }, 0); } }, 0); return false; } function beforeRemove(treeId, treeNode) { className = (className === "dark" ? "" : "dark"); showLog("[ " + getTime() + " beforeRemove ] " + treeNode.name); var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.selectNode(treeNode); return confirm("確認刪除 節點 -- " + treeNode.name + " 嗎?"); } function onRemove(e, treeId, treeNode) { showLog("[ " + getTime() + " onRemove ] " + treeNode.name); } function beforeRename(treeId, treeNode, newName, isCancel) { className = (className === "dark" ? "" : "dark"); showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ] " + treeNode.name + (isCancel ? "</span>" : "")); if (newName.length == 0) { setTimeout(function () { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.cancelEditName(); alert("節點名稱不能為空."); }, 0); return false; } return true; } function onRename(e, treeId, treeNode, isCancel) { showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ] " + treeNode.name + (isCancel ? "</span>" : "")); } function showRemoveBtn(treeId, treeNode) { return !treeNode.isFirstNode; } function showRenameBtn(treeId, treeNode) { return !treeNode.isLastNode; } function showLog(str) { if (!log) log = $("#log"); log.append("<li class='" + className + "'>" + str + "</li>"); if (log.children("li").length > 8) { log.get(0).removeChild(log.children("li")[0]); } } function getTime() { var now = new Date(), h = now.getHours(), m = now.getMinutes(), s = now.getSeconds(), ms = now.getMilliseconds(); return (h + ":" + m + ":" + s + " " + ms); } var newCount = 1; function addHoverDom(treeId, treeNode) { var sObj = $("#" + treeNode.tId + "_span"); if (treeNode.editNameFlag || $("#addBtn_" + treeNode.tId).length > 0) return; var addStr = "<span class='button add' id='addBtn_" + treeNode.tId + "' title='add node' onfocus='this.blur();'></span>"; sObj.after(addStr); var btn = $("#addBtn_" + treeNode.tId); if (btn) btn.bind("click", function () { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.addNodes(treeNode, {id: (100 + newCount), pId: treeNode.id, name: "new node" + (newCount++)}); return false; }); }; function removeHoverDom(treeId, treeNode) { $("#addBtn_" + treeNode.tId).unbind().remove(); }; function selectAll() { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked"); } $(document).ready(function () { $.fn.zTree.init($("#treeDemo"), setting, zNodes); $("#selectAll").bind("click", selectAll); }); //--> </SCRIPT> <style type="text/css"> .ztree li span.button.add { margin-left: 2px; margin-right: -1px; background-position: -144px 0; vertical-align: top; *vertical-align: middle } </style> </HEAD> <BODY> <ul id="treeDemo" class="ztree"></ul> </BODY> </HTML>
其中head引入了jquery,ztree的一些JS文件。
然后var setting,設置了ztree的一些參數
var zNodes,設置了一個簡單結構的數結構數據。
一些function,定義了鼠標移上去,移出去,點擊……的一些事件。
<style type="text/css">
.ztree li span.button.add {
這個樣式,定義了添加按鈕的圖標。
在body中
直接 <ul id="treeDemo" class="ztree"></ul>就可以顯示了
2.在flask中顯示
把整頁作為模板,動態傳入zNodes,就可以直接顯示了。
3.在flask-admin中顯示
因為我這個懶蛋,准備把flask-admin當前台用。數據展示也挺方便。
先寫一個頁面,單獨只顯示ztree
視圖函數:
用extra_css引入自定義css(ztree的)
用extra_js引入自定義js(ztree的)
用@expose('/')定義路由URL(flask-admin的)
tree_info,是讀取數據庫拿到的樹的基本信息。

class User_Groups_ModelView(ModelView): extra_css=[ "../static/zTree_v3/css/demo.css", "../static/zTree_v3/css/zTreeStyle/zTreeStyle.css" ] extra_js = [ "../static/zTree_v3/js/jquery.ztree.all.js", "../static/zTree_v3/js/jquery.ztree.core.js", "../static/zTree_v3/js/jquery.ztree.excheck.js", "../static/zTree_v3/js/jquery.ztree.exedit.js", "../static/zTree_v3/js/jquery.ztree.exhide.js", # "../static/zTree_v3/js/jquery-1.4.4.min.js", ] page_size = 20 # the number of entries to display on the list view can_create = False # can_edit = True # can_delete = False # can_view_details = True @expose('/') def index(self): tree_info=get_simple_json() return self.render('users/list.html',tree_info=tree_info)
get_simple_json就是按ztree的簡單結構,生成樹的基本數據。帶到模板去渲染

def get_simple_json(): groups = db_session.query(User_Groups).all() # print(len(groups)) result = [] for i in groups: if i.parent_id == None: i.parent_id = 0 _dict = { 'id': i.id, 'pId': i.parent_id, 'name': i.groups_name, } result.append(_dict) # print(result) return result
模板文件
前面引入了flask-admin的基本模板
block body(顯示主體)
block model_menu_bar(菜單導航,就是新建--刪除--導出那一行)
<script></script>中間,寫了ztree的一些方法,用來動態的顯示出添加,編輯,刪除的小按鈕。
<ul id="treeDemo" class="ztree"></ul>
這個就是顯示了。
block tail
ztree的渲染,我把它寫在了尾部

{% extends 'admin/master.html' %} {% import 'admin/lib.html' as lib with context %} {% import 'admin/actions.html' as actionslib with context %} {% block body %} {% block model_menu_bar %} <ul class="nav nav-tabs actions-nav"> <li class="active"> <a href="javascript:void(0)">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a> </li> {% if admin_view.can_create %} <li> {%- if admin_view.create_modal -%} {{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), title=_gettext('Create New Record'), content=_gettext('Create')) }} {% else %} <a href="{{ get_url('.create_view', url=return_url) }}" title="{{ _gettext('Create New Record') }}">{{ _gettext('Create') }}</a> {%- endif -%} </li> {% endif %} {% if admin_view.can_export %} {{ model_layout.export_options() }} {% endif %} {% block model_menu_bar_before_filters %}{% endblock %} {% if filters %} <li class="dropdown"> {{ model_layout.filter_options() }} </li> {% endif %} {% if can_set_page_size %} <li class="dropdown"> {{ model_layout.page_size_form(page_size_url) }} </li> {% endif %} {% if actions %} <li class="dropdown"> {{ actionlib.dropdown(actions) }} </li> {% endif %} {% if search_supported %} <li> {{ model_layout.search_form() }} </li> {% endif %} {% block model_menu_bar_after_filters %}{% endblock %} </ul> {% endblock %} <SCRIPT type="text/javascript"> <!-- var setting = { view: { addHoverDom: addHoverDom, removeHoverDom: removeHoverDom, selectedMulti: false }, edit: { enable: true, editNameSelectAll: true, showRemoveBtn: showRemoveBtn, showRenameBtn: showRenameBtn }, data: { simpleData: { enable: true } }, callback: { beforeDrag: beforeDrag, beforeEditName: beforeEditName, beforeRemove: beforeRemove, beforeRename: beforeRename, onRemove: onRemove, onRename: onRename } }; var zNodes = {{ tree_info|safe }}; var log, className = "dark"; function beforeDrag(treeId, treeNodes) { alert('這里是增加?') return false; } function beforeEditName(treeId, treeNode) { className = (className === "dark" ? "" : "dark"); showLog("[ " + getTime() + " beforeEditName ] " + treeNode.name); var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.selectNode(treeNode); setTimeout(function () { if (confirm("進入節點 -- " + treeNode.name + " 的編輯狀態嗎?")) { setTimeout(function () { zTree.editName(treeNode); }, 0); } }, 0); return false; } function beforeRemove(treeId, treeNode) { className = (className === "dark" ? "" : "dark"); showLog("[ " + getTime() + " beforeRemove ] " + treeNode.name); var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.selectNode(treeNode); return confirm("確認刪除 節點 -- " + treeNode.name + " 嗎?"); } function onRemove(e, treeId, treeNode) { showLog("[ " + getTime() + " onRemove ] " + treeNode.name); } function beforeRename(treeId, treeNode, newName, isCancel) { className = (className === "dark" ? "" : "dark"); showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ] " + treeNode.name + (isCancel ? "</span>" : "")); if (newName.length == 0) { setTimeout(function () { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.cancelEditName(); alert("節點名稱不能為空."); }, 0); return false; } return true; } function onRename(e, treeId, treeNode, isCancel) { showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ] " + treeNode.name + (isCancel ? "</span>" : "")); } function showRemoveBtn(treeId, treeNode) { return !treeNode.isFirstNode; } function showRenameBtn(treeId, treeNode) { return !treeNode.isLastNode; } function showLog(str) { if (!log) log = $("#log"); log.append("<li class='" + className + "'>" + str + "</li>"); if (log.children("li").length > 8) { log.get(0).removeChild(log.children("li")[0]); } } function getTime() { var now = new Date(), h = now.getHours(), m = now.getMinutes(), s = now.getSeconds(), ms = now.getMilliseconds(); return (h + ":" + m + ":" + s + " " + ms); } var newCount = 1; function addHoverDom(treeId, treeNode) { var sObj = $("#" + treeNode.tId + "_span"); if (treeNode.editNameFlag || $("#addBtn_"+treeNode.tId).length>0) return; var addStr = "<span class='button add' id='addBtn_" + treeNode.tId + "' title='增加節點' onfocus='this.blur();'></span>"; sObj.after(addStr); var btn = $("#addBtn_"+treeNode.tId); if (btn) btn.bind("click", function(){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.addNodes(treeNode, {id:(100 + newCount), pId:treeNode.id, name:"新節點" + (newCount++)}); return false; }); }; function removeHoverDom(treeId, treeNode) { $("#addBtn_"+treeNode.tId).unbind().remove(); }; function selectAll() { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked"); } //--> </SCRIPT> <ul id="treeDemo" class="ztree"></ul> {% endblock %} {% block tail %} <SCRIPT> $(document).ready(function () { $.fn.zTree.init($("#treeDemo"), setting, zNodes); $("#selectAll").bind("click", selectAll); }); </SCRIPT> {% endblock %}
有一個小問題
ztree的編輯功能當中沒有默認增加。所以增加這個功能寫在了,默認樣式表中也沒有這個樣式。
基礎示例當中有定義這個CSS。
.ztree li span.button.add { margin-left: 2px; margin-right: -1px; background-position: -144px 0; vertical-align: top; *vertical-align: middle }
我就隨便寫進了demo.css,這樣顯示的就正常了。
4.定制flask-admin的顯示
待編輯功能寫完,再來研究這個問題。
六:ztree的編輯功能
1.簡介
默認的flask-admin功能是不能管理這個樹形結構的。它管理的是簡單結構,就是 id pid name 這種格式。
或許可以嘗試由簡單格式,自動創建為左右值樹格式
2.
3.
七:
八:
九: