##// END OF EJS Templates
Merged r10865 and r10866 from trunk (#12431)....
Jean-Philippe Lang -
r10646:bb4c530ba29c
parent child
Show More
@@ -1,601 +1,601
1 1 module CollectiveIdea #:nodoc:
2 2 module Acts #:nodoc:
3 3 module NestedSet #:nodoc:
4 4
5 5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
6 6 # an _ordered_ tree, with the added feature that you can select the children and all of their
7 7 # descendants with a single query. The drawback is that insertion or move need some complex
8 8 # sql queries. But everything is done here by this module!
9 9 #
10 10 # Nested sets are appropriate each time you want either an orderd tree (menus,
11 11 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 12 #
13 13 # == API
14 14 #
15 15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 16 # by another easier.
17 17 #
18 18 # item.children.create(:name => "child1")
19 19 #
20 20
21 21 # Configuration options are:
22 22 #
23 23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 24 # * +:left_column+ - column name for left boundry data, default "lft"
25 25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 26 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 27 # (if it hasn't been already) and use that as the foreign key restriction. You
28 28 # can also pass an array to scope by multiple attributes.
29 29 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 30 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 31 # child objects are destroyed alongside this object by calling their destroy
32 32 # method. If set to :delete_all (default), all the child objects are deleted
33 33 # without calling their destroy method.
34 34 # * +:counter_cache+ adds a counter cache for the number of children.
35 35 # defaults to false.
36 36 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 37 #
38 38 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
39 39 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
40 40 # to acts_as_nested_set models
41 41 def acts_as_nested_set(options = {})
42 42 options = {
43 43 :parent_column => 'parent_id',
44 44 :left_column => 'lft',
45 45 :right_column => 'rgt',
46 46 :dependent => :delete_all, # or :destroy
47 47 :counter_cache => false,
48 48 :order => 'id'
49 49 }.merge(options)
50 50
51 51 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
52 52 options[:scope] = "#{options[:scope]}_id".intern
53 53 end
54 54
55 55 class_attribute :acts_as_nested_set_options
56 56 self.acts_as_nested_set_options = options
57 57
58 58 include CollectiveIdea::Acts::NestedSet::Model
59 59 include Columns
60 60 extend Columns
61 61
62 62 belongs_to :parent, :class_name => self.base_class.to_s,
63 63 :foreign_key => parent_column_name,
64 64 :counter_cache => options[:counter_cache],
65 65 :inverse_of => :children
66 66 has_many :children, :class_name => self.base_class.to_s,
67 67 :foreign_key => parent_column_name, :order => left_column_name,
68 68 :inverse_of => :parent,
69 69 :before_add => options[:before_add],
70 70 :after_add => options[:after_add],
71 71 :before_remove => options[:before_remove],
72 72 :after_remove => options[:after_remove]
73 73
74 74 attr_accessor :skip_before_destroy
75 75
76 76 before_create :set_default_left_and_right
77 77 before_save :store_new_parent
78 78 after_save :move_to_new_parent
79 79 before_destroy :destroy_descendants
80 80
81 81 # no assignment to structure fields
82 82 [left_column_name, right_column_name].each do |column|
83 83 module_eval <<-"end_eval", __FILE__, __LINE__
84 84 def #{column}=(x)
85 85 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
86 86 end
87 87 end_eval
88 88 end
89 89
90 90 define_model_callbacks :move
91 91 end
92 92
93 93 module Model
94 94 extend ActiveSupport::Concern
95 95
96 96 module ClassMethods
97 97 # Returns the first root
98 98 def root
99 99 roots.first
100 100 end
101 101
102 102 def roots
103 103 where(parent_column_name => nil).order(quoted_left_column_name)
104 104 end
105 105
106 106 def leaves
107 107 where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
108 108 end
109 109
110 110 def valid?
111 111 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 112 end
113 113
114 114 def left_and_rights_valid?
115 115 joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
116 116 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
117 117 where(
118 118 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
119 119 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
120 120 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
121 121 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
122 122 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
123 123 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
124 124 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
125 125 ).count == 0
126 126 end
127 127
128 128 def no_duplicates_for_columns?
129 129 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
130 130 connection.quote_column_name(c)
131 131 end.push(nil).join(", ")
132 132 [quoted_left_column_name, quoted_right_column_name].all? do |column|
133 133 # No duplicates
134 134 select("#{scope_string}#{column}, COUNT(#{column})").
135 135 group("#{scope_string}#{column}").
136 136 having("COUNT(#{column}) > 1").
137 137 first.nil?
138 138 end
139 139 end
140 140
141 141 # Wrapper for each_root_valid? that can deal with scope.
142 142 def all_roots_valid?
143 143 if acts_as_nested_set_options[:scope]
144 144 roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
145 145 each_root_valid?(grouped_roots)
146 146 end
147 147 else
148 148 each_root_valid?(roots)
149 149 end
150 150 end
151 151
152 152 def each_root_valid?(roots_to_validate)
153 153 left = right = 0
154 154 roots_to_validate.all? do |root|
155 155 (root.left > left && root.right > right).tap do
156 156 left = root.left
157 157 right = root.right
158 158 end
159 159 end
160 160 end
161 161
162 162 # Rebuilds the left & rights if unset or invalid.
163 163 # Also very useful for converting from acts_as_tree.
164 164 def rebuild!(validate_nodes = true)
165 165 # Don't rebuild a valid tree.
166 166 return true if valid?
167 167
168 168 scope = lambda{|node|}
169 169 if acts_as_nested_set_options[:scope]
170 170 scope = lambda{|node|
171 171 scope_column_names.inject(""){|str, column_name|
172 172 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 173 }
174 174 }
175 175 end
176 176 indices = {}
177 177
178 178 set_left_and_rights = lambda do |node|
179 179 # set left
180 180 node[left_column_name] = indices[scope.call(node)] += 1
181 181 # find
182 182 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
183 183 # set right
184 184 node[right_column_name] = indices[scope.call(node)] += 1
185 185 node.save!(:validate => validate_nodes)
186 186 end
187 187
188 188 # Find root node(s)
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").each do |root_node|
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node|
190 190 # setup index for this scope
191 191 indices[scope.call(root_node)] ||= 0
192 192 set_left_and_rights.call(root_node)
193 193 end
194 194 end
195 195
196 196 # Iterates over tree elements and determines the current level in the tree.
197 197 # Only accepts default ordering, odering by an other column than lft
198 198 # does not work. This method is much more efficent than calling level
199 199 # because it doesn't require any additional database queries.
200 200 #
201 201 # Example:
202 202 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
203 203 #
204 204 def each_with_level(objects)
205 205 path = [nil]
206 206 objects.each do |o|
207 207 if o.parent_id != path.last
208 208 # we are on a new level, did we decent or ascent?
209 209 if path.include?(o.parent_id)
210 210 # remove wrong wrong tailing paths elements
211 211 path.pop while path.last != o.parent_id
212 212 else
213 213 path << o.parent_id
214 214 end
215 215 end
216 216 yield(o, path.length - 1)
217 217 end
218 218 end
219 219 end
220 220
221 221 # 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.
222 222 #
223 223 # category.self_and_descendants.count
224 224 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
225 225
226 226 # Value of the parent column
227 227 def parent_id
228 228 self[parent_column_name]
229 229 end
230 230
231 231 # Value of the left column
232 232 def left
233 233 self[left_column_name]
234 234 end
235 235
236 236 # Value of the right column
237 237 def right
238 238 self[right_column_name]
239 239 end
240 240
241 241 # Returns true if this is a root node.
242 242 def root?
243 243 parent_id.nil?
244 244 end
245 245
246 246 def leaf?
247 247 new_record? || (right - left == 1)
248 248 end
249 249
250 250 # Returns true is this is a child node
251 251 def child?
252 252 !parent_id.nil?
253 253 end
254 254
255 255 # Returns root
256 256 def root
257 257 self_and_ancestors.where(parent_column_name => nil).first
258 258 end
259 259
260 260 # Returns the array of all parents and self
261 261 def self_and_ancestors
262 262 nested_set_scope.where([
263 263 "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
264 264 ])
265 265 end
266 266
267 267 # Returns an array of all parents
268 268 def ancestors
269 269 without_self self_and_ancestors
270 270 end
271 271
272 272 # Returns the array of all children of the parent, including self
273 273 def self_and_siblings
274 274 nested_set_scope.where(parent_column_name => parent_id)
275 275 end
276 276
277 277 # Returns the array of all children of the parent, except self
278 278 def siblings
279 279 without_self self_and_siblings
280 280 end
281 281
282 282 # Returns a set of all of its nested children which do not have children
283 283 def leaves
284 284 descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
285 285 end
286 286
287 287 # Returns the level of this object in the tree
288 288 # root level is 0
289 289 def level
290 290 parent_id.nil? ? 0 : ancestors.count
291 291 end
292 292
293 293 # Returns a set of itself and all of its nested children
294 294 def self_and_descendants
295 295 nested_set_scope.where([
296 296 "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
297 297 ])
298 298 end
299 299
300 300 # Returns a set of all of its children and nested children
301 301 def descendants
302 302 without_self self_and_descendants
303 303 end
304 304
305 305 def is_descendant_of?(other)
306 306 other.left < self.left && self.left < other.right && same_scope?(other)
307 307 end
308 308
309 309 def is_or_is_descendant_of?(other)
310 310 other.left <= self.left && self.left < other.right && same_scope?(other)
311 311 end
312 312
313 313 def is_ancestor_of?(other)
314 314 self.left < other.left && other.left < self.right && same_scope?(other)
315 315 end
316 316
317 317 def is_or_is_ancestor_of?(other)
318 318 self.left <= other.left && other.left < self.right && same_scope?(other)
319 319 end
320 320
321 321 # Check if other model is in the same scope
322 322 def same_scope?(other)
323 323 Array(acts_as_nested_set_options[:scope]).all? do |attr|
324 324 self.send(attr) == other.send(attr)
325 325 end
326 326 end
327 327
328 328 # Find the first sibling to the left
329 329 def left_sibling
330 330 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
331 331 order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
332 332 end
333 333
334 334 # Find the first sibling to the right
335 335 def right_sibling
336 336 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
337 337 end
338 338
339 339 # Shorthand method for finding the left sibling and moving to the left of it.
340 340 def move_left
341 341 move_to_left_of left_sibling
342 342 end
343 343
344 344 # Shorthand method for finding the right sibling and moving to the right of it.
345 345 def move_right
346 346 move_to_right_of right_sibling
347 347 end
348 348
349 349 # Move the node to the left of another node (you can pass id only)
350 350 def move_to_left_of(node)
351 351 move_to node, :left
352 352 end
353 353
354 354 # Move the node to the left of another node (you can pass id only)
355 355 def move_to_right_of(node)
356 356 move_to node, :right
357 357 end
358 358
359 359 # Move the node to the child of another node (you can pass id only)
360 360 def move_to_child_of(node)
361 361 move_to node, :child
362 362 end
363 363
364 364 # Move the node to root nodes
365 365 def move_to_root
366 366 move_to nil, :root
367 367 end
368 368
369 369 def move_possible?(target)
370 370 self != target && # Can't target self
371 371 same_scope?(target) && # can't be in different scopes
372 372 # !(left..right).include?(target.left..target.right) # this needs tested more
373 373 # detect impossible move
374 374 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
375 375 end
376 376
377 377 def to_text
378 378 self_and_descendants.map do |node|
379 379 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
380 380 end.join("\n")
381 381 end
382 382
383 383 protected
384 384
385 385 def without_self(scope)
386 386 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
387 387 end
388 388
389 389 # All nested set queries should use this nested_set_scope, which performs finds on
390 390 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
391 391 # declaration.
392 392 def nested_set_scope(options = {})
393 393 options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
394 394 scopes = Array(acts_as_nested_set_options[:scope])
395 395 options[:conditions] = scopes.inject({}) do |conditions,attr|
396 396 conditions.merge attr => self[attr]
397 397 end unless scopes.empty?
398 398 self.class.base_class.scoped options
399 399 end
400 400
401 401 def store_new_parent
402 402 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
403 403 true # force callback to return true
404 404 end
405 405
406 406 def move_to_new_parent
407 407 if @move_to_new_parent_id.nil?
408 408 move_to_root
409 409 elsif @move_to_new_parent_id
410 410 move_to_child_of(@move_to_new_parent_id)
411 411 end
412 412 end
413 413
414 414 # on creation, set automatically lft and rgt to the end of the tree
415 415 def set_default_left_and_right
416 416 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
417 417 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
418 418 # adds the new node to the right of all existing nodes
419 419 self[left_column_name] = maxright + 1
420 420 self[right_column_name] = maxright + 2
421 421 end
422 422
423 423 def in_tenacious_transaction(&block)
424 424 retry_count = 0
425 425 begin
426 426 transaction(&block)
427 427 rescue ActiveRecord::StatementInvalid => error
428 428 raise unless connection.open_transactions.zero?
429 429 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
430 430 raise unless retry_count < 10
431 431 retry_count += 1
432 432 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
433 433 sleep(rand(retry_count)*0.1) # Aloha protocol
434 434 retry
435 435 end
436 436 end
437 437
438 438 # Prunes a branch off of the tree, shifting all of the elements on the right
439 439 # back to the left so the counts still work.
440 440 def destroy_descendants
441 441 return if right.nil? || left.nil? || skip_before_destroy
442 442
443 443 in_tenacious_transaction do
444 444 reload_nested_set
445 445 # select the rows in the model that extend past the deletion point and apply a lock
446 446 self.class.base_class.find(:all,
447 447 :select => "id",
448 448 :conditions => ["#{quoted_left_column_name} >= ?", left],
449 449 :lock => true
450 450 )
451 451
452 452 if acts_as_nested_set_options[:dependent] == :destroy
453 453 descendants.each do |model|
454 454 model.skip_before_destroy = true
455 455 model.destroy
456 456 end
457 457 else
458 458 nested_set_scope.delete_all(
459 459 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
460 460 left, right]
461 461 )
462 462 end
463 463
464 464 # update lefts and rights for remaining nodes
465 465 diff = right - left + 1
466 466 nested_set_scope.update_all(
467 467 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
468 468 ["#{quoted_left_column_name} > ?", right]
469 469 )
470 470 nested_set_scope.update_all(
471 471 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
472 472 ["#{quoted_right_column_name} > ?", right]
473 473 )
474 474
475 475 reload
476 476 # Don't allow multiple calls to destroy to corrupt the set
477 477 self.skip_before_destroy = true
478 478 end
479 479 end
480 480
481 481 # reload left, right, and parent
482 482 def reload_nested_set
483 483 reload(
484 484 :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
485 485 :lock => true
486 486 )
487 487 end
488 488
489 489 def move_to(target, position)
490 490 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
491 491 run_callbacks :move do
492 492 in_tenacious_transaction do
493 493 if target.is_a? self.class.base_class
494 494 target.reload_nested_set
495 495 elsif position != :root
496 496 # load object if node is not an object
497 497 target = nested_set_scope.find(target)
498 498 end
499 499 self.reload_nested_set
500 500
501 501 unless position == :root || move_possible?(target)
502 502 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
503 503 end
504 504
505 505 bound = case position
506 506 when :child; target[right_column_name]
507 507 when :left; target[left_column_name]
508 508 when :right; target[right_column_name] + 1
509 509 when :root; 1
510 510 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
511 511 end
512 512
513 513 if bound > self[right_column_name]
514 514 bound = bound - 1
515 515 other_bound = self[right_column_name] + 1
516 516 else
517 517 other_bound = self[left_column_name] - 1
518 518 end
519 519
520 520 # there would be no change
521 521 return if bound == self[right_column_name] || bound == self[left_column_name]
522 522
523 523 # we have defined the boundaries of two non-overlapping intervals,
524 524 # so sorting puts both the intervals and their boundaries in order
525 525 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
526 526
527 527 # select the rows in the model between a and d, and apply a lock
528 528 self.class.base_class.select('id').lock(true).where(
529 529 ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
530 530 )
531 531
532 532 new_parent = case position
533 533 when :child; target.id
534 534 when :root; nil
535 535 else target[parent_column_name]
536 536 end
537 537
538 538 self.nested_set_scope.update_all([
539 539 "#{quoted_left_column_name} = CASE " +
540 540 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
541 541 "THEN #{quoted_left_column_name} + :d - :b " +
542 542 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
543 543 "THEN #{quoted_left_column_name} + :a - :c " +
544 544 "ELSE #{quoted_left_column_name} END, " +
545 545 "#{quoted_right_column_name} = CASE " +
546 546 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
547 547 "THEN #{quoted_right_column_name} + :d - :b " +
548 548 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
549 549 "THEN #{quoted_right_column_name} + :a - :c " +
550 550 "ELSE #{quoted_right_column_name} END, " +
551 551 "#{quoted_parent_column_name} = CASE " +
552 552 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
553 553 "ELSE #{quoted_parent_column_name} END",
554 554 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
555 555 ])
556 556 end
557 557 target.reload_nested_set if target
558 558 self.reload_nested_set
559 559 end
560 560 end
561 561
562 562 end
563 563
564 564 # Mixed into both classes and instances to provide easy access to the column names
565 565 module Columns
566 566 def left_column_name
567 567 acts_as_nested_set_options[:left_column]
568 568 end
569 569
570 570 def right_column_name
571 571 acts_as_nested_set_options[:right_column]
572 572 end
573 573
574 574 def parent_column_name
575 575 acts_as_nested_set_options[:parent_column]
576 576 end
577 577
578 578 def scope_column_names
579 579 Array(acts_as_nested_set_options[:scope])
580 580 end
581 581
582 582 def quoted_left_column_name
583 583 connection.quote_column_name(left_column_name)
584 584 end
585 585
586 586 def quoted_right_column_name
587 587 connection.quote_column_name(right_column_name)
588 588 end
589 589
590 590 def quoted_parent_column_name
591 591 connection.quote_column_name(parent_column_name)
592 592 end
593 593
594 594 def quoted_scope_column_names
595 595 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
596 596 end
597 597 end
598 598
599 599 end
600 600 end
601 601 end
@@ -1,167 +1,174
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 ProjectNestedSetTest < ActiveSupport::TestCase
21 21
22 22 def setup
23 23 Project.delete_all
24 24
25 25 @a = Project.create!(:name => 'A', :identifier => 'projecta')
26 26 @a1 = Project.create!(:name => 'A1', :identifier => 'projecta1')
27 27 @a1.set_parent!(@a)
28 28 @a2 = Project.create!(:name => 'A2', :identifier => 'projecta2')
29 29 @a2.set_parent!(@a)
30 30
31 @c = Project.create!(:name => 'C', :identifier => 'projectc')
32 @c1 = Project.create!(:name => 'C1', :identifier => 'projectc1')
33 @c1.set_parent!(@c)
34
31 35 @b = Project.create!(:name => 'B', :identifier => 'projectb')
36 @b2 = Project.create!(:name => 'B2', :identifier => 'projectb2')
37 @b2.set_parent!(@b)
32 38 @b1 = Project.create!(:name => 'B1', :identifier => 'projectb1')
33 39 @b1.set_parent!(@b)
34 40 @b11 = Project.create!(:name => 'B11', :identifier => 'projectb11')
35 41 @b11.set_parent!(@b1)
36 @b2 = Project.create!(:name => 'B2', :identifier => 'projectb2')
37 @b2.set_parent!(@b)
38
39 @c = Project.create!(:name => 'C', :identifier => 'projectc')
40 @c1 = Project.create!(:name => 'C1', :identifier => 'projectc1')
41 @c1.set_parent!(@c)
42 42
43 43 @a, @a1, @a2, @b, @b1, @b11, @b2, @c, @c1 = *(Project.all.sort_by(&:name))
44 44 end
45 45
46 46 def test_valid_tree
47 47 assert_valid_nested_set
48 48 end
49 49
50 def test_rebuild_should_build_valid_tree
51 Project.update_all "lft = NULL, rgt = NULL"
52
53 Project.rebuild!
54 assert_valid_nested_set
55 end
56
50 57 def test_moving_a_child_to_a_different_parent_should_keep_valid_tree
51 58 assert_no_difference 'Project.count' do
52 59 Project.find_by_name('B1').set_parent!(Project.find_by_name('A2'))
53 60 end
54 61 assert_valid_nested_set
55 62 end
56 63
57 64 def test_renaming_a_root_to_first_position_should_update_nested_set_order
58 65 @c.name = '1'
59 66 @c.save!
60 67 assert_valid_nested_set
61 68 end
62 69
63 70 def test_renaming_a_root_to_middle_position_should_update_nested_set_order
64 71 @a.name = 'BA'
65 72 @a.save!
66 73 assert_valid_nested_set
67 74 end
68 75
69 76 def test_renaming_a_root_to_last_position_should_update_nested_set_order
70 77 @a.name = 'D'
71 78 @a.save!
72 79 assert_valid_nested_set
73 80 end
74 81
75 82 def test_renaming_a_root_to_same_position_should_update_nested_set_order
76 83 @c.name = 'D'
77 84 @c.save!
78 85 assert_valid_nested_set
79 86 end
80 87
81 88 def test_renaming_a_child_should_update_nested_set_order
82 89 @a1.name = 'A3'
83 90 @a1.save!
84 91 assert_valid_nested_set
85 92 end
86 93
87 94 def test_renaming_a_child_with_child_should_update_nested_set_order
88 95 @b1.name = 'B3'
89 96 @b1.save!
90 97 assert_valid_nested_set
91 98 end
92 99
93 100 def test_adding_a_root_to_first_position_should_update_nested_set_order
94 101 project = Project.create!(:name => '1', :identifier => 'projectba')
95 102 assert_valid_nested_set
96 103 end
97 104
98 105 def test_adding_a_root_to_middle_position_should_update_nested_set_order
99 106 project = Project.create!(:name => 'BA', :identifier => 'projectba')
100 107 assert_valid_nested_set
101 108 end
102 109
103 110 def test_adding_a_root_to_last_position_should_update_nested_set_order
104 111 project = Project.create!(:name => 'Z', :identifier => 'projectba')
105 112 assert_valid_nested_set
106 113 end
107 114
108 115 def test_destroying_a_root_with_children_should_keep_valid_tree
109 116 assert_difference 'Project.count', -4 do
110 117 Project.find_by_name('B').destroy
111 118 end
112 119 assert_valid_nested_set
113 120 end
114 121
115 122 def test_destroying_a_child_with_children_should_keep_valid_tree
116 123 assert_difference 'Project.count', -2 do
117 124 Project.find_by_name('B1').destroy
118 125 end
119 126 assert_valid_nested_set
120 127 end
121 128
122 129 private
123 130
124 131 def assert_nested_set_values(h)
125 132 assert Project.valid?
126 133 h.each do |project, expected|
127 134 project.reload
128 135 assert_equal expected, [project.parent_id, project.lft, project.rgt], "Unexpected nested set values for #{project.name}"
129 136 end
130 137 end
131 138
132 139 def assert_valid_nested_set
133 140 projects = Project.all
134 141 lft_rgt = projects.map {|p| [p.lft, p.rgt]}.flatten
135 142 assert_equal projects.size * 2, lft_rgt.uniq.size
136 143 assert_equal 1, lft_rgt.min
137 144 assert_equal projects.size * 2, lft_rgt.max
138 145
139 146 projects.each do |project|
140 147 # lft should always be < rgt
141 148 assert project.lft < project.rgt, "lft=#{project.lft} was not < rgt=#{project.rgt} for project #{project.name}"
142 149 if project.parent_id
143 150 # child lft/rgt values must be greater/lower
144 151 assert_not_nil project.parent, "parent was nil for project #{project.name}"
145 152 assert project.lft > project.parent.lft, "lft=#{project.lft} was not > parent.lft=#{project.parent.lft} for project #{project.name}"
146 153 assert project.rgt < project.parent.rgt, "rgt=#{project.rgt} was not < parent.rgt=#{project.parent.rgt} for project #{project.name}"
147 154 end
148 155 # no overlapping lft/rgt values
149 156 overlapping = projects.detect {|other|
150 157 other != project && (
151 158 (other.lft > project.lft && other.lft < project.rgt && other.rgt > project.rgt) ||
152 159 (other.rgt > project.lft && other.rgt < project.rgt && other.lft < project.lft)
153 160 )
154 161 }
155 162 assert_nil overlapping, (overlapping && "Project #{overlapping.name} (#{overlapping.lft}/#{overlapping.rgt}) overlapped #{project.name} (#{project.lft}/#{project.rgt})")
156 163 end
157 164
158 165 # root projects sorted alphabetically
159 166 assert_equal Project.roots.map(&:name).sort, Project.roots.sort_by(&:lft).map(&:name), "Root projects were not properly sorted"
160 167 projects.each do |project|
161 168 if project.children.any?
162 169 # sibling projects sorted alphabetically
163 170 assert_equal project.children.map(&:name).sort, project.children.order('lft').map(&:name), "Project #{project.name}'s children were not properly sorted"
164 171 end
165 172 end
166 173 end
167 174 end
General Comments 0
You need to be logged in to leave comments. Login now