##// END OF EJS Templates
Fixed: Deleting a subtasks doesn't update parent's rgt & lft values, introduced by r5286 (#9577)....
Jean-Philippe Lang -
r7694:594074d91bd4
parent child
Show More
@@ -1,381 +1,395
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueNestedSetTest < ActiveSupport::TestCase
20 class IssueNestedSetTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 self.use_transactional_fixtures = false
30 self.use_transactional_fixtures = false
31
31
32 def test_create_root_issue
32 def test_create_root_issue
33 issue1 = create_issue!
33 issue1 = create_issue!
34 issue2 = create_issue!
34 issue2 = create_issue!
35 issue1.reload
35 issue1.reload
36 issue2.reload
36 issue2.reload
37
37
38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
40 end
40 end
41
41
42 def test_create_child_issue
42 def test_create_child_issue
43 parent = create_issue!
43 parent = create_issue!
44 child = create_issue!(:parent_issue_id => parent.id)
44 child = create_issue!(:parent_issue_id => parent.id)
45 parent.reload
45 parent.reload
46 child.reload
46 child.reload
47
47
48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
50 end
50 end
51
51
52 def test_creating_a_child_in_different_project_should_not_validate
52 def test_creating_a_child_in_different_project_should_not_validate
53 issue = create_issue!
53 issue = create_issue!
54 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
54 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
55 :subject => 'child', :parent_issue_id => issue.id)
55 :subject => 'child', :parent_issue_id => issue.id)
56 assert !child.save
56 assert !child.save
57 assert_not_nil child.errors[:parent_issue_id]
57 assert_not_nil child.errors[:parent_issue_id]
58 end
58 end
59
59
60 def test_move_a_root_to_child
60 def test_move_a_root_to_child
61 parent1 = create_issue!
61 parent1 = create_issue!
62 parent2 = create_issue!
62 parent2 = create_issue!
63 child = create_issue!(:parent_issue_id => parent1.id)
63 child = create_issue!(:parent_issue_id => parent1.id)
64
64
65 parent2.parent_issue_id = parent1.id
65 parent2.parent_issue_id = parent1.id
66 parent2.save!
66 parent2.save!
67 child.reload
67 child.reload
68 parent1.reload
68 parent1.reload
69 parent2.reload
69 parent2.reload
70
70
71 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
71 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
72 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
72 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
73 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
73 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
74 end
74 end
75
75
76 def test_move_a_child_to_root
76 def test_move_a_child_to_root
77 parent1 = create_issue!
77 parent1 = create_issue!
78 parent2 = create_issue!
78 parent2 = create_issue!
79 child = create_issue!(:parent_issue_id => parent1.id)
79 child = create_issue!(:parent_issue_id => parent1.id)
80
80
81 child.parent_issue_id = nil
81 child.parent_issue_id = nil
82 child.save!
82 child.save!
83 child.reload
83 child.reload
84 parent1.reload
84 parent1.reload
85 parent2.reload
85 parent2.reload
86
86
87 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
87 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
88 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
88 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
89 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
89 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
90 end
90 end
91
91
92 def test_move_a_child_to_another_issue
92 def test_move_a_child_to_another_issue
93 parent1 = create_issue!
93 parent1 = create_issue!
94 parent2 = create_issue!
94 parent2 = create_issue!
95 child = create_issue!(:parent_issue_id => parent1.id)
95 child = create_issue!(:parent_issue_id => parent1.id)
96
96
97 child.parent_issue_id = parent2.id
97 child.parent_issue_id = parent2.id
98 child.save!
98 child.save!
99 child.reload
99 child.reload
100 parent1.reload
100 parent1.reload
101 parent2.reload
101 parent2.reload
102
102
103 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
103 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
104 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
104 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
105 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
105 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
106 end
106 end
107
107
108 def test_move_a_child_with_descendants_to_another_issue
108 def test_move_a_child_with_descendants_to_another_issue
109 parent1 = create_issue!
109 parent1 = create_issue!
110 parent2 = create_issue!
110 parent2 = create_issue!
111 child = create_issue!(:parent_issue_id => parent1.id)
111 child = create_issue!(:parent_issue_id => parent1.id)
112 grandchild = create_issue!(:parent_issue_id => child.id)
112 grandchild = create_issue!(:parent_issue_id => child.id)
113
113
114 parent1.reload
114 parent1.reload
115 parent2.reload
115 parent2.reload
116 child.reload
116 child.reload
117 grandchild.reload
117 grandchild.reload
118
118
119 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
119 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
120 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
120 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
121 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
121 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
122 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
122 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
123
123
124 child.reload.parent_issue_id = parent2.id
124 child.reload.parent_issue_id = parent2.id
125 child.save!
125 child.save!
126 child.reload
126 child.reload
127 grandchild.reload
127 grandchild.reload
128 parent1.reload
128 parent1.reload
129 parent2.reload
129 parent2.reload
130
130
131 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
131 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
132 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
132 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
133 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
133 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
134 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
134 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
135 end
135 end
136
136
137 def test_move_a_child_with_descendants_to_another_project
137 def test_move_a_child_with_descendants_to_another_project
138 parent1 = create_issue!
138 parent1 = create_issue!
139 child = create_issue!(:parent_issue_id => parent1.id)
139 child = create_issue!(:parent_issue_id => parent1.id)
140 grandchild = create_issue!(:parent_issue_id => child.id)
140 grandchild = create_issue!(:parent_issue_id => child.id)
141
141
142 assert child.reload.move_to_project(Project.find(2))
142 assert child.reload.move_to_project(Project.find(2))
143 child.reload
143 child.reload
144 grandchild.reload
144 grandchild.reload
145 parent1.reload
145 parent1.reload
146
146
147 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
147 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
148 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
148 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
149 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
149 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
150 end
150 end
151
151
152 def test_invalid_move_to_another_project
152 def test_invalid_move_to_another_project
153 parent1 = create_issue!
153 parent1 = create_issue!
154 child = create_issue!(:parent_issue_id => parent1.id)
154 child = create_issue!(:parent_issue_id => parent1.id)
155 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
155 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
156 Project.find(2).tracker_ids = [1]
156 Project.find(2).tracker_ids = [1]
157
157
158 parent1.reload
158 parent1.reload
159 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
159 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
160
160
161 # child can not be moved to Project 2 because its child is on a disabled tracker
161 # child can not be moved to Project 2 because its child is on a disabled tracker
162 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
162 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
163 child.reload
163 child.reload
164 grandchild.reload
164 grandchild.reload
165 parent1.reload
165 parent1.reload
166
166
167 # no change
167 # no change
168 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
168 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
169 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
169 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
170 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
170 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
171 end
171 end
172
172
173 def test_moving_an_issue_to_a_descendant_should_not_validate
173 def test_moving_an_issue_to_a_descendant_should_not_validate
174 parent1 = create_issue!
174 parent1 = create_issue!
175 parent2 = create_issue!
175 parent2 = create_issue!
176 child = create_issue!(:parent_issue_id => parent1.id)
176 child = create_issue!(:parent_issue_id => parent1.id)
177 grandchild = create_issue!(:parent_issue_id => child.id)
177 grandchild = create_issue!(:parent_issue_id => child.id)
178
178
179 child.reload
179 child.reload
180 child.parent_issue_id = grandchild.id
180 child.parent_issue_id = grandchild.id
181 assert !child.save
181 assert !child.save
182 assert_not_nil child.errors[:parent_issue_id]
182 assert_not_nil child.errors[:parent_issue_id]
183 end
183 end
184
184
185 def test_moving_an_issue_should_keep_valid_relations_only
185 def test_moving_an_issue_should_keep_valid_relations_only
186 issue1 = create_issue!
186 issue1 = create_issue!
187 issue2 = create_issue!
187 issue2 = create_issue!
188 issue3 = create_issue!(:parent_issue_id => issue2.id)
188 issue3 = create_issue!(:parent_issue_id => issue2.id)
189 issue4 = create_issue!
189 issue4 = create_issue!
190 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
190 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
191 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
191 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
192 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
192 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
193 issue2.reload
193 issue2.reload
194 issue2.parent_issue_id = issue1.id
194 issue2.parent_issue_id = issue1.id
195 issue2.save!
195 issue2.save!
196 assert !IssueRelation.exists?(r1.id)
196 assert !IssueRelation.exists?(r1.id)
197 assert !IssueRelation.exists?(r2.id)
197 assert !IssueRelation.exists?(r2.id)
198 assert IssueRelation.exists?(r3.id)
198 assert IssueRelation.exists?(r3.id)
199 end
199 end
200
200
201 def test_destroy_should_destroy_children
201 def test_destroy_should_destroy_children
202 issue1 = create_issue!
202 issue1 = create_issue!
203 issue2 = create_issue!
203 issue2 = create_issue!
204 issue3 = create_issue!(:parent_issue_id => issue2.id)
204 issue3 = create_issue!(:parent_issue_id => issue2.id)
205 issue4 = create_issue!(:parent_issue_id => issue1.id)
205 issue4 = create_issue!(:parent_issue_id => issue1.id)
206
206
207 issue3.init_journal(User.find(2))
207 issue3.init_journal(User.find(2))
208 issue3.subject = 'child with journal'
208 issue3.subject = 'child with journal'
209 issue3.save!
209 issue3.save!
210
210
211 assert_difference 'Issue.count', -2 do
211 assert_difference 'Issue.count', -2 do
212 assert_difference 'Journal.count', -1 do
212 assert_difference 'Journal.count', -1 do
213 assert_difference 'JournalDetail.count', -1 do
213 assert_difference 'JournalDetail.count', -1 do
214 Issue.find(issue2.id).destroy
214 Issue.find(issue2.id).destroy
215 end
215 end
216 end
216 end
217 end
217 end
218
218
219 issue1.reload
219 issue1.reload
220 issue4.reload
220 issue4.reload
221 assert !Issue.exists?(issue2.id)
221 assert !Issue.exists?(issue2.id)
222 assert !Issue.exists?(issue3.id)
222 assert !Issue.exists?(issue3.id)
223 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
223 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
224 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
224 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
225 end
225 end
226
227 def test_destroy_child_should_update_parent
228 issue = create_issue!
229 child1 = create_issue!(:parent_issue_id => issue.id)
230 child2 = create_issue!(:parent_issue_id => issue.id)
231
232 issue.reload
233 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
234
235 child2.reload.destroy
236
237 issue.reload
238 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
239 end
226
240
227 def test_destroy_parent_issue_updated_during_children_destroy
241 def test_destroy_parent_issue_updated_during_children_destroy
228 parent = create_issue!
242 parent = create_issue!
229 create_issue!(:start_date => Date.today, :parent_issue_id => parent.id)
243 create_issue!(:start_date => Date.today, :parent_issue_id => parent.id)
230 create_issue!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
244 create_issue!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
231
245
232 assert_difference 'Issue.count', -3 do
246 assert_difference 'Issue.count', -3 do
233 Issue.find(parent.id).destroy
247 Issue.find(parent.id).destroy
234 end
248 end
235 end
249 end
236
250
237 def test_destroy_child_issue_with_children
251 def test_destroy_child_issue_with_children
238 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
252 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
239 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
253 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
240 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
254 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
241 leaf.init_journal(User.find(2))
255 leaf.init_journal(User.find(2))
242 leaf.subject = 'leaf with journal'
256 leaf.subject = 'leaf with journal'
243 leaf.save!
257 leaf.save!
244
258
245 assert_difference 'Issue.count', -2 do
259 assert_difference 'Issue.count', -2 do
246 assert_difference 'Journal.count', -1 do
260 assert_difference 'Journal.count', -1 do
247 assert_difference 'JournalDetail.count', -1 do
261 assert_difference 'JournalDetail.count', -1 do
248 Issue.find(child.id).destroy
262 Issue.find(child.id).destroy
249 end
263 end
250 end
264 end
251 end
265 end
252
266
253 root = Issue.find(root.id)
267 root = Issue.find(root.id)
254 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
268 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
255 end
269 end
256
270
257 def test_destroy_issue_with_grand_child
271 def test_destroy_issue_with_grand_child
258 parent = create_issue!
272 parent = create_issue!
259 issue = create_issue!(:parent_issue_id => parent.id)
273 issue = create_issue!(:parent_issue_id => parent.id)
260 child = create_issue!(:parent_issue_id => issue.id)
274 child = create_issue!(:parent_issue_id => issue.id)
261 grandchild1 = create_issue!(:parent_issue_id => child.id)
275 grandchild1 = create_issue!(:parent_issue_id => child.id)
262 grandchild2 = create_issue!(:parent_issue_id => child.id)
276 grandchild2 = create_issue!(:parent_issue_id => child.id)
263
277
264 assert_difference 'Issue.count', -4 do
278 assert_difference 'Issue.count', -4 do
265 Issue.find(issue.id).destroy
279 Issue.find(issue.id).destroy
266 parent.reload
280 parent.reload
267 assert_equal [1, 2], [parent.lft, parent.rgt]
281 assert_equal [1, 2], [parent.lft, parent.rgt]
268 end
282 end
269 end
283 end
270
284
271 def test_parent_priority_should_be_the_highest_child_priority
285 def test_parent_priority_should_be_the_highest_child_priority
272 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
286 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
273 # Create children
287 # Create children
274 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
288 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
275 assert_equal 'High', parent.reload.priority.name
289 assert_equal 'High', parent.reload.priority.name
276 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
290 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
277 assert_equal 'Immediate', child1.reload.priority.name
291 assert_equal 'Immediate', child1.reload.priority.name
278 assert_equal 'Immediate', parent.reload.priority.name
292 assert_equal 'Immediate', parent.reload.priority.name
279 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
293 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
280 assert_equal 'Immediate', parent.reload.priority.name
294 assert_equal 'Immediate', parent.reload.priority.name
281 # Destroy a child
295 # Destroy a child
282 child1.destroy
296 child1.destroy
283 assert_equal 'Low', parent.reload.priority.name
297 assert_equal 'Low', parent.reload.priority.name
284 # Update a child
298 # Update a child
285 child3.reload.priority = IssuePriority.find_by_name('Normal')
299 child3.reload.priority = IssuePriority.find_by_name('Normal')
286 child3.save!
300 child3.save!
287 assert_equal 'Normal', parent.reload.priority.name
301 assert_equal 'Normal', parent.reload.priority.name
288 end
302 end
289
303
290 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
304 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
291 parent = create_issue!
305 parent = create_issue!
292 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
306 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
293 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
307 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
294 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
308 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
295 parent.reload
309 parent.reload
296 assert_equal Date.parse('2010-01-25'), parent.start_date
310 assert_equal Date.parse('2010-01-25'), parent.start_date
297 assert_equal Date.parse('2010-02-22'), parent.due_date
311 assert_equal Date.parse('2010-02-22'), parent.due_date
298 end
312 end
299
313
300 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
314 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
301 parent = create_issue!
315 parent = create_issue!
302 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
316 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
303 assert_equal 20, parent.reload.done_ratio
317 assert_equal 20, parent.reload.done_ratio
304 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
318 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
305 assert_equal 45, parent.reload.done_ratio
319 assert_equal 45, parent.reload.done_ratio
306
320
307 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
321 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
308 assert_equal 30, parent.reload.done_ratio
322 assert_equal 30, parent.reload.done_ratio
309
323
310 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
324 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
311 assert_equal 30, child.reload.done_ratio
325 assert_equal 30, child.reload.done_ratio
312 assert_equal 40, parent.reload.done_ratio
326 assert_equal 40, parent.reload.done_ratio
313 end
327 end
314
328
315 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
329 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
316 parent = create_issue!
330 parent = create_issue!
317 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
331 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
318 assert_equal 20, parent.reload.done_ratio
332 assert_equal 20, parent.reload.done_ratio
319 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
333 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
320 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
334 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
321 end
335 end
322
336
323 def test_parent_estimate_should_be_sum_of_leaves
337 def test_parent_estimate_should_be_sum_of_leaves
324 parent = create_issue!
338 parent = create_issue!
325 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
339 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
326 assert_equal nil, parent.reload.estimated_hours
340 assert_equal nil, parent.reload.estimated_hours
327 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
341 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
328 assert_equal 5, parent.reload.estimated_hours
342 assert_equal 5, parent.reload.estimated_hours
329 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
343 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
330 assert_equal 12, parent.reload.estimated_hours
344 assert_equal 12, parent.reload.estimated_hours
331 end
345 end
332
346
333 def test_move_parent_updates_old_parent_attributes
347 def test_move_parent_updates_old_parent_attributes
334 first_parent = create_issue!
348 first_parent = create_issue!
335 second_parent = create_issue!
349 second_parent = create_issue!
336 child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
350 child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
337 assert_equal 5, first_parent.reload.estimated_hours
351 assert_equal 5, first_parent.reload.estimated_hours
338 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
352 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
339 assert_equal 7, second_parent.reload.estimated_hours
353 assert_equal 7, second_parent.reload.estimated_hours
340 assert_nil first_parent.reload.estimated_hours
354 assert_nil first_parent.reload.estimated_hours
341 end
355 end
342
356
343 def test_reschuling_a_parent_should_reschedule_subtasks
357 def test_reschuling_a_parent_should_reschedule_subtasks
344 parent = create_issue!
358 parent = create_issue!
345 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
359 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
346 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
360 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
347 parent.reload
361 parent.reload
348 parent.reschedule_after(Date.parse('2010-06-02'))
362 parent.reschedule_after(Date.parse('2010-06-02'))
349 c1.reload
363 c1.reload
350 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
364 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
351 c2.reload
365 c2.reload
352 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
366 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
353 parent.reload
367 parent.reload
354 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
368 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
355 end
369 end
356
370
357 def test_project_copy_should_copy_issue_tree
371 def test_project_copy_should_copy_issue_tree
358 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
372 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
359 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
373 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
360 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
374 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
361 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
375 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
362 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
376 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
363 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
377 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
364 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
378 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
365 c.copy(p, :only => 'issues')
379 c.copy(p, :only => 'issues')
366 c.reload
380 c.reload
367
381
368 assert_equal 5, c.issues.count
382 assert_equal 5, c.issues.count
369 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
383 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
370 assert ic1.root?
384 assert ic1.root?
371 assert_equal ic1, ic2.parent
385 assert_equal ic1, ic2.parent
372 assert_equal ic1, ic3.parent
386 assert_equal ic1, ic3.parent
373 assert_equal ic2, ic4.parent
387 assert_equal ic2, ic4.parent
374 assert ic5.root?
388 assert ic5.root?
375 end
389 end
376
390
377 # Helper that creates an issue with default attributes
391 # Helper that creates an issue with default attributes
378 def create_issue!(attributes={})
392 def create_issue!(attributes={})
379 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
393 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
380 end
394 end
381 end
395 end
@@ -1,553 +1,553
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4 def self.included(base)
4 def self.included(base)
5 base.extend(SingletonMethods)
5 base.extend(SingletonMethods)
6 end
6 end
7
7
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
10 # descendants with a single query. The drawback is that insertion or move need some complex
10 # descendants with a single query. The drawback is that insertion or move need some complex
11 # sql queries. But everything is done here by this module!
11 # sql queries. But everything is done here by this module!
12 #
12 #
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
15 #
15 #
16 # == API
16 # == API
17 #
17 #
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19 # by another easier, except for the creation:
19 # by another easier, except for the creation:
20 #
20 #
21 # in acts_as_tree:
21 # in acts_as_tree:
22 # item.children.create(:name => "child1")
22 # item.children.create(:name => "child1")
23 #
23 #
24 # in acts_as_nested_set:
24 # in acts_as_nested_set:
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26 # child = MyClass.new(:name => "child1")
26 # child = MyClass.new(:name => "child1")
27 # child.save
27 # child.save
28 # # now move the item to its right place
28 # # now move the item to its right place
29 # child.move_to_child_of my_item
29 # child.move_to_child_of my_item
30 #
30 #
31 # You can pass an id or an object to:
31 # You can pass an id or an object to:
32 # * <tt>#move_to_child_of</tt>
32 # * <tt>#move_to_child_of</tt>
33 # * <tt>#move_to_right_of</tt>
33 # * <tt>#move_to_right_of</tt>
34 # * <tt>#move_to_left_of</tt>
34 # * <tt>#move_to_left_of</tt>
35 #
35 #
36 module SingletonMethods
36 module SingletonMethods
37 # Configuration options are:
37 # Configuration options are:
38 #
38 #
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40 # * +:left_column+ - column name for left boundry data, default "lft"
40 # * +:left_column+ - column name for left boundry data, default "lft"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43 # (if it hasn't been already) and use that as the foreign key restriction. You
43 # (if it hasn't been already) and use that as the foreign key restriction. You
44 # can also pass an array to scope by multiple attributes.
44 # can also pass an array to scope by multiple attributes.
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47 # child objects are destroyed alongside this object by calling their destroy
47 # child objects are destroyed alongside this object by calling their destroy
48 # method. If set to :delete_all (default), all the child objects are deleted
48 # method. If set to :delete_all (default), all the child objects are deleted
49 # without calling their destroy method.
49 # without calling their destroy method.
50 #
50 #
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53 # to acts_as_nested_set models
53 # to acts_as_nested_set models
54 def acts_as_nested_set(options = {})
54 def acts_as_nested_set(options = {})
55 options = {
55 options = {
56 :parent_column => 'parent_id',
56 :parent_column => 'parent_id',
57 :left_column => 'lft',
57 :left_column => 'lft',
58 :right_column => 'rgt',
58 :right_column => 'rgt',
59 :order => 'id',
59 :order => 'id',
60 :dependent => :delete_all, # or :destroy
60 :dependent => :delete_all, # or :destroy
61 }.merge(options)
61 }.merge(options)
62
62
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
64 options[:scope] = "#{options[:scope]}_id".intern
64 options[:scope] = "#{options[:scope]}_id".intern
65 end
65 end
66
66
67 write_inheritable_attribute :acts_as_nested_set_options, options
67 write_inheritable_attribute :acts_as_nested_set_options, options
68 class_inheritable_reader :acts_as_nested_set_options
68 class_inheritable_reader :acts_as_nested_set_options
69
69
70 include Comparable
70 include Comparable
71 include Columns
71 include Columns
72 include InstanceMethods
72 include InstanceMethods
73 extend Columns
73 extend Columns
74 extend ClassMethods
74 extend ClassMethods
75
75
76 # no bulk assignment
76 # no bulk assignment
77 attr_protected left_column_name.intern,
77 attr_protected left_column_name.intern,
78 right_column_name.intern,
78 right_column_name.intern,
79 parent_column_name.intern
79 parent_column_name.intern
80
80
81 before_create :set_default_left_and_right
81 before_create :set_default_left_and_right
82 before_destroy :prune_from_tree
82 before_destroy :prune_from_tree
83
83
84 # no assignment to structure fields
84 # no assignment to structure fields
85 [left_column_name, right_column_name, parent_column_name].each do |column|
85 [left_column_name, right_column_name, parent_column_name].each do |column|
86 module_eval <<-"end_eval", __FILE__, __LINE__
86 module_eval <<-"end_eval", __FILE__, __LINE__
87 def #{column}=(x)
87 def #{column}=(x)
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
89 end
89 end
90 end_eval
90 end_eval
91 end
91 end
92
92
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
95 if self.respond_to?(:define_callbacks)
95 if self.respond_to?(:define_callbacks)
96 define_callbacks("before_move", "after_move")
96 define_callbacks("before_move", "after_move")
97 end
97 end
98
98
99
99
100 end
100 end
101
101
102 end
102 end
103
103
104 module ClassMethods
104 module ClassMethods
105
105
106 # Returns the first root
106 # Returns the first root
107 def root
107 def root
108 roots.find(:first)
108 roots.find(:first)
109 end
109 end
110
110
111 def valid?
111 def valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
113 end
113 end
114
114
115 def left_and_rights_valid?
115 def left_and_rights_valid?
116 count(
116 count(
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
119 :conditions =>
119 :conditions =>
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
127 ) == 0
127 ) == 0
128 end
128 end
129
129
130 def no_duplicates_for_columns?
130 def no_duplicates_for_columns?
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
132 connection.quote_column_name(c)
132 connection.quote_column_name(c)
133 end.push(nil).join(", ")
133 end.push(nil).join(", ")
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
135 # No duplicates
135 # No duplicates
136 find(:first,
136 find(:first,
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
138 :group => "#{scope_string}#{column}
138 :group => "#{scope_string}#{column}
139 HAVING COUNT(#{column}) > 1").nil?
139 HAVING COUNT(#{column}) > 1").nil?
140 end
140 end
141 end
141 end
142
142
143 # Wrapper for each_root_valid? that can deal with scope.
143 # Wrapper for each_root_valid? that can deal with scope.
144 def all_roots_valid?
144 def all_roots_valid?
145 if acts_as_nested_set_options[:scope]
145 if acts_as_nested_set_options[:scope]
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
147 each_root_valid?(grouped_roots)
147 each_root_valid?(grouped_roots)
148 end
148 end
149 else
149 else
150 each_root_valid?(roots)
150 each_root_valid?(roots)
151 end
151 end
152 end
152 end
153
153
154 def each_root_valid?(roots_to_validate)
154 def each_root_valid?(roots_to_validate)
155 left = right = 0
155 left = right = 0
156 roots_to_validate.all? do |root|
156 roots_to_validate.all? do |root|
157 (root.left > left && root.right > right).tap do
157 (root.left > left && root.right > right).tap do
158 left = root.left
158 left = root.left
159 right = root.right
159 right = root.right
160 end
160 end
161 end
161 end
162 end
162 end
163
163
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
165 def rebuild!
165 def rebuild!
166 # Don't rebuild a valid tree.
166 # Don't rebuild a valid tree.
167 return true if valid?
167 return true if valid?
168
168
169 scope = lambda{|node|}
169 scope = lambda{|node|}
170 if acts_as_nested_set_options[:scope]
170 if acts_as_nested_set_options[:scope]
171 scope = lambda{|node|
171 scope = lambda{|node|
172 scope_column_names.inject(""){|str, column_name|
172 scope_column_names.inject(""){|str, column_name|
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
174 }
174 }
175 }
175 }
176 end
176 end
177 indices = {}
177 indices = {}
178
178
179 set_left_and_rights = lambda do |node|
179 set_left_and_rights = lambda do |node|
180 # set left
180 # set left
181 node[left_column_name] = indices[scope.call(node)] += 1
181 node[left_column_name] = indices[scope.call(node)] += 1
182 # find
182 # find
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
184 # set right
184 # set right
185 node[right_column_name] = indices[scope.call(node)] += 1
185 node[right_column_name] = indices[scope.call(node)] += 1
186 node.save!
186 node.save!
187 end
187 end
188
188
189 # Find root node(s)
189 # Find root node(s)
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
191 # setup index for this scope
191 # setup index for this scope
192 indices[scope.call(root_node)] ||= 0
192 indices[scope.call(root_node)] ||= 0
193 set_left_and_rights.call(root_node)
193 set_left_and_rights.call(root_node)
194 end
194 end
195 end
195 end
196 end
196 end
197
197
198 # Mixed into both classes and instances to provide easy access to the column names
198 # Mixed into both classes and instances to provide easy access to the column names
199 module Columns
199 module Columns
200 def left_column_name
200 def left_column_name
201 acts_as_nested_set_options[:left_column]
201 acts_as_nested_set_options[:left_column]
202 end
202 end
203
203
204 def right_column_name
204 def right_column_name
205 acts_as_nested_set_options[:right_column]
205 acts_as_nested_set_options[:right_column]
206 end
206 end
207
207
208 def parent_column_name
208 def parent_column_name
209 acts_as_nested_set_options[:parent_column]
209 acts_as_nested_set_options[:parent_column]
210 end
210 end
211
211
212 def scope_column_names
212 def scope_column_names
213 Array(acts_as_nested_set_options[:scope])
213 Array(acts_as_nested_set_options[:scope])
214 end
214 end
215
215
216 def quoted_left_column_name
216 def quoted_left_column_name
217 connection.quote_column_name(left_column_name)
217 connection.quote_column_name(left_column_name)
218 end
218 end
219
219
220 def quoted_right_column_name
220 def quoted_right_column_name
221 connection.quote_column_name(right_column_name)
221 connection.quote_column_name(right_column_name)
222 end
222 end
223
223
224 def quoted_parent_column_name
224 def quoted_parent_column_name
225 connection.quote_column_name(parent_column_name)
225 connection.quote_column_name(parent_column_name)
226 end
226 end
227
227
228 def quoted_scope_column_names
228 def quoted_scope_column_names
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
230 end
230 end
231 end
231 end
232
232
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
234 #
234 #
235 # category.self_and_descendants.count
235 # category.self_and_descendants.count
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
237 module InstanceMethods
237 module InstanceMethods
238 # Value of the parent column
238 # Value of the parent column
239 def parent_id
239 def parent_id
240 self[parent_column_name]
240 self[parent_column_name]
241 end
241 end
242
242
243 # Value of the left column
243 # Value of the left column
244 def left
244 def left
245 self[left_column_name]
245 self[left_column_name]
246 end
246 end
247
247
248 # Value of the right column
248 # Value of the right column
249 def right
249 def right
250 self[right_column_name]
250 self[right_column_name]
251 end
251 end
252
252
253 # Returns true if this is a root node.
253 # Returns true if this is a root node.
254 def root?
254 def root?
255 parent_id.nil?
255 parent_id.nil?
256 end
256 end
257
257
258 def leaf?
258 def leaf?
259 new_record? || (right - left == 1)
259 new_record? || (right - left == 1)
260 end
260 end
261
261
262 # Returns true is this is a child node
262 # Returns true is this is a child node
263 def child?
263 def child?
264 !parent_id.nil?
264 !parent_id.nil?
265 end
265 end
266
266
267 # order by left column
267 # order by left column
268 def <=>(x)
268 def <=>(x)
269 left <=> x.left
269 left <=> x.left
270 end
270 end
271
271
272 # Redefine to act like active record
272 # Redefine to act like active record
273 def ==(comparison_object)
273 def ==(comparison_object)
274 comparison_object.equal?(self) ||
274 comparison_object.equal?(self) ||
275 (comparison_object.instance_of?(self.class) &&
275 (comparison_object.instance_of?(self.class) &&
276 comparison_object.id == id &&
276 comparison_object.id == id &&
277 !comparison_object.new_record?)
277 !comparison_object.new_record?)
278 end
278 end
279
279
280 # Returns root
280 # Returns root
281 def root
281 def root
282 self_and_ancestors.find(:first)
282 self_and_ancestors.find(:first)
283 end
283 end
284
284
285 # Returns the immediate parent
285 # Returns the immediate parent
286 def parent
286 def parent
287 nested_set_scope.find_by_id(parent_id) if parent_id
287 nested_set_scope.find_by_id(parent_id) if parent_id
288 end
288 end
289
289
290 # Returns the array of all parents and self
290 # Returns the array of all parents and self
291 def self_and_ancestors
291 def self_and_ancestors
292 nested_set_scope.scoped :conditions => [
292 nested_set_scope.scoped :conditions => [
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
294 ]
294 ]
295 end
295 end
296
296
297 # Returns an array of all parents
297 # Returns an array of all parents
298 def ancestors
298 def ancestors
299 without_self self_and_ancestors
299 without_self self_and_ancestors
300 end
300 end
301
301
302 # Returns the array of all children of the parent, including self
302 # Returns the array of all children of the parent, including self
303 def self_and_siblings
303 def self_and_siblings
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
305 end
305 end
306
306
307 # Returns the array of all children of the parent, except self
307 # Returns the array of all children of the parent, except self
308 def siblings
308 def siblings
309 without_self self_and_siblings
309 without_self self_and_siblings
310 end
310 end
311
311
312 # Returns a set of all of its nested children which do not have children
312 # Returns a set of all of its nested children which do not have children
313 def leaves
313 def leaves
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
315 end
315 end
316
316
317 # Returns the level of this object in the tree
317 # Returns the level of this object in the tree
318 # root level is 0
318 # root level is 0
319 def level
319 def level
320 parent_id.nil? ? 0 : ancestors.count
320 parent_id.nil? ? 0 : ancestors.count
321 end
321 end
322
322
323 # Returns a set of itself and all of its nested children
323 # Returns a set of itself and all of its nested children
324 def self_and_descendants
324 def self_and_descendants
325 nested_set_scope.scoped :conditions => [
325 nested_set_scope.scoped :conditions => [
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
327 ]
327 ]
328 end
328 end
329
329
330 # Returns a set of all of its children and nested children
330 # Returns a set of all of its children and nested children
331 def descendants
331 def descendants
332 without_self self_and_descendants
332 without_self self_and_descendants
333 end
333 end
334
334
335 # Returns a set of only this entry's immediate children
335 # Returns a set of only this entry's immediate children
336 def children
336 def children
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
338 end
338 end
339
339
340 def is_descendant_of?(other)
340 def is_descendant_of?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
342 end
342 end
343
343
344 def is_or_is_descendant_of?(other)
344 def is_or_is_descendant_of?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
346 end
346 end
347
347
348 def is_ancestor_of?(other)
348 def is_ancestor_of?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
350 end
350 end
351
351
352 def is_or_is_ancestor_of?(other)
352 def is_or_is_ancestor_of?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
354 end
354 end
355
355
356 # Check if other model is in the same scope
356 # Check if other model is in the same scope
357 def same_scope?(other)
357 def same_scope?(other)
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
359 self.send(attr) == other.send(attr)
359 self.send(attr) == other.send(attr)
360 end
360 end
361 end
361 end
362
362
363 # Find the first sibling to the left
363 # Find the first sibling to the left
364 def left_sibling
364 def left_sibling
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
367 end
367 end
368
368
369 # Find the first sibling to the right
369 # Find the first sibling to the right
370 def right_sibling
370 def right_sibling
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
372 end
372 end
373
373
374 # Shorthand method for finding the left sibling and moving to the left of it.
374 # Shorthand method for finding the left sibling and moving to the left of it.
375 def move_left
375 def move_left
376 move_to_left_of left_sibling
376 move_to_left_of left_sibling
377 end
377 end
378
378
379 # Shorthand method for finding the right sibling and moving to the right of it.
379 # Shorthand method for finding the right sibling and moving to the right of it.
380 def move_right
380 def move_right
381 move_to_right_of right_sibling
381 move_to_right_of right_sibling
382 end
382 end
383
383
384 # Move the node to the left of another node (you can pass id only)
384 # Move the node to the left of another node (you can pass id only)
385 def move_to_left_of(node)
385 def move_to_left_of(node)
386 move_to node, :left
386 move_to node, :left
387 end
387 end
388
388
389 # Move the node to the left of another node (you can pass id only)
389 # Move the node to the left of another node (you can pass id only)
390 def move_to_right_of(node)
390 def move_to_right_of(node)
391 move_to node, :right
391 move_to node, :right
392 end
392 end
393
393
394 # Move the node to the child of another node (you can pass id only)
394 # Move the node to the child of another node (you can pass id only)
395 def move_to_child_of(node)
395 def move_to_child_of(node)
396 move_to node, :child
396 move_to node, :child
397 end
397 end
398
398
399 # Move the node to root nodes
399 # Move the node to root nodes
400 def move_to_root
400 def move_to_root
401 move_to nil, :root
401 move_to nil, :root
402 end
402 end
403
403
404 def move_possible?(target)
404 def move_possible?(target)
405 self != target && # Can't target self
405 self != target && # Can't target self
406 same_scope?(target) && # can't be in different scopes
406 same_scope?(target) && # can't be in different scopes
407 # !(left..right).include?(target.left..target.right) # this needs tested more
407 # !(left..right).include?(target.left..target.right) # this needs tested more
408 # detect impossible move
408 # detect impossible move
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
410 end
410 end
411
411
412 def to_text
412 def to_text
413 self_and_descendants.map do |node|
413 self_and_descendants.map do |node|
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
415 end.join("\n")
415 end.join("\n")
416 end
416 end
417
417
418 protected
418 protected
419
419
420 def without_self(scope)
420 def without_self(scope)
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
422 end
422 end
423
423
424 # All nested set queries should use this nested_set_scope, which performs finds on
424 # All nested set queries should use this nested_set_scope, which performs finds on
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
426 # declaration.
426 # declaration.
427 def nested_set_scope
427 def nested_set_scope
428 options = {:order => "#{self.class.table_name}.#{quoted_left_column_name}"}
428 options = {:order => "#{self.class.table_name}.#{quoted_left_column_name}"}
429 scopes = Array(acts_as_nested_set_options[:scope])
429 scopes = Array(acts_as_nested_set_options[:scope])
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
431 conditions.merge attr => self[attr]
431 conditions.merge attr => self[attr]
432 end unless scopes.empty?
432 end unless scopes.empty?
433 self.class.base_class.scoped options
433 self.class.base_class.scoped options
434 end
434 end
435
435
436 # on creation, set automatically lft and rgt to the end of the tree
436 # on creation, set automatically lft and rgt to the end of the tree
437 def set_default_left_and_right
437 def set_default_left_and_right
438 maxright = nested_set_scope.maximum(right_column_name) || 0
438 maxright = nested_set_scope.maximum(right_column_name) || 0
439 # adds the new node to the right of all existing nodes
439 # adds the new node to the right of all existing nodes
440 self[left_column_name] = maxright + 1
440 self[left_column_name] = maxright + 1
441 self[right_column_name] = maxright + 2
441 self[right_column_name] = maxright + 2
442 end
442 end
443
443
444 # Prunes a branch off of the tree, shifting all of the elements on the right
444 # Prunes a branch off of the tree, shifting all of the elements on the right
445 # back to the left so the counts still work.
445 # back to the left so the counts still work.
446 def prune_from_tree
446 def prune_from_tree
447 return if right.nil? || left.nil? || leaf? || !self.class.exists?(id)
447 return if right.nil? || left.nil? || !self.class.exists?(id)
448
448
449 self.class.base_class.transaction do
449 self.class.base_class.transaction do
450 reload_nested_set
450 reload_nested_set
451 if acts_as_nested_set_options[:dependent] == :destroy
451 if acts_as_nested_set_options[:dependent] == :destroy
452 children.each(&:destroy)
452 children.each(&:destroy)
453 else
453 else
454 nested_set_scope.send(:delete_all,
454 nested_set_scope.send(:delete_all,
455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 left, right]
456 left, right]
457 )
457 )
458 end
458 end
459 reload_nested_set
459 reload_nested_set
460 diff = right - left + 1
460 diff = right - left + 1
461 nested_set_scope.update_all(
461 nested_set_scope.update_all(
462 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
462 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
463 ["#{quoted_left_column_name} >= ?", right]
463 ["#{quoted_left_column_name} >= ?", right]
464 )
464 )
465 nested_set_scope.update_all(
465 nested_set_scope.update_all(
466 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
466 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
467 ["#{quoted_right_column_name} >= ?", right]
467 ["#{quoted_right_column_name} >= ?", right]
468 )
468 )
469 end
469 end
470
470
471 # Reload is needed because children may have updated their parent (self) during deletion.
471 # Reload is needed because children may have updated their parent (self) during deletion.
472 reload
472 reload
473 end
473 end
474
474
475 # reload left, right, and parent
475 # reload left, right, and parent
476 def reload_nested_set
476 def reload_nested_set
477 reload(:select => "#{quoted_left_column_name}, " +
477 reload(:select => "#{quoted_left_column_name}, " +
478 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
478 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
479 end
479 end
480
480
481 def move_to(target, position)
481 def move_to(target, position)
482 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
482 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
483 return if callback(:before_move) == false
483 return if callback(:before_move) == false
484 transaction do
484 transaction do
485 if target.is_a? self.class.base_class
485 if target.is_a? self.class.base_class
486 target.reload_nested_set
486 target.reload_nested_set
487 elsif position != :root
487 elsif position != :root
488 # load object if node is not an object
488 # load object if node is not an object
489 target = nested_set_scope.find(target)
489 target = nested_set_scope.find(target)
490 end
490 end
491 self.reload_nested_set
491 self.reload_nested_set
492
492
493 unless position == :root || move_possible?(target)
493 unless position == :root || move_possible?(target)
494 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
494 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
495 end
495 end
496
496
497 bound = case position
497 bound = case position
498 when :child; target[right_column_name]
498 when :child; target[right_column_name]
499 when :left; target[left_column_name]
499 when :left; target[left_column_name]
500 when :right; target[right_column_name] + 1
500 when :right; target[right_column_name] + 1
501 when :root; 1
501 when :root; 1
502 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
502 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
503 end
503 end
504
504
505 if bound > self[right_column_name]
505 if bound > self[right_column_name]
506 bound = bound - 1
506 bound = bound - 1
507 other_bound = self[right_column_name] + 1
507 other_bound = self[right_column_name] + 1
508 else
508 else
509 other_bound = self[left_column_name] - 1
509 other_bound = self[left_column_name] - 1
510 end
510 end
511
511
512 # there would be no change
512 # there would be no change
513 return if bound == self[right_column_name] || bound == self[left_column_name]
513 return if bound == self[right_column_name] || bound == self[left_column_name]
514
514
515 # we have defined the boundaries of two non-overlapping intervals,
515 # we have defined the boundaries of two non-overlapping intervals,
516 # so sorting puts both the intervals and their boundaries in order
516 # so sorting puts both the intervals and their boundaries in order
517 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
517 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
518
518
519 new_parent = case position
519 new_parent = case position
520 when :child; target.id
520 when :child; target.id
521 when :root; nil
521 when :root; nil
522 else target[parent_column_name]
522 else target[parent_column_name]
523 end
523 end
524
524
525 self.class.base_class.update_all([
525 self.class.base_class.update_all([
526 "#{quoted_left_column_name} = CASE " +
526 "#{quoted_left_column_name} = CASE " +
527 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
527 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
528 "THEN #{quoted_left_column_name} + :d - :b " +
528 "THEN #{quoted_left_column_name} + :d - :b " +
529 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
529 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
530 "THEN #{quoted_left_column_name} + :a - :c " +
530 "THEN #{quoted_left_column_name} + :a - :c " +
531 "ELSE #{quoted_left_column_name} END, " +
531 "ELSE #{quoted_left_column_name} END, " +
532 "#{quoted_right_column_name} = CASE " +
532 "#{quoted_right_column_name} = CASE " +
533 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
533 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
534 "THEN #{quoted_right_column_name} + :d - :b " +
534 "THEN #{quoted_right_column_name} + :d - :b " +
535 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
535 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
536 "THEN #{quoted_right_column_name} + :a - :c " +
536 "THEN #{quoted_right_column_name} + :a - :c " +
537 "ELSE #{quoted_right_column_name} END, " +
537 "ELSE #{quoted_right_column_name} END, " +
538 "#{quoted_parent_column_name} = CASE " +
538 "#{quoted_parent_column_name} = CASE " +
539 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
539 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
540 "ELSE #{quoted_parent_column_name} END",
540 "ELSE #{quoted_parent_column_name} END",
541 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
541 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
542 ], nested_set_scope.proxy_options[:conditions])
542 ], nested_set_scope.proxy_options[:conditions])
543 end
543 end
544 target.reload_nested_set if target
544 target.reload_nested_set if target
545 self.reload_nested_set
545 self.reload_nested_set
546 callback(:after_move)
546 callback(:after_move)
547 end
547 end
548
548
549 end
549 end
550
550
551 end
551 end
552 end
552 end
553 end
553 end
General Comments 0
You need to be logged in to leave comments. Login now