Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

338 wiersze
10 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_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)