You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

254 lines
8.0 KiB

  1. # Copyright (c) 2012 Web Notes Technologies Pvt Ltd (http://erpnext.com)
  2. #
  3. # MIT License (MIT)
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the "Software"),
  7. # to deal in the Software without restriction, including without limitation
  8. # the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. # and/or sell copies of the Software, and to permit persons to whom the
  10. # Software is furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  16. # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  17. # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  18. # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  19. # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
  20. # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21. #
  22. # Tree (Hierarchical) Nested Set Model (nsm)
  23. #
  24. # To use the nested set model,
  25. # use the following pattern
  26. # 1. name your parent field as "parent_node" if not have a property nsm_parent_field as your field name in the document class
  27. # 2. have a field called "old_parent" in your fields list - this identifies whether the parent has been changed
  28. # 3. call update_nsm(doc_obj) in the on_upate method
  29. # ------------------------------------------
  30. import webnotes, unittest
  31. class TestNSM(unittest.TestCase):
  32. def setUp(self):
  33. from webnotes.model.doc import Document
  34. self.root = Document(fielddata={'doctype':'nsmtest', 'name':'T001', 'parent':None})
  35. self.first_child = Document(fielddata={'doctype':'nsmtest', 'name':'C001', 'parent_node':'T001'})
  36. self.first_sibling = Document(fielddata={'doctype':'nsmtest', 'name':'C002', 'parent_node':'T001'})
  37. self.grand_child = Document(fielddata={'doctype':'nsmtest', 'name':'GC001', 'parent_node':'C001'})
  38. webnotes.conn.sql("""
  39. create table `tabnsmtest` (
  40. name varchar(120) not null primary key,
  41. creation datetime,
  42. modified datetime,
  43. modified_by varchar(40),
  44. owner varchar(40),
  45. docstatus int(1) default '0',
  46. parent varchar(120),
  47. parentfield varchar(120),
  48. parenttype varchar(120),
  49. idx int(8),
  50. parent_node varchar(180),
  51. old_parent varchar(180),
  52. lft int,
  53. rgt int) ENGINE=InnoDB""")
  54. def test_root(self):
  55. self.root.save(1)
  56. update_nsm(self.root)
  57. self.assertTrue(self.root.lft==1)
  58. self.assertTrue(self.root.rgt==2)
  59. def test_first_child(self):
  60. self.root.save(1)
  61. update_nsm(self.root)
  62. self.first_child.save(1)
  63. update_nsm(self.first_child)
  64. self.root._loadfromdb()
  65. self.assertTrue(self.root.lft==1)
  66. self.assertTrue(self.first_child.lft==2)
  67. self.assertTrue(self.first_child.rgt==3)
  68. self.assertTrue(self.root.rgt==4)
  69. def test_sibling(self):
  70. self.test_first_child()
  71. self.first_sibling.save(1)
  72. update_nsm(self.first_sibling)
  73. self.root._loadfromdb()
  74. self.first_child._loadfromdb()
  75. self.assertTrue(self.root.lft==1)
  76. self.assertTrue(self.first_child.lft==2)
  77. self.assertTrue(self.first_child.rgt==3)
  78. self.assertTrue(self.first_sibling.lft==4)
  79. self.assertTrue(self.first_sibling.rgt==5)
  80. self.assertTrue(self.root.rgt==6)
  81. def test_remove_sibling(self):
  82. self.test_sibling()
  83. self.first_sibling.parent_node = ''
  84. update_nsm(self.first_sibling)
  85. self.root._loadfromdb()
  86. self.first_child._loadfromdb()
  87. self.assertTrue(self.root.lft==1)
  88. self.assertTrue(self.first_child.lft==2)
  89. self.assertTrue(self.first_child.rgt==3)
  90. self.assertTrue(self.root.rgt==4)
  91. self.assertTrue(self.first_sibling.lft==5)
  92. self.assertTrue(self.first_sibling.rgt==6)
  93. def test_change_parent(self):
  94. self.test_sibling()
  95. # add grand child
  96. self.grand_child.save(1)
  97. update_nsm(self.grand_child)
  98. # check lft rgt
  99. self.assertTrue(self.grand_child.lft==3)
  100. self.assertTrue(self.grand_child.rgt==4)
  101. # change parent
  102. self.grand_child.parent_node = 'C002'
  103. self.grand_child.save()
  104. # update
  105. update_nsm(self.grand_child)
  106. # check lft rgt
  107. self.assertTrue(self.grand_child.lft==5)
  108. self.assertTrue(self.grand_child.rgt==6)
  109. def tearDown(self):
  110. webnotes.conn.sql("drop table tabnsmtest")
  111. # called in the on_update method
  112. def update_nsm(doc_obj):
  113. # get fields, data from the DocType
  114. pf, opf = 'parent_node', 'old_parent'
  115. if str(doc_obj.__class__)=='webnotes.model.doc.Document':
  116. # passed as a Document object
  117. d = doc_obj
  118. else:
  119. # passed as a DocType object
  120. d = doc_obj.doc
  121. if hasattr(doc_obj,'nsm_parent_field'):
  122. pf = doc_obj.nsm_parent_field
  123. if hasattr(doc_obj,'nsm_oldparent_field'):
  124. opf = doc_obj.nsm_oldparent_field
  125. p, op = d.fields.get(pf, ''), d.fields.get(opf, '')
  126. # has parent changed (?) or parent is None (root)
  127. if not d.lft and not d.rgt:
  128. update_add_node(d.doctype, d.name, p or '', pf)
  129. elif op != p:
  130. update_remove_node(d.doctype, d.name)
  131. update_add_node(d.doctype, d.name, p or '', pf)
  132. # set old parent
  133. webnotes.conn.set(d, opf, p or '')
  134. # reload
  135. d._loadfromdb()
  136. def rebuild_tree(doctype, parent_field):
  137. """
  138. call rebuild_node for all root nodes
  139. """
  140. # get all roots
  141. right = 1
  142. result = webnotes.conn.sql("SELECT name FROM `tab%s` WHERE `%s`='' or `%s` IS NULL ORDER BY name ASC" % (doctype, parent_field, parent_field))
  143. for r in result:
  144. right = rebuild_node(doctype, r[0], right, parent_field)
  145. webnotes.conn.sql("commit")
  146. webnotes.conn.sql("start transaction")
  147. def rebuild_node(doctype, parent, left, parent_field, cnt = 0):
  148. """
  149. reset lft, rgt and recursive call for all children
  150. """
  151. from webnotes.utils import now
  152. n = now()
  153. # the right value of this node is the left value + 1
  154. right = left+1
  155. # get all children of this node
  156. result = webnotes.conn.sql("SELECT name FROM `tab%s` WHERE `%s`='%s'" % (doctype, parent_field, parent))
  157. for r in result:
  158. right = rebuild_node(doctype, r[0], right, parent_field, cnt)
  159. # we've got the left value, and now that we've processed
  160. # the children of this node we also know the right value
  161. webnotes.conn.sql("UPDATE `tab%s` SET lft=%s, rgt=%s, modified='%s' WHERE name='%s'" % (doctype,left,right,n,parent))
  162. # commit after every 100
  163. cnt += 1
  164. if cnt % 100 == 0:
  165. cnt = 0
  166. webnotes.conn.sql("commit")
  167. webnotes.conn.sql("start transaction")
  168. #return the right value of this node + 1
  169. return right+1
  170. def update_add_node(doctype, name, parent, parent_field):
  171. """
  172. insert a new node
  173. """
  174. from webnotes.utils import now
  175. n = now()
  176. # get the last sibling of the parent
  177. if parent:
  178. right = webnotes.conn.sql("select rgt from `tab%s` where name='%s'" % (doctype, parent))[0][0]
  179. else: # root
  180. right = webnotes.conn.sql("select ifnull(max(rgt),0)+1 from `tab%s` where ifnull(`%s`,'') =''" % (doctype, parent_field))[0][0]
  181. right = right or 1
  182. # update all on the right
  183. webnotes.conn.sql("update `tab%s` set rgt = rgt+2, modified='%s' where rgt >= %s" %(doctype,n,right))
  184. webnotes.conn.sql("update `tab%s` set lft = lft+2, modified='%s' where lft >= %s" %(doctype,n,right))
  185. # update index of new node
  186. if webnotes.conn.sql("select * from `tab%s` where lft=%s or rgt=%s"% (doctype, right, right+1)):
  187. webnotes.msgprint("Nested set error. Please send mail to support")
  188. raise Exception
  189. webnotes.conn.sql("update `tab%s` set lft=%s, rgt=%s, modified='%s' where name='%s'" % (doctype,right,right+1,n,name))
  190. return right
  191. def update_remove_node(doctype, name):
  192. """
  193. remove a node
  194. """
  195. from webnotes.utils import now
  196. n = now()
  197. left = webnotes.conn.sql("select lft from `tab%s` where name='%s'" % (doctype,name))
  198. if left[0][0]:
  199. # reset this node
  200. webnotes.conn.sql("update `tab%s` set lft=0, rgt=0, modified='%s' where name='%s'" % (doctype,n,name))
  201. # update all on the right
  202. webnotes.conn.sql("update `tab%s` set rgt = rgt-2, modified='%s' where rgt > %s" %(doctype,n,left[0][0]))
  203. webnotes.conn.sql("update `tab%s` set lft = lft-2, modified='%s' where lft > %s" %(doctype,n,left[0][0]))