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.

nestedset.py 10 KiB

13 jaren geleden
13 jaren geleden
12 jaren geleden
12 jaren geleden
12 jaren geleden
12 jaren geleden
12 jaren geleden
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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_item_group" 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. from __future__ import unicode_literals
  31. import webnotes, unittest
  32. from webnotes import msgprint
  33. from webnotes.model.wrapper import ModelWrapper
  34. from webnotes.model.doc import Document
  35. class TestNSM(unittest.TestCase):
  36. def setUp(self):
  37. webnotes.conn.sql("delete from `tabItem Group`")
  38. self.data = [
  39. ["t1", None, 1, 20],
  40. ["c0", "t1", 2, 3],
  41. ["c1", "t1", 4, 11],
  42. ["gc1", "c1", 5, 6],
  43. ["gc2", "c1", 7, 8],
  44. ["gc3", "c1", 9, 10],
  45. ["c2", "t1", 12, 17],
  46. ["gc4", "c2", 13, 14],
  47. ["gc5", "c2", 15, 16],
  48. ["c3", "t1", 18, 19]
  49. ]
  50. for d in self.data:
  51. self.__dict__[d[0]] = ModelWrapper([Document(fielddata = {
  52. "doctype": "Item Group", "item_group_name": d[0], "parent_item_group": d[1],
  53. "__islocal": 1
  54. })])
  55. self.save_all()
  56. self.reload_all()
  57. def save_all(self):
  58. for d in self.data:
  59. self.__dict__[d[0]].save()
  60. def reload_all(self, data=None):
  61. for d in data or self.data:
  62. self.__dict__[d[0]].load_from_db()
  63. def test_basic_tree(self, data=None):
  64. for d in data or self.data:
  65. self.assertEquals(self.__dict__[d[0]].doc.lft, d[2])
  66. self.assertEquals(self.__dict__[d[0]].doc.rgt, d[3])
  67. def test_move_group(self):
  68. self.c1.doc.parent_item_group = 'c2'
  69. self.c1.save()
  70. self.reload_all()
  71. new_tree = [
  72. ["t1", None, 1, 20],
  73. ["c0", "t1", 2, 3],
  74. ["c2", "t1", 4, 17],
  75. ["gc4", "c2", 5, 6],
  76. ["gc5", "c2", 7, 8],
  77. ["c1", "t1", 9, 16],
  78. ["gc1", "c1", 10, 11],
  79. ["gc2", "c1", 12, 13],
  80. ["gc3", "c1", 14, 15],
  81. ["c3", "t1", 18, 19]
  82. ]
  83. self.test_basic_tree(new_tree)
  84. # Move back
  85. self.c1.doc.parent_item_group = 'gc4'
  86. self.c1.save()
  87. self.reload_all()
  88. new_tree = [
  89. ["t1", None, 1, 20],
  90. ["c0", "t1", 2, 3],
  91. ["c2", "t1", 4, 17],
  92. ["gc4", "c2", 5, 14],
  93. ["c1", "t1", 6, 13],
  94. ["gc1", "c1", 7, 8],
  95. ["gc2", "c1", 9, 10],
  96. ["gc3", "c1", 11, 12],
  97. ["gc5", "c2", 15, 16],
  98. ["c3", "t1", 18, 19]
  99. ]
  100. self.test_basic_tree(new_tree)
  101. # Move to root
  102. self.c1.doc.parent_item_group = ''
  103. self.c1.save()
  104. self.reload_all()
  105. new_tree = [
  106. ["t1", None, 1, 12],
  107. ["c0", "t1", 2, 3],
  108. ["c2", "t1", 4, 9],
  109. ["gc4", "c2", 5, 6],
  110. ["gc5", "c2", 7, 8],
  111. ["c3", "t1", 10, 11],
  112. ["c1", "t1", 13, 20],
  113. ["gc1", "c1", 14, 15],
  114. ["gc2", "c1", 16, 17],
  115. ["gc3", "c1", 18, 19],
  116. ]
  117. self.test_basic_tree(new_tree)
  118. # move leaf
  119. self.gc3.doc.parent_item_group = 'c2'
  120. self.gc3.save()
  121. self.reload_all()
  122. new_tree = [
  123. ["t1", None, 1, 14],
  124. ["c0", "t1", 2, 3],
  125. ["c2", "t1", 4, 11],
  126. ["gc4", "c2", 5, 6],
  127. ["gc5", "c2", 7, 8],
  128. ["gc3", "c1", 9, 10],
  129. ["c3", "t1", 12, 13],
  130. ["c1", "t1", 15, 20],
  131. ["gc1", "c1", 16, 17],
  132. ["gc2", "c1", 18, 19],
  133. ]
  134. self.test_basic_tree(new_tree)
  135. # delete leaf
  136. from webnotes.model import delete_doc
  137. delete_doc(self.gc2.doc.doctype, self.gc2.doc.name)
  138. new_tree = [
  139. ["t1", None, 1, 14],
  140. ["c0", "t1", 2, 3],
  141. ["c2", "t1", 4, 11],
  142. ["gc4", "c2", 5, 6],
  143. ["gc5", "c2", 7, 8],
  144. ["gc3", "c1", 9, 10],
  145. ["c3", "t1", 12, 13],
  146. ["c1", "t1", 15, 18],
  147. ["gc1", "c1", 16, 17],
  148. ]
  149. del self.__dict__["gc2"]
  150. self.reload_all(new_tree)
  151. self.test_basic_tree(new_tree)
  152. # for testing
  153. # for d in new_tree:
  154. # doc = self.__dict__[d[0]].doc
  155. # print doc.name, doc.lft, doc.rgt
  156. def tearDown(self):
  157. webnotes.conn.rollback()
  158. # called in the on_update method
  159. def update_nsm(doc_obj):
  160. # get fields, data from the DocType
  161. pf, opf = 'parent_node', 'old_parent'
  162. if str(doc_obj.__class__)=='webnotes.model.doc.Document':
  163. # passed as a Document object
  164. d = doc_obj
  165. else:
  166. # passed as a DocType object
  167. d = doc_obj.doc
  168. if hasattr(doc_obj,'nsm_parent_field'):
  169. pf = doc_obj.nsm_parent_field
  170. if hasattr(doc_obj,'nsm_oldparent_field'):
  171. opf = doc_obj.nsm_oldparent_field
  172. p, op = d.fields.get(pf, ''), d.fields.get(opf, '')
  173. # has parent changed (?) or parent is None (root)
  174. if not d.lft and not d.rgt:
  175. update_add_node(d.doctype, d.name, p or '', pf)
  176. elif op != p:
  177. update_move_node(d, pf)
  178. # set old parent
  179. webnotes.conn.set(d, opf, p or '')
  180. # reload
  181. d._loadfromdb()
  182. def update_move_node(doc, parent_field):
  183. # move to dark side
  184. parent = doc.fields.get(parent_field)
  185. webnotes.conn.sql("""update `tab%s` set lft = -lft, rgt = -rgt
  186. where lft >= %s and rgt <= %s"""% (doc.doctype, '%s', '%s'), (doc.lft, doc.rgt))
  187. # shift left
  188. diff = doc.rgt - doc.lft + 1
  189. webnotes.conn.sql("""update `tab%s` set lft = lft -%s, rgt = rgt - %s
  190. where lft > %s"""% (doc.doctype, '%s', '%s', '%s'), (diff, diff, doc.rgt))
  191. # shift left rgts of ancestors whose only rgts must shift
  192. webnotes.conn.sql("""update `tab%s` set rgt = rgt - %s
  193. where lft < %s and rgt > %s"""% (doc.doctype, '%s', '%s', '%s'),
  194. (diff, doc.lft, doc.rgt))
  195. if parent:
  196. new_parent = webnotes.conn.sql("""select lft, rgt from `tab%s`
  197. where name = %s""" % (doc.doctype, '%s'), parent, as_dict=1)[0]
  198. # set parent lft, rgt
  199. webnotes.conn.sql("""update `tab%s` set rgt = rgt + %s
  200. where name = %s"""% (doc.doctype, '%s', '%s'), (diff, parent))
  201. # shift right at new parent
  202. webnotes.conn.sql("""update `tab%s` set lft = lft + %s, rgt = rgt + %s
  203. where lft > %s""" % (doc.doctype, '%s', '%s', '%s'),
  204. (diff, diff, new_parent.rgt))
  205. # shift right rgts of ancestors whose only rgts must shift
  206. webnotes.conn.sql("""update `tab%s` set rgt = rgt + %s
  207. where lft < %s and rgt > %s""" % (doc.doctype, '%s', '%s', '%s'),
  208. (diff, new_parent.lft, new_parent.rgt))
  209. new_diff = new_parent.rgt - doc.lft
  210. else:
  211. # new root
  212. max_rgt = webnotes.conn.sql("""select max(rgt) from `tab%s`""" % doc.doctype)[0][0]
  213. new_diff = max_rgt + 1 - doc.lft
  214. # bring back from dark side
  215. webnotes.conn.sql("""update `tab%s` set lft = -lft + %s, rgt = -rgt + %s
  216. where lft < 0"""% (doc.doctype, '%s', '%s'), (new_diff, new_diff))
  217. def rebuild_tree(doctype, parent_field):
  218. """
  219. call rebuild_node for all root nodes
  220. """
  221. # get all roots
  222. webnotes.conn.auto_commit_on_many_writes = 1
  223. right = 1
  224. result = webnotes.conn.sql("SELECT name FROM `tab%s` WHERE `%s`='' or `%s` IS NULL ORDER BY name ASC" % (doctype, parent_field, parent_field))
  225. for r in result:
  226. right = rebuild_node(doctype, r[0], right, parent_field)
  227. webnotes.conn.auto_commit_on_many_writes = 0
  228. def rebuild_node(doctype, parent, left, parent_field):
  229. """
  230. reset lft, rgt and recursive call for all children
  231. """
  232. from webnotes.utils import now
  233. n = now()
  234. # the right value of this node is the left value + 1
  235. right = left+1
  236. # get all children of this node
  237. result = webnotes.conn.sql("SELECT name FROM `tab%s` WHERE `%s`='%s'" % (doctype, parent_field, parent))
  238. for r in result:
  239. right = rebuild_node(doctype, r[0], right, parent_field)
  240. # we've got the left value, and now that we've processed
  241. # the children of this node we also know the right value
  242. webnotes.conn.sql("UPDATE `tab%s` SET lft=%s, rgt=%s, modified='%s' WHERE name='%s'" % (doctype,left,right,n,parent))
  243. #return the right value of this node + 1
  244. return right+1
  245. def update_add_node(doctype, name, parent, parent_field):
  246. """
  247. insert a new node
  248. """
  249. from webnotes.utils import now
  250. n = now()
  251. # get the last sibling of the parent
  252. if parent:
  253. right = webnotes.conn.sql("select rgt from `tab%s` where name='%s'" % (doctype, parent))[0][0]
  254. else: # root
  255. right = webnotes.conn.sql("select ifnull(max(rgt),0)+1 from `tab%s` where ifnull(`%s`,'') =''" % (doctype, parent_field))[0][0]
  256. right = right or 1
  257. # update all on the right
  258. webnotes.conn.sql("update `tab%s` set rgt = rgt+2, modified='%s' where rgt >= %s" %(doctype,n,right))
  259. webnotes.conn.sql("update `tab%s` set lft = lft+2, modified='%s' where lft >= %s" %(doctype,n,right))
  260. # update index of new node
  261. if webnotes.conn.sql("select * from `tab%s` where lft=%s or rgt=%s"% (doctype, right, right+1)):
  262. webnotes.msgprint("Nested set error. Please send mail to support")
  263. raise Exception
  264. webnotes.conn.sql("update `tab%s` set lft=%s, rgt=%s, modified='%s' where name='%s'" % (doctype,right,right+1,n,name))
  265. return right
  266. class DocTypeNestedSet(object):
  267. def on_update(self):
  268. update_nsm(self)
  269. def on_trash(self):
  270. self.doc.fields[self.nsm_parent_field] = ""
  271. update_nsm(self)
  272. def validate_root_details(self, root, parent_field):
  273. #does not exists parent
  274. if self.doc.name==root and self.doc.fields.get(parent_field):
  275. msgprint("You can not assign parent for root: %s" % (root, ), raise_exception=1)
  276. elif self.doc.name!=root and not self.doc.parent_account:
  277. msgprint("Parent is mandatory for %s" % (self.doc.name, ), raise_exception=1)