##// END OF EJS Templates
Fixed: deleting a parent issue may lead to a stale object error (#7920)....
Jean-Philippe Lang -
r5165:6550ef9df55f
parent child
Show More
@@ -1,356 +1,366
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 def test_destroy_parent_issue_updated_during_children_destroy
227 parent = create_issue!
228 create_issue!(:start_date => Date.today, :parent_issue_id => parent.id)
229 create_issue!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
230
231 assert_difference 'Issue.count', -3 do
232 Issue.find(parent.id).destroy
233 end
234 end
235
226 236 def test_destroy_child_issue_with_children
227 237 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
228 238 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
229 239 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
230 240 leaf.init_journal(User.find(2))
231 241 leaf.subject = 'leaf with journal'
232 242 leaf.save!
233 243
234 244 assert_difference 'Issue.count', -2 do
235 245 assert_difference 'Journal.count', -1 do
236 246 assert_difference 'JournalDetail.count', -1 do
237 247 Issue.find(child.id).destroy
238 248 end
239 249 end
240 250 end
241 251
242 252 root = Issue.find(root.id)
243 253 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
244 254 end
245 255
246 256 def test_parent_priority_should_be_the_highest_child_priority
247 257 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
248 258 # Create children
249 259 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
250 260 assert_equal 'High', parent.reload.priority.name
251 261 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
252 262 assert_equal 'Immediate', child1.reload.priority.name
253 263 assert_equal 'Immediate', parent.reload.priority.name
254 264 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
255 265 assert_equal 'Immediate', parent.reload.priority.name
256 266 # Destroy a child
257 267 child1.destroy
258 268 assert_equal 'Low', parent.reload.priority.name
259 269 # Update a child
260 270 child3.reload.priority = IssuePriority.find_by_name('Normal')
261 271 child3.save!
262 272 assert_equal 'Normal', parent.reload.priority.name
263 273 end
264 274
265 275 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
266 276 parent = create_issue!
267 277 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
268 278 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
269 279 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
270 280 parent.reload
271 281 assert_equal Date.parse('2010-01-25'), parent.start_date
272 282 assert_equal Date.parse('2010-02-22'), parent.due_date
273 283 end
274 284
275 285 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
276 286 parent = create_issue!
277 287 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
278 288 assert_equal 20, parent.reload.done_ratio
279 289 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
280 290 assert_equal 45, parent.reload.done_ratio
281 291
282 292 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
283 293 assert_equal 30, parent.reload.done_ratio
284 294
285 295 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
286 296 assert_equal 30, child.reload.done_ratio
287 297 assert_equal 40, parent.reload.done_ratio
288 298 end
289 299
290 300 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
291 301 parent = create_issue!
292 302 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
293 303 assert_equal 20, parent.reload.done_ratio
294 304 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
295 305 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
296 306 end
297 307
298 308 def test_parent_estimate_should_be_sum_of_leaves
299 309 parent = create_issue!
300 310 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
301 311 assert_equal nil, parent.reload.estimated_hours
302 312 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
303 313 assert_equal 5, parent.reload.estimated_hours
304 314 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
305 315 assert_equal 12, parent.reload.estimated_hours
306 316 end
307 317
308 318 def test_move_parent_updates_old_parent_attributes
309 319 first_parent = create_issue!
310 320 second_parent = create_issue!
311 321 child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
312 322 assert_equal 5, first_parent.reload.estimated_hours
313 323 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
314 324 assert_equal 7, second_parent.reload.estimated_hours
315 325 assert_nil first_parent.reload.estimated_hours
316 326 end
317 327
318 328 def test_reschuling_a_parent_should_reschedule_subtasks
319 329 parent = create_issue!
320 330 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
321 331 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
322 332 parent.reload
323 333 parent.reschedule_after(Date.parse('2010-06-02'))
324 334 c1.reload
325 335 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
326 336 c2.reload
327 337 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
328 338 parent.reload
329 339 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
330 340 end
331 341
332 342 def test_project_copy_should_copy_issue_tree
333 343 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
334 344 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
335 345 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
336 346 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
337 347 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
338 348 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
339 349 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
340 350 c.copy(p, :only => 'issues')
341 351 c.reload
342 352
343 353 assert_equal 5, c.issues.count
344 354 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
345 355 assert ic1.root?
346 356 assert_equal ic1, ic2.parent
347 357 assert_equal ic1, ic3.parent
348 358 assert_equal ic2, ic4.parent
349 359 assert ic5.root?
350 360 end
351 361
352 362 # Helper that creates an issue with default attributes
353 363 def create_issue!(attributes={})
354 364 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
355 365 end
356 366 end
@@ -1,549 +1,552
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 => 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? || !self.class.exists?(id)
448 448
449 449 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
450 450 :destroy_all : :delete_all
451 451
452 452 self.class.base_class.transaction do
453 453 reload_nested_set
454 454 nested_set_scope.send(delete_method,
455 455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 456 left, right]
457 457 )
458 458 reload_nested_set
459 459 diff = right - left + 1
460 460 nested_set_scope.update_all(
461 461 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
462 462 ["#{quoted_left_column_name} >= ?", right]
463 463 )
464 464 nested_set_scope.update_all(
465 465 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
466 466 ["#{quoted_right_column_name} >= ?", right]
467 467 )
468 468 end
469
470 # Reload is needed because children may have updated their parent (self) during deletion.
471 reload
469 472 end
470 473
471 474 # reload left, right, and parent
472 475 def reload_nested_set
473 476 reload(:select => "#{quoted_left_column_name}, " +
474 477 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
475 478 end
476 479
477 480 def move_to(target, position)
478 481 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
479 482 return if callback(:before_move) == false
480 483 transaction do
481 484 if target.is_a? self.class.base_class
482 485 target.reload_nested_set
483 486 elsif position != :root
484 487 # load object if node is not an object
485 488 target = nested_set_scope.find(target)
486 489 end
487 490 self.reload_nested_set
488 491
489 492 unless position == :root || move_possible?(target)
490 493 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
491 494 end
492 495
493 496 bound = case position
494 497 when :child; target[right_column_name]
495 498 when :left; target[left_column_name]
496 499 when :right; target[right_column_name] + 1
497 500 when :root; 1
498 501 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
499 502 end
500 503
501 504 if bound > self[right_column_name]
502 505 bound = bound - 1
503 506 other_bound = self[right_column_name] + 1
504 507 else
505 508 other_bound = self[left_column_name] - 1
506 509 end
507 510
508 511 # there would be no change
509 512 return if bound == self[right_column_name] || bound == self[left_column_name]
510 513
511 514 # we have defined the boundaries of two non-overlapping intervals,
512 515 # so sorting puts both the intervals and their boundaries in order
513 516 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
514 517
515 518 new_parent = case position
516 519 when :child; target.id
517 520 when :root; nil
518 521 else target[parent_column_name]
519 522 end
520 523
521 524 self.class.base_class.update_all([
522 525 "#{quoted_left_column_name} = CASE " +
523 526 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
524 527 "THEN #{quoted_left_column_name} + :d - :b " +
525 528 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
526 529 "THEN #{quoted_left_column_name} + :a - :c " +
527 530 "ELSE #{quoted_left_column_name} END, " +
528 531 "#{quoted_right_column_name} = CASE " +
529 532 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
530 533 "THEN #{quoted_right_column_name} + :d - :b " +
531 534 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
532 535 "THEN #{quoted_right_column_name} + :a - :c " +
533 536 "ELSE #{quoted_right_column_name} END, " +
534 537 "#{quoted_parent_column_name} = CASE " +
535 538 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
536 539 "ELSE #{quoted_parent_column_name} END",
537 540 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
538 541 ], nested_set_scope.proxy_options[:conditions])
539 542 end
540 543 target.reload_nested_set if target
541 544 self.reload_nested_set
542 545 callback(:after_move)
543 546 end
544 547
545 548 end
546 549
547 550 end
548 551 end
549 552 end
General Comments 0
You need to be logged in to leave comments. Login now