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