awesome_nested_set 1.4.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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