##// END OF EJS Templates
Fixes a wrong number of arguments error with ruby1.9....
Jean-Philippe Lang -
r2797:531eb6555777
parent child
Show More
@@ -1,547 +1,547
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 returning(root.left > left && root.right > right) 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 scope = lambda{}
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 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?
448 448 diff = right - left + 1
449 449
450 450 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
451 451 :destroy_all : :delete_all
452 452
453 453 self.class.base_class.transaction do
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 nested_set_scope.update_all(
459 459 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
460 460 ["#{quoted_left_column_name} >= ?", right]
461 461 )
462 462 nested_set_scope.update_all(
463 463 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
464 464 ["#{quoted_right_column_name} >= ?", right]
465 465 )
466 466 end
467 467 end
468 468
469 469 # reload left, right, and parent
470 470 def reload_nested_set
471 471 reload(:select => "#{quoted_left_column_name}, " +
472 472 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
473 473 end
474 474
475 475 def move_to(target, position)
476 476 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
477 477 return if callback(:before_move) == false
478 478 transaction do
479 479 if target.is_a? self.class.base_class
480 480 target.reload_nested_set
481 481 elsif position != :root
482 482 # load object if node is not an object
483 483 target = nested_set_scope.find(target)
484 484 end
485 485 self.reload_nested_set
486 486
487 487 unless position == :root || move_possible?(target)
488 488 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
489 489 end
490 490
491 491 bound = case position
492 492 when :child; target[right_column_name]
493 493 when :left; target[left_column_name]
494 494 when :right; target[right_column_name] + 1
495 495 when :root; 1
496 496 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
497 497 end
498 498
499 499 if bound > self[right_column_name]
500 500 bound = bound - 1
501 501 other_bound = self[right_column_name] + 1
502 502 else
503 503 other_bound = self[left_column_name] - 1
504 504 end
505 505
506 506 # there would be no change
507 507 return if bound == self[right_column_name] || bound == self[left_column_name]
508 508
509 509 # we have defined the boundaries of two non-overlapping intervals,
510 510 # so sorting puts both the intervals and their boundaries in order
511 511 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
512 512
513 513 new_parent = case position
514 514 when :child; target.id
515 515 when :root; nil
516 516 else target[parent_column_name]
517 517 end
518 518
519 519 self.class.base_class.update_all([
520 520 "#{quoted_left_column_name} = CASE " +
521 521 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
522 522 "THEN #{quoted_left_column_name} + :d - :b " +
523 523 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
524 524 "THEN #{quoted_left_column_name} + :a - :c " +
525 525 "ELSE #{quoted_left_column_name} END, " +
526 526 "#{quoted_right_column_name} = CASE " +
527 527 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
528 528 "THEN #{quoted_right_column_name} + :d - :b " +
529 529 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
530 530 "THEN #{quoted_right_column_name} + :a - :c " +
531 531 "ELSE #{quoted_right_column_name} END, " +
532 532 "#{quoted_parent_column_name} = CASE " +
533 533 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
534 534 "ELSE #{quoted_parent_column_name} END",
535 535 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
536 536 ], nested_set_scope.proxy_options[:conditions])
537 537 end
538 538 target.reload_nested_set if target
539 539 self.reload_nested_set
540 540 callback(:after_move)
541 541 end
542 542
543 543 end
544 544
545 545 end
546 546 end
547 547 end
General Comments 0
You need to be logged in to leave comments. Login now