##// END OF EJS Templates
Merged r6311 from trunk (#8880)....
Jean-Philippe Lang -
r7649:21910aa42869
parent child
Show More
@@ -1,366 +1,380
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, :subject => 'child', :parent_issue_id => issue.id)
55 55 assert !child.save
56 56 assert_not_nil child.errors.on(:parent_issue_id)
57 57 end
58 58
59 59 def test_move_a_root_to_child
60 60 parent1 = create_issue!
61 61 parent2 = create_issue!
62 62 child = create_issue!(:parent_issue_id => parent1.id)
63 63
64 64 parent2.parent_issue_id = parent1.id
65 65 parent2.save!
66 66 child.reload
67 67 parent1.reload
68 68 parent2.reload
69 69
70 70 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
71 71 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
72 72 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
73 73 end
74 74
75 75 def test_move_a_child_to_root
76 76 parent1 = create_issue!
77 77 parent2 = create_issue!
78 78 child = create_issue!(:parent_issue_id => parent1.id)
79 79
80 80 child.parent_issue_id = nil
81 81 child.save!
82 82 child.reload
83 83 parent1.reload
84 84 parent2.reload
85 85
86 86 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
87 87 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
88 88 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
89 89 end
90 90
91 91 def test_move_a_child_to_another_issue
92 92 parent1 = create_issue!
93 93 parent2 = create_issue!
94 94 child = create_issue!(:parent_issue_id => parent1.id)
95 95
96 96 child.parent_issue_id = parent2.id
97 97 child.save!
98 98 child.reload
99 99 parent1.reload
100 100 parent2.reload
101 101
102 102 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
103 103 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
104 104 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
105 105 end
106 106
107 107 def test_move_a_child_with_descendants_to_another_issue
108 108 parent1 = create_issue!
109 109 parent2 = create_issue!
110 110 child = create_issue!(:parent_issue_id => parent1.id)
111 111 grandchild = create_issue!(:parent_issue_id => child.id)
112 112
113 113 parent1.reload
114 114 parent2.reload
115 115 child.reload
116 116 grandchild.reload
117 117
118 118 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
119 119 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
120 120 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
121 121 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
122 122
123 123 child.reload.parent_issue_id = parent2.id
124 124 child.save!
125 125 child.reload
126 126 grandchild.reload
127 127 parent1.reload
128 128 parent2.reload
129 129
130 130 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
131 131 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
132 132 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
133 133 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
134 134 end
135 135
136 136 def test_move_a_child_with_descendants_to_another_project
137 137 parent1 = create_issue!
138 138 child = create_issue!(:parent_issue_id => parent1.id)
139 139 grandchild = create_issue!(:parent_issue_id => child.id)
140 140
141 141 assert child.reload.move_to_project(Project.find(2))
142 142 child.reload
143 143 grandchild.reload
144 144 parent1.reload
145 145
146 146 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
147 147 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
148 148 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
149 149 end
150 150
151 151 def test_invalid_move_to_another_project
152 152 parent1 = create_issue!
153 153 child = create_issue!(:parent_issue_id => parent1.id)
154 154 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
155 155 Project.find(2).tracker_ids = [1]
156 156
157 157 parent1.reload
158 158 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
159 159
160 160 # child can not be moved to Project 2 because its child is on a disabled tracker
161 161 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
162 162 child.reload
163 163 grandchild.reload
164 164 parent1.reload
165 165
166 166 # no change
167 167 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
168 168 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
169 169 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
170 170 end
171 171
172 172 def test_moving_an_issue_to_a_descendant_should_not_validate
173 173 parent1 = create_issue!
174 174 parent2 = create_issue!
175 175 child = create_issue!(:parent_issue_id => parent1.id)
176 176 grandchild = create_issue!(:parent_issue_id => child.id)
177 177
178 178 child.reload
179 179 child.parent_issue_id = grandchild.id
180 180 assert !child.save
181 181 assert_not_nil child.errors.on(:parent_issue_id)
182 182 end
183 183
184 184 def test_moving_an_issue_should_keep_valid_relations_only
185 185 issue1 = create_issue!
186 186 issue2 = create_issue!
187 187 issue3 = create_issue!(:parent_issue_id => issue2.id)
188 188 issue4 = create_issue!
189 189 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
190 190 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
191 191 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
192 192 issue2.reload
193 193 issue2.parent_issue_id = issue1.id
194 194 issue2.save!
195 195 assert !IssueRelation.exists?(r1.id)
196 196 assert !IssueRelation.exists?(r2.id)
197 197 assert IssueRelation.exists?(r3.id)
198 198 end
199 199
200 200 def test_destroy_should_destroy_children
201 201 issue1 = create_issue!
202 202 issue2 = create_issue!
203 203 issue3 = create_issue!(:parent_issue_id => issue2.id)
204 204 issue4 = create_issue!(:parent_issue_id => issue1.id)
205 205
206 206 issue3.init_journal(User.find(2))
207 207 issue3.subject = 'child with journal'
208 208 issue3.save!
209 209
210 210 assert_difference 'Issue.count', -2 do
211 211 assert_difference 'Journal.count', -1 do
212 212 assert_difference 'JournalDetail.count', -1 do
213 213 Issue.find(issue2.id).destroy
214 214 end
215 215 end
216 216 end
217 217
218 218 issue1.reload
219 219 issue4.reload
220 220 assert !Issue.exists?(issue2.id)
221 221 assert !Issue.exists?(issue3.id)
222 222 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
223 223 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
224 224 end
225 225
226 226 def test_destroy_parent_issue_updated_during_children_destroy
227 227 parent = create_issue!
228 228 create_issue!(:start_date => Date.today, :parent_issue_id => parent.id)
229 229 create_issue!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
230 230
231 231 assert_difference 'Issue.count', -3 do
232 232 Issue.find(parent.id).destroy
233 233 end
234 234 end
235 235
236 236 def test_destroy_child_issue_with_children
237 237 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
238 238 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
239 239 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
240 240 leaf.init_journal(User.find(2))
241 241 leaf.subject = 'leaf with journal'
242 242 leaf.save!
243 243
244 244 assert_difference 'Issue.count', -2 do
245 245 assert_difference 'Journal.count', -1 do
246 246 assert_difference 'JournalDetail.count', -1 do
247 247 Issue.find(child.id).destroy
248 248 end
249 249 end
250 250 end
251 251
252 252 root = Issue.find(root.id)
253 253 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
254 254 end
255
256 def test_destroy_issue_with_grand_child
257 parent = create_issue!
258 issue = create_issue!(:parent_issue_id => parent.id)
259 child = create_issue!(:parent_issue_id => issue.id)
260 grandchild1 = create_issue!(:parent_issue_id => child.id)
261 grandchild2 = create_issue!(:parent_issue_id => child.id)
262
263 assert_difference 'Issue.count', -4 do
264 Issue.find(issue.id).destroy
265 parent.reload
266 assert_equal [1, 2], [parent.lft, parent.rgt]
267 end
268 end
255 269
256 270 def test_parent_priority_should_be_the_highest_child_priority
257 271 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
258 272 # Create children
259 273 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
260 274 assert_equal 'High', parent.reload.priority.name
261 275 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
262 276 assert_equal 'Immediate', child1.reload.priority.name
263 277 assert_equal 'Immediate', parent.reload.priority.name
264 278 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
265 279 assert_equal 'Immediate', parent.reload.priority.name
266 280 # Destroy a child
267 281 child1.destroy
268 282 assert_equal 'Low', parent.reload.priority.name
269 283 # Update a child
270 284 child3.reload.priority = IssuePriority.find_by_name('Normal')
271 285 child3.save!
272 286 assert_equal 'Normal', parent.reload.priority.name
273 287 end
274 288
275 289 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
276 290 parent = create_issue!
277 291 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
278 292 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
279 293 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
280 294 parent.reload
281 295 assert_equal Date.parse('2010-01-25'), parent.start_date
282 296 assert_equal Date.parse('2010-02-22'), parent.due_date
283 297 end
284 298
285 299 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
286 300 parent = create_issue!
287 301 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
288 302 assert_equal 20, parent.reload.done_ratio
289 303 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
290 304 assert_equal 45, parent.reload.done_ratio
291 305
292 306 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
293 307 assert_equal 30, parent.reload.done_ratio
294 308
295 309 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
296 310 assert_equal 30, child.reload.done_ratio
297 311 assert_equal 40, parent.reload.done_ratio
298 312 end
299 313
300 314 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
301 315 parent = create_issue!
302 316 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
303 317 assert_equal 20, parent.reload.done_ratio
304 318 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
305 319 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
306 320 end
307 321
308 322 def test_parent_estimate_should_be_sum_of_leaves
309 323 parent = create_issue!
310 324 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
311 325 assert_equal nil, parent.reload.estimated_hours
312 326 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
313 327 assert_equal 5, parent.reload.estimated_hours
314 328 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
315 329 assert_equal 12, parent.reload.estimated_hours
316 330 end
317 331
318 332 def test_move_parent_updates_old_parent_attributes
319 333 first_parent = create_issue!
320 334 second_parent = create_issue!
321 335 child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
322 336 assert_equal 5, first_parent.reload.estimated_hours
323 337 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
324 338 assert_equal 7, second_parent.reload.estimated_hours
325 339 assert_nil first_parent.reload.estimated_hours
326 340 end
327 341
328 342 def test_reschuling_a_parent_should_reschedule_subtasks
329 343 parent = create_issue!
330 344 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
331 345 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
332 346 parent.reload
333 347 parent.reschedule_after(Date.parse('2010-06-02'))
334 348 c1.reload
335 349 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
336 350 c2.reload
337 351 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
338 352 parent.reload
339 353 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
340 354 end
341 355
342 356 def test_project_copy_should_copy_issue_tree
343 357 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
344 358 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
345 359 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
346 360 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
347 361 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
348 362 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
349 363 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
350 364 c.copy(p, :only => 'issues')
351 365 c.reload
352 366
353 367 assert_equal 5, c.issues.count
354 368 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
355 369 assert ic1.root?
356 370 assert_equal ic1, ic2.parent
357 371 assert_equal ic1, ic3.parent
358 372 assert_equal ic2, ic4.parent
359 373 assert ic5.root?
360 374 end
361 375
362 376 # Helper that creates an issue with default attributes
363 377 def create_issue!(attributes={})
364 378 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
365 379 end
366 380 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 447 return if right.nil? || left.nil? || leaf? || !self.class.exists?(id)
448 448
449 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
450 :destroy_all : :delete_all
451
452 # TODO: should destroy children (not descendants) when deleted_method is :destroy_all
453 449 self.class.base_class.transaction do
454 450 reload_nested_set
455 nested_set_scope.send(delete_method,
456 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
457 left, right]
458 )
451 if acts_as_nested_set_options[:dependent] == :destroy
452 children.each(&:destroy)
453 else
454 nested_set_scope.send(:delete_all,
455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 left, right]
457 )
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