awesome_nested_set 1.4.4 → 2.0.0

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