##// END OF EJS Templates
awesome_nested_set: import git 2-1-stable branch revision 606847769 (#6579)...
Toshi MARUYAMA -
r12868:43e84c6c1070
parent child
Show More
@@ -0,0 +1,14
1 # Contributing to AwesomeNestedSet
2
3 If you find what you might think is a bug:
4
5 1. Check the [GitHub issue tracker](https://github.com/collectiveidea/awesome_nested_set/issues/) to see if anyone else has had the same issue.
6 2. If you don't see anything, create an issue with information on how to reproduce it.
7
8 If you want to contribute an enhancement or a fix:
9
10 1. Fork [the project on GitHub](https://github.com/collectiveidea/awesome_nested_set)
11 2. Make your changes with tests.
12 3. Commit the changes without making changes to the [Rakefile](Rakefile) or any other files that aren't related to your enhancement or fix.
13 4. Write an entry in the [CHANGELOG](CHANGELOG)
14 5. Send a pull request.
@@ -0,0 +1,68
1 # Mixed into both classes and instances to provide easy access to the column names
2 module CollectiveIdea #:nodoc:
3 module Acts #:nodoc:
4 module NestedSet #:nodoc:
5 module Columns
6 def left_column_name
7 acts_as_nested_set_options[:left_column]
8 end
9
10 def right_column_name
11 acts_as_nested_set_options[:right_column]
12 end
13
14 def depth_column_name
15 acts_as_nested_set_options[:depth_column]
16 end
17
18 def parent_column_name
19 acts_as_nested_set_options[:parent_column]
20 end
21
22 def order_column
23 acts_as_nested_set_options[:order_column] || left_column_name
24 end
25
26 def scope_column_names
27 Array(acts_as_nested_set_options[:scope])
28 end
29
30 def quoted_left_column_name
31 connection.quote_column_name(left_column_name)
32 end
33
34 def quoted_right_column_name
35 connection.quote_column_name(right_column_name)
36 end
37
38 def quoted_depth_column_name
39 connection.quote_column_name(depth_column_name)
40 end
41
42 def quoted_parent_column_name
43 connection.quote_column_name(parent_column_name)
44 end
45
46 def quoted_scope_column_names
47 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
48 end
49
50 def quoted_order_column_name
51 connection.quote_column_name(order_column)
52 end
53
54 def quoted_left_column_full_name
55 "#{quoted_table_name}.#{quoted_left_column_name}"
56 end
57
58 def quoted_right_column_full_name
59 "#{quoted_table_name}.#{quoted_right_column_name}"
60 end
61
62 def quoted_parent_column_full_name
63 "#{quoted_table_name}.#{quoted_parent_column_name}"
64 end
65 end
66 end
67 end
68 end
@@ -0,0 +1,29
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 class Iterator
5 attr_reader :objects
6
7 def initialize(objects)
8 @objects = objects
9 end
10
11 def each_with_level
12 path = [nil]
13 objects.each do |o|
14 if o.parent_id != path.last
15 # we are on a new level, did we descend or ascend?
16 if path.include?(o.parent_id)
17 # remove wrong tailing paths elements
18 path.pop while path.last != o.parent_id
19 else
20 path << o.parent_id
21 end
22 end
23 yield(o, path.length - 1)
24 end
25 end
26 end
27 end
28 end
29 end
@@ -0,0 +1,212
1 require 'awesome_nested_set/model/prunable'
2 require 'awesome_nested_set/model/movable'
3 require 'awesome_nested_set/model/transactable'
4 require 'awesome_nested_set/model/relatable'
5 require 'awesome_nested_set/model/rebuildable'
6 require 'awesome_nested_set/model/validatable'
7 require 'awesome_nested_set/iterator'
8
9 module CollectiveIdea #:nodoc:
10 module Acts #:nodoc:
11 module NestedSet #:nodoc:
12
13 module Model
14 extend ActiveSupport::Concern
15
16 included do
17 delegate :quoted_table_name, :arel_table, :to => self
18 extend Validatable
19 extend Rebuildable
20 include Movable
21 include Prunable
22 include Relatable
23 include Transactable
24 end
25
26 module ClassMethods
27 def associate_parents(objects)
28 return objects unless objects.all? {|o| o.respond_to?(:association)}
29
30 id_indexed = objects.index_by(&:id)
31 objects.each do |object|
32 association = object.association(:parent)
33 parent = id_indexed[object.parent_id]
34
35 if !association.loaded? && parent
36 association.target = parent
37 association.set_inverse_instance(parent)
38 end
39 end
40 end
41
42 def children_of(parent_id)
43 where arel_table[parent_column_name].eq(parent_id)
44 end
45
46 # Iterates over tree elements and determines the current level in the tree.
47 # Only accepts default ordering, odering by an other column than lft
48 # does not work. This method is much more efficent than calling level
49 # because it doesn't require any additional database queries.
50 #
51 # Example:
52 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
53 #
54 def each_with_level(objects, &block)
55 Iterator.new(objects).each_with_level(&block)
56 end
57
58 def leaves
59 nested_set_scope.where "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
60 end
61
62 def left_of(node)
63 where arel_table[left_column_name].lt(node)
64 end
65
66 def left_of_right_side(node)
67 where arel_table[right_column_name].lteq(node)
68 end
69
70 def right_of(node)
71 where arel_table[left_column_name].gteq(node)
72 end
73
74 def nested_set_scope(options = {})
75 options = {:order => quoted_order_column_name}.merge(options)
76
77 order(options.delete(:order)).scoped options
78 end
79
80 def primary_key_scope(id)
81 where arel_table[primary_key].eq(id)
82 end
83
84 def root
85 roots.first
86 end
87
88 def roots
89 nested_set_scope.children_of nil
90 end
91 end # end class methods
92
93 # 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.
94 #
95 # category.self_and_descendants.count
96 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
97 # Value of the parent column
98 def parent_id(target = self)
99 target[parent_column_name]
100 end
101
102 # Value of the left column
103 def left(target = self)
104 target[left_column_name]
105 end
106
107 # Value of the right column
108 def right(target = self)
109 target[right_column_name]
110 end
111
112 # Returns true if this is a root node.
113 def root?
114 parent_id.nil?
115 end
116
117 # Returns true is this is a child node
118 def child?
119 !root?
120 end
121
122 # Returns true if this is the end of a branch.
123 def leaf?
124 persisted? && right.to_i - left.to_i == 1
125 end
126
127 # All nested set queries should use this nested_set_scope, which
128 # performs finds on the base ActiveRecord class, using the :scope
129 # declared in the acts_as_nested_set declaration.
130 def nested_set_scope(options = {})
131 if (scopes = Array(acts_as_nested_set_options[:scope])).any?
132 options[:conditions] = scopes.inject({}) do |conditions,attr|
133 conditions.merge attr => self[attr]
134 end
135 end
136
137 self.class.nested_set_scope options
138 end
139
140 def to_text
141 self_and_descendants.map do |node|
142 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
143 end.join("\n")
144 end
145
146 protected
147
148 def without_self(scope)
149 return scope if new_record?
150 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
151 end
152
153 def store_new_parent
154 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
155 true # force callback to return true
156 end
157
158 def has_depth_column?
159 nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
160 end
161
162 def right_most_node
163 @right_most_node ||= self.class.base_class.unscoped.nested_set_scope(
164 :order => "#{quoted_right_column_full_name} desc"
165 ).first
166 end
167
168 def right_most_bound
169 @right_most_bound ||= begin
170 return 0 if right_most_node.nil?
171
172 right_most_node.lock!
173 right_most_node[right_column_name] || 0
174 end
175 end
176
177 def set_depth!
178 return unless has_depth_column?
179
180 in_tenacious_transaction do
181 reload
182 nested_set_scope.primary_key_scope(id).
183 update_all(["#{quoted_depth_column_name} = ?", level])
184 end
185 self[depth_column_name] = self.level
186 end
187
188 def set_default_left_and_right
189 # adds the new node to the right of all existing nodes
190 self[left_column_name] = right_most_bound + 1
191 self[right_column_name] = right_most_bound + 2
192 end
193
194 # reload left, right, and parent
195 def reload_nested_set
196 reload(
197 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
198 :lock => true
199 )
200 end
201
202 def reload_target(target)
203 if target.is_a? self.class.base_class
204 target.reload
205 else
206 nested_set_scope.find(target)
207 end
208 end
209 end
210 end
211 end
212 end
@@ -0,0 +1,137
1 require 'awesome_nested_set/move'
2
3 module CollectiveIdea #:nodoc:
4 module Acts #:nodoc:
5 module NestedSet #:nodoc:
6 module Model
7 module Movable
8
9 def move_possible?(target)
10 self != target && # Can't target self
11 same_scope?(target) && # can't be in different scopes
12 # detect impossible move
13 within_bounds?(target.left, target.left) &&
14 within_bounds?(target.right, target.right)
15 end
16
17 # Shorthand method for finding the left sibling and moving to the left of it.
18 def move_left
19 move_to_left_of left_sibling
20 end
21
22 # Shorthand method for finding the right sibling and moving to the right of it.
23 def move_right
24 move_to_right_of right_sibling
25 end
26
27 # Move the node to the left of another node
28 def move_to_left_of(node)
29 move_to node, :left
30 end
31
32 # Move the node to the left of another node
33 def move_to_right_of(node)
34 move_to node, :right
35 end
36
37 # Move the node to the child of another node
38 def move_to_child_of(node)
39 move_to node, :child
40 end
41
42 # Move the node to the child of another node with specify index
43 def move_to_child_with_index(node, index)
44 if node.children.empty?
45 move_to_child_of(node)
46 elsif node.children.count == index
47 move_to_right_of(node.children.last)
48 else
49 move_to_left_of(node.children[index])
50 end
51 end
52
53 # Move the node to root nodes
54 def move_to_root
55 move_to_right_of(root)
56 end
57
58 # Order children in a nested set by an attribute
59 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
60 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
61 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
62 self.move_to_root and return unless parent
63
64 left_neighbor = find_left_neighbor(parent, order_attribute, ascending)
65 self.move_to_child_of(parent)
66
67 return unless parent.children.many?
68
69 if left_neighbor
70 self.move_to_right_of(left_neighbor)
71 else # Self is the left most node.
72 self.move_to_left_of(parent.children[0])
73 end
74 end
75
76 # Find the node immediately to the left of this node.
77 def find_left_neighbor(parent, order_attribute, ascending)
78 left = nil
79 parent.children.each do |n|
80 if ascending
81 left = n if n.send(order_attribute) < self.send(order_attribute)
82 else
83 left = n if n.send(order_attribute) > self.send(order_attribute)
84 end
85 end
86 left
87 end
88
89 def move_to(target, position)
90 prevent_unpersisted_move
91
92 run_callbacks :move do
93 in_tenacious_transaction do
94 target = reload_target(target)
95 self.reload_nested_set
96
97 Move.new(target, position, self).move
98 end
99 after_move_to(target, position)
100 end
101 end
102
103 protected
104
105 def after_move_to(target, position)
106 target.reload_nested_set if target
107 self.set_depth!
108 self.descendants.each(&:save)
109 self.reload_nested_set
110 end
111
112 def move_to_new_parent
113 if @move_to_new_parent_id.nil?
114 move_to_root
115 elsif @move_to_new_parent_id
116 move_to_child_of(@move_to_new_parent_id)
117 end
118 end
119
120 def out_of_bounds?(left_bound, right_bound)
121 left <= left_bound && right >= right_bound
122 end
123
124 def prevent_unpersisted_move
125 if self.new_record?
126 raise ActiveRecord::ActiveRecordError, "You cannot move a new node"
127 end
128 end
129
130 def within_bounds?(left_bound, right_bound)
131 !out_of_bounds?(left_bound, right_bound)
132 end
133 end
134 end
135 end
136 end
137 end
@@ -0,0 +1,58
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 module Model
5 module Prunable
6
7 # Prunes a branch off of the tree, shifting all of the elements on the right
8 # back to the left so the counts still work.
9 def destroy_descendants
10 return if right.nil? || left.nil? || skip_before_destroy
11
12 in_tenacious_transaction do
13 reload_nested_set
14 # select the rows in the model that extend past the deletion point and apply a lock
15 nested_set_scope.right_of(left).select(id).lock(true)
16
17 destroy_or_delete_descendants
18
19 # update lefts and rights for remaining nodes
20 update_siblings_for_remaining_nodes
21
22 # Don't allow multiple calls to destroy to corrupt the set
23 self.skip_before_destroy = true
24 end
25 end
26
27 def destroy_or_delete_descendants
28 if acts_as_nested_set_options[:dependent] == :destroy
29 descendants.each do |model|
30 model.skip_before_destroy = true
31 model.destroy
32 end
33 else
34 descendants.delete_all
35 end
36 end
37
38 def update_siblings_for_remaining_nodes
39 update_siblings(:left)
40 update_siblings(:right)
41 end
42
43 def update_siblings(direction)
44 full_column_name = send("quoted_#{direction}_column_full_name")
45 column_name = send("quoted_#{direction}_column_name")
46
47 nested_set_scope.where(["#{full_column_name} > ?", right]).
48 update_all(["#{column_name} = (#{column_name} - ?)", diff])
49 end
50
51 def diff
52 right - left + 1
53 end
54 end
55 end
56 end
57 end
58 end
@@ -0,0 +1,41
1 require 'awesome_nested_set/tree'
2
3 module CollectiveIdea
4 module Acts
5 module NestedSet
6 module Model
7 module Rebuildable
8
9
10 # Rebuilds the left & rights if unset or invalid.
11 # Also very useful for converting from acts_as_tree.
12 def rebuild!(validate_nodes = true)
13 # default_scope with order may break database queries so we do all operation without scope
14 unscoped do
15 Tree.new(self, validate_nodes).rebuild!
16 end
17 end
18
19 private
20 def scope_for_rebuild
21 scope = proc {}
22
23 if acts_as_nested_set_options[:scope]
24 scope = proc {|node|
25 scope_column_names.inject("") {|str, column_name|
26 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name))} "
27 }
28 }
29 end
30 scope
31 end
32
33 def order_for_rebuild
34 "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{primary_key}"
35 end
36 end
37
38 end
39 end
40 end
41 end
@@ -0,0 +1,121
1 module CollectiveIdea
2 module Acts
3 module NestedSet
4 module Model
5 module Relatable
6
7 # Returns an collection of all parents
8 def ancestors
9 without_self self_and_ancestors
10 end
11
12 # Returns the collection of all parents and self
13 def self_and_ancestors
14 nested_set_scope.
15 where(arel_table[left_column_name].lteq(left)).
16 where(arel_table[right_column_name].gteq(right))
17 end
18
19 # Returns the collection of all children of the parent, except self
20 def siblings
21 without_self self_and_siblings
22 end
23
24 # Returns the collection of all children of the parent, including self
25 def self_and_siblings
26 nested_set_scope.children_of parent_id
27 end
28
29 # Returns a set of all of its nested children which do not have children
30 def leaves
31 descendants.where(
32 "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
33 )
34 end
35
36 # Returns the level of this object in the tree
37 # root level is 0
38 def level
39 parent_id.nil? ? 0 : compute_level
40 end
41
42 # Returns a collection including all of its children and nested children
43 def descendants
44 without_self self_and_descendants
45 end
46
47 # Returns a collection including itself and all of its nested children
48 def self_and_descendants
49 # using _left_ for both sides here lets us benefit from an index on that column if one exists
50 nested_set_scope.right_of(left).left_of(right)
51 end
52
53 def is_descendant_of?(other)
54 within_node?(other, self) && same_scope?(other)
55 end
56
57 def is_or_is_descendant_of?(other)
58 (other == self || within_node?(other, self)) && same_scope?(other)
59 end
60
61 def is_ancestor_of?(other)
62 within_node?(self, other) && same_scope?(other)
63 end
64
65 def is_or_is_ancestor_of?(other)
66 (self == other || within_node?(self, other)) && same_scope?(other)
67 end
68
69 # Check if other model is in the same scope
70 def same_scope?(other)
71 Array(acts_as_nested_set_options[:scope]).all? do |attr|
72 self.send(attr) == other.send(attr)
73 end
74 end
75
76 # Find the first sibling to the left
77 def left_sibling
78 siblings.left_of(left).last
79 end
80
81 # Find the first sibling to the right
82 def right_sibling
83 siblings.right_of(left).first
84 end
85
86 def root
87 return self_and_ancestors.children_of(nil).first if persisted?
88
89 if parent_id && current_parent = nested_set_scope.find(parent_id)
90 current_parent.root
91 else
92 self
93 end
94 end
95
96 protected
97
98 def compute_level
99 node, nesting = determine_depth
100
101 node == self ? ancestors.count : node.level + nesting
102 end
103
104 def determine_depth(node = self, nesting = 0)
105 while (association = node.association(:parent)).loaded? && association.target
106 nesting += 1
107 node = node.parent
108 end if node.respond_to?(:association)
109
110 [node, nesting]
111 end
112
113 def within_node?(node, within)
114 node.left < within.left && within.left < node.right
115 end
116
117 end
118 end
119 end
120 end
121 end
@@ -0,0 +1,27
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 module Model
5 module Transactable
6
7 protected
8 def in_tenacious_transaction(&block)
9 retry_count = 0
10 begin
11 transaction(&block)
12 rescue ActiveRecord::StatementInvalid => error
13 raise unless connection.open_transactions.zero?
14 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
15 raise unless retry_count < 10
16 retry_count += 1
17 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
18 sleep(rand(retry_count)*0.1) # Aloha protocol
19 retry
20 end
21 end
22
23 end
24 end
25 end
26 end
27 end
@@ -0,0 +1,69
1 require 'awesome_nested_set/set_validator'
2
3 module CollectiveIdea
4 module Acts
5 module NestedSet
6 module Model
7 module Validatable
8
9 def valid?
10 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
11 end
12
13 def left_and_rights_valid?
14 SetValidator.new(self).valid?
15 end
16
17 def no_duplicates_for_columns?
18 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
19 # No duplicates
20 select("#{scope_string}#{column}, COUNT(#{column})").
21 group("#{scope_string}#{column}").
22 having("COUNT(#{column}) > 1").
23 first.nil?
24 end
25 end
26
27 # Wrapper for each_root_valid? that can deal with scope.
28 def all_roots_valid?
29 if acts_as_nested_set_options[:scope]
30 all_roots_valid_by_scope?(roots)
31 else
32 each_root_valid?(roots)
33 end
34 end
35
36 def all_roots_valid_by_scope?(roots_to_validate)
37 roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots|
38 each_root_valid?(grouped_roots)
39 end
40 end
41
42 def each_root_valid?(roots_to_validate)
43 left = right = 0
44 roots_to_validate.all? do |root|
45 (root.left > left && root.right > right).tap do
46 left = root.left
47 right = root.right
48 end
49 end
50 end
51
52 private
53 def roots_grouped_by_scope(roots_to_group)
54 roots_to_group.group_by {|record|
55 scope_column_names.collect {|col| record.send(col) }
56 }
57 end
58
59 def scope_string
60 Array(acts_as_nested_set_options[:scope]).map do |c|
61 connection.quote_column_name(c)
62 end.push(nil).join(", ")
63 end
64
65 end
66 end
67 end
68 end
69 end
@@ -0,0 +1,117
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 class Move
5 attr_reader :target, :position, :instance
6
7 def initialize(target, position, instance)
8 @target = target
9 @position = position
10 @instance = instance
11 end
12
13 def move
14 prevent_impossible_move
15
16 bound, other_bound = get_boundaries
17
18 # there would be no change
19 return if bound == right || bound == left
20
21 # we have defined the boundaries of two non-overlapping intervals,
22 # so sorting puts both the intervals and their boundaries in order
23 a, b, c, d = [left, right, bound, other_bound].sort
24
25 lock_nodes_between! a, d
26
27 nested_set_scope.where(where_statement(a, d)).
28 update_all(conditions(a, b, c, d))
29 end
30
31 private
32
33 delegate :left, :right, :left_column_name, :right_column_name,
34 :quoted_left_column_name, :quoted_right_column_name,
35 :quoted_parent_column_name, :parent_column_name, :nested_set_scope,
36 :to => :instance
37
38 delegate :arel_table, :class, :to => :instance, :prefix => true
39 delegate :base_class, :to => :instance_class, :prefix => :instance
40
41 def where_statement(left_bound, right_bound)
42 instance_arel_table[left_column_name].in(left_bound..right_bound).
43 or(instance_arel_table[right_column_name].in(left_bound..right_bound))
44 end
45
46 def conditions(a, b, c, d)
47 [
48 case_condition_for_direction(:quoted_left_column_name) +
49 case_condition_for_direction(:quoted_right_column_name) +
50 case_condition_for_parent,
51 {:a => a, :b => b, :c => c, :d => d, :id => instance.id, :new_parent => new_parent}
52 ]
53 end
54
55 def case_condition_for_direction(column_name)
56 column = send(column_name)
57 "#{column} = CASE " +
58 "WHEN #{column} BETWEEN :a AND :b " +
59 "THEN #{column} + :d - :b " +
60 "WHEN #{column} BETWEEN :c AND :d " +
61 "THEN #{column} + :a - :c " +
62 "ELSE #{column} END, "
63 end
64
65 def case_condition_for_parent
66 "#{quoted_parent_column_name} = CASE " +
67 "WHEN #{instance_base_class.primary_key} = :id THEN :new_parent " +
68 "ELSE #{quoted_parent_column_name} END"
69 end
70
71 def lock_nodes_between!(left_bound, right_bound)
72 # select the rows in the model between a and d, and apply a lock
73 instance_base_class.right_of(left_bound).left_of_right_side(right_bound).
74 select(:id).lock(true)
75 end
76
77 def root
78 position == :root
79 end
80
81 def new_parent
82 case position
83 when :child
84 target.id
85 else
86 target[parent_column_name]
87 end
88 end
89
90 def get_boundaries
91 if (bound = target_bound) > right
92 bound -= 1
93 other_bound = right + 1
94 else
95 other_bound = left - 1
96 end
97 [bound, other_bound]
98 end
99
100 def prevent_impossible_move
101 if !root && !instance.move_possible?(target)
102 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
103 end
104 end
105
106 def target_bound
107 case position
108 when :child; right(target)
109 when :left; left(target)
110 when :right; right(target) + 1
111 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
112 end
113 end
114 end
115 end
116 end
117 end
@@ -0,0 +1,63
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 class SetValidator
5
6 def initialize(model)
7 @model = model
8 @scope = model.scoped
9 @parent = arel_table.alias('parent')
10 end
11
12 def valid?
13 query.count == 0
14 end
15
16 private
17
18 attr_reader :model, :parent
19 attr_accessor :scope
20
21 delegate :parent_column_name, :primary_key, :left_column_name, :right_column_name, :arel_table,
22 :quoted_table_name, :quoted_parent_column_full_name, :quoted_left_column_full_name, :quoted_right_column_full_name, :quoted_left_column_name, :quoted_right_column_name,
23 :to => :model
24
25 def query
26 join_scope
27 filter_scope
28 end
29
30 def join_scope
31 join_arel = arel_table.join(parent, Arel::Nodes::OuterJoin).on(parent[primary_key].eq(arel_table[parent_column_name]))
32 self.scope = scope.joins(join_arel.join_sql)
33 end
34
35 def filter_scope
36 self.scope = scope.where(
37 bound_is_null(left_column_name).
38 or(bound_is_null(right_column_name)).
39 or(left_bound_greater_than_right).
40 or(parent_not_null.and(bounds_outside_parent))
41 )
42 end
43
44 def bound_is_null(column_name)
45 arel_table[column_name].eq(nil)
46 end
47
48 def left_bound_greater_than_right
49 arel_table[left_column_name].gteq(arel_table[right_column_name])
50 end
51
52 def parent_not_null
53 arel_table[parent_column_name].not_eq(nil)
54 end
55
56 def bounds_outside_parent
57 arel_table[left_column_name].lteq(parent[left_column_name]).or(arel_table[right_column_name].gteq(parent[right_column_name]))
58 end
59
60 end
61 end
62 end
63 end
@@ -0,0 +1,63
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 class Tree
5 attr_reader :model, :validate_nodes
6 attr_accessor :indices
7
8 delegate :left_column_name, :right_column_name, :quoted_parent_column_full_name,
9 :order_for_rebuild, :scope_for_rebuild,
10 :to => :model
11
12 def initialize(model, validate_nodes)
13 @model = model
14 @validate_nodes = validate_nodes
15 @indices = {}
16 end
17
18 def rebuild!
19 # Don't rebuild a valid tree.
20 return true if model.valid?
21
22 root_nodes.each do |root_node|
23 # setup index for this scope
24 indices[scope_for_rebuild.call(root_node)] ||= 0
25 set_left_and_rights(root_node)
26 end
27 end
28
29 private
30
31 def increment_indice!(node)
32 indices[scope_for_rebuild.call(node)] += 1
33 end
34
35 def set_left_and_rights(node)
36 set_left!(node)
37 # find
38 node_children(node).each { |n| set_left_and_rights(n) }
39 set_right!(node)
40
41 node.save!(:validate => validate_nodes)
42 end
43
44 def node_children(node)
45 model.where(["#{quoted_parent_column_full_name} = ? #{scope_for_rebuild.call(node)}", node]).
46 order(order_for_rebuild)
47 end
48
49 def root_nodes
50 model.where("#{quoted_parent_column_full_name} IS NULL").order(order_for_rebuild)
51 end
52
53 def set_left!(node)
54 node[left_column_name] = increment_indice!(node)
55 end
56
57 def set_right!(node)
58 node[right_column_name] = increment_indice!(node)
59 end
60 end
61 end
62 end
63 end
@@ -1,7 +1,4
1 1 language: ruby
2 notifications:
3 email:
4 - parndt@gmail.com
5 2 script: bundle exec rspec spec
6 3 env:
7 4 - DB=sqlite3
@@ -11,9 +8,9 env:
11 8 rvm:
12 9 - 2.0.0
13 10 - 1.9.3
14 - 1.8.7
15 11 - rbx-19mode
16 12 - jruby-19mode
13 - 1.8.7
17 14 - rbx-18mode
18 15 - jruby-18mode
19 16 gemfile:
@@ -1,4 +1,4
1 gem 'combustion', :github => 'pat/combustion'
1 gem 'combustion', :github => 'pat/combustion', :branch => 'master'
2 2
3 3 source 'https://rubygems.org'
4 4
@@ -7,6 +7,7 gemspec :path => File.expand_path('../', __FILE__)
7 7 platforms :jruby do
8 8 gem 'activerecord-jdbcsqlite3-adapter'
9 9 gem 'activerecord-jdbcmysql-adapter'
10 gem 'jdbc-mysql'
10 11 gem 'activerecord-jdbcpostgresql-adapter'
11 12 gem 'jruby-openssl'
12 13 end
@@ -27,5 +28,5 gem 'actionpack', RAILS_VERSION
27 28 # gem 'activerecord-oracle_enhanced-adapter'
28 29
29 30 # Debuggers
30 # gem 'pry'
31 # gem 'pry-nav'
31 gem 'pry'
32 gem 'pry-nav'
@@ -1,153 +1,163
1 = AwesomeNestedSet
1 # AwesomeNestedSet
2 2
3 Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but more awesome.
3 Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models.
4 It is a replacement for acts_as_nested_set and BetterNestedSet, but more awesome.
4 5
5 6 Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2.
6 7
7 == What makes this so awesome?
8 ## What makes this so awesome?
8 9
9 10 This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
10 11
11 == Installation
12 [![Code Climate](https://codeclimate.com/github/collectiveidea/awesome_nested_set.png)](https://codeclimate.com/github/collectiveidea/awesome_nested_set)
12 13
13 Add to your Gemfile:
14 ## Installation
14 15
15 gem 'awesome_nested_set'
16 Add to your Gemfile:
16 17
17 == Usage
18 ```ruby
19 gem 'awesome_nested_set'
20 ```
18 21
19 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id.
20 You can also have an optional field: depth:
22 ## Usage
21 23
22 class CreateCategories < ActiveRecord::Migration
23 def self.up
24 create_table :categories do |t|
25 t.string :name
26 t.integer :parent_id
27 t.integer :lft
28 t.integer :rgt
29 t.integer :depth # this is optional.
30 end
31 end
24 To make use of `awesome_nested_set`, your model needs to have 3 fields:
25 `lft`, `rgt`, and `parent_id`. The names of these fields are configurable.
26 You can also have an optional field, `depth`:
32 27
33 def self.down
34 drop_table :categories
28 ```ruby
29 class CreateCategories < ActiveRecord::Migration
30 def self.up
31 create_table :categories do |t|
32 t.string :name
33 t.integer :parent_id
34 t.integer :lft
35 t.integer :rgt
36 t.integer :depth # this is optional.
35 37 end
36 38 end
37 39
38 Enable the nested set functionality by declaring acts_as_nested_set on your model
39
40 class Category < ActiveRecord::Base
41 acts_as_nested_set
40 def self.down
41 drop_table :categories
42 42 end
43 end
44 ```
43 45
44 Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
46 Enable the nested set functionality by declaring `acts_as_nested_set` on your model
45 47
46 == Callbacks
48 ```ruby
49 class Category < ActiveRecord::Base
50 acts_as_nested_set
51 end
52 ```
47 53
48 There are three callbacks called when moving a node. `before_move`, `after_move` and `around_move`.
54 Run `rake rdoc` to generate the API docs and see [CollectiveIdea::Acts::NestedSet](lib/awesome_nested_set/awesome_nested_set.rb) for more information.
49 55
50 class Category < ActiveRecord::Base
51 acts_as_nested_set
56 ## Callbacks
52 57
53 after_move :rebuild_slug
54 around_move :da_fancy_things_around
58 There are three callbacks called when moving a node:
59 `before_move`, `after_move` and `around_move`.
55 60
56 private
57
58 def rebuild_slug
59 # do whatever
60 end
61 ```ruby
62 class Category < ActiveRecord::Base
63 acts_as_nested_set
61 64
62 def da_fancy_things_around
63 # do something...
64 yield # actually moves
65 # do something else...
66 end
65 after_move :rebuild_slug
66 around_move :da_fancy_things_around
67
68 private
69
70 def rebuild_slug
71 # do whatever
67 72 end
68 73
74 def da_fancy_things_around
75 # do something...
76 yield # actually moves
77 # do something else...
78 end
79 end
80 ```
81
69 82 Beside this there are also hooks to act on the newly added or removed children.
70 83
71 class Category < ActiveRecord::Base
72 acts_as_nested_set :before_add => :do_before_add_stuff,
73 :after_add => :do_after_add_stuff,
74 :before_remove => :do_before_remove_stuff,
75 :after_remove => :do_after_remove_stuff
84 ```ruby
85 class Category < ActiveRecord::Base
86 acts_as_nested_set :before_add => :do_before_add_stuff,
87 :after_add => :do_after_add_stuff,
88 :before_remove => :do_before_remove_stuff,
89 :after_remove => :do_after_remove_stuff
76 90
77 private
91 private
78 92
79 def do_before_add_stuff(child_node)
80 # do whatever with the child
81 end
82
83 def do_after_add_stuff(child_node)
84 # do whatever with the child
85 end
93 def do_before_add_stuff(child_node)
94 # do whatever with the child
95 end
86 96
87 def do_before_remove_stuff(child_node)
88 # do whatever with the child
89 end
97 def do_after_add_stuff(child_node)
98 # do whatever with the child
99 end
90 100
91 def do_after_remove_stuff(child_node)
92 # do whatever with the child
93 end
101 def do_before_remove_stuff(child_node)
102 # do whatever with the child
94 103 end
95 104
105 def do_after_remove_stuff(child_node)
106 # do whatever with the child
107 end
108 end
109 ```
96 110
97 == Protecting attributes from mass assignment
111 ## Protecting attributes from mass assignment
98 112
99 It's generally best to "white list" the attributes that can be used in mass assignment:
113 It's generally best to "whitelist" the attributes that can be used in mass assignment:
100 114
101 class Category < ActiveRecord::Base
102 acts_as_nested_set
103 attr_accessible :name, :parent_id
104 end
115 ```ruby
116 class Category < ActiveRecord::Base
117 acts_as_nested_set
118 attr_accessible :name, :parent_id
119 end
120 ```
105 121
106 If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
122 If for some reason that is not possible, you will probably want to protect the `lft` and `rgt` attributes:
107 123
108 class Category < ActiveRecord::Base
109 acts_as_nested_set
110 attr_protected :lft, :rgt
111 end
124 ```ruby
125 class Category < ActiveRecord::Base
126 acts_as_nested_set
127 attr_protected :lft, :rgt
128 end
129 ```
112 130
113 == Conversion from other trees
131 ## Conversion from other trees
114 132
115 Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run
133 Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run:
116 134
117 Category.rebuild!
135 ```ruby
136 Category.rebuild!
137 ```
118 138
119 139 Your tree will be converted to a valid nested set. Awesome!
120 140
121 == View Helper
141 ## View Helper
122 142
123 143 The view helper is called #nested_set_options.
124 144
125 145 Example usage:
126 146
127 <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
147 ```erb
148 <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
128 149
129 <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
150 <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
151 ```
130 152
131 See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
153 See [CollectiveIdea::Acts::NestedSet::Helper](lib/awesome_nested_set/helper.rb) for more information about the helpers.
132 154
133 == References
155 ## References
134 156
135 157 You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
136 158
137 == How to contribute
138
139 If you find what you might think is a bug:
140
141 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
142 https://github.com/collectiveidea/awesome_nested_set/issues/
143 2. If you don't see anything, create an issue with information on how to reproduce it.
144
145 If you want to contribute an enhancement or a fix:
159 ## How to contribute
146 160
147 1. Fork the project on GitHub.
148 https://github.com/collectiveidea/awesome_nested_set/
149 2. Make your changes with tests.
150 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
151 4. Send a pull request.
161 Please see the ['Contributing' document](CONTRIBUTING.md).
152 162
153 Copyright ©2008 Collective Idea, released under the MIT license
163 Copyright © 2008 - 2013 Collective Idea, released under the MIT license
@@ -7,10 +7,9 Gem::Specification.new do |s|
7 7 s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
8 8 s.description = %q{An awesome nested set implementation for Active Record}
9 9 s.email = %q{info@collectiveidea.com}
10 s.extra_rdoc_files = %w[README.rdoc]
11 s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG)
10 s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.md CHANGELOG)
12 11 s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
13 s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
12 s.rdoc_options = ["--inline-source", "--line-numbers"]
14 13 s.require_paths = ["lib"]
15 14 s.rubygems_version = %q{1.3.6}
16 15 s.summary = %q{An awesome nested set implementation for Active Record}
@@ -21,4 +20,5 Gem::Specification.new do |s|
21 20 s.add_development_dependency 'rspec-rails', '~> 2.12'
22 21 s.add_development_dependency 'rake', '~> 10'
23 22 s.add_development_dependency 'combustion', '>= 0.3.3'
23 s.add_development_dependency 'database_cleaner'
24 24 end
@@ -5,4 +5,4 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
5 5 if defined?(ActionView)
6 6 require 'awesome_nested_set/helper'
7 7 ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
8 end No newline at end of file
8 end
This diff has been collapsed as it changes many lines, (755 lines changed) Show them Hide them
@@ -1,3 +1,6
1 require 'awesome_nested_set/columns'
2 require 'awesome_nested_set/model'
3
1 4 module CollectiveIdea #:nodoc:
2 5 module Acts #:nodoc:
3 6 module NestedSet #:nodoc:
@@ -42,731 +45,89 module CollectiveIdea #:nodoc:
42 45 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
43 46 # to acts_as_nested_set models
44 47 def acts_as_nested_set(options = {})
45 options = {
46 :parent_column => 'parent_id',
47 :left_column => 'lft',
48 :right_column => 'rgt',
49 :depth_column => 'depth',
50 :dependent => :delete_all, # or :destroy
51 :polymorphic => false,
52 :counter_cache => false
53 }.merge(options)
54
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
56 options[:scope] = "#{options[:scope]}_id".intern
57 end
58
59 class_attribute :acts_as_nested_set_options
60 self.acts_as_nested_set_options = options
48 acts_as_nested_set_parse_options! options
61 49
62 include CollectiveIdea::Acts::NestedSet::Model
50 include Model
63 51 include Columns
64 52 extend Columns
65 53
66 belongs_to :parent, :class_name => self.base_class.to_s,
67 :foreign_key => parent_column_name,
68 :counter_cache => options[:counter_cache],
69 :inverse_of => (:children unless options[:polymorphic]),
70 :polymorphic => options[:polymorphic]
71
72 has_many_children_options = {
73 :class_name => self.base_class.to_s,
74 :foreign_key => parent_column_name,
75 :order => order_column,
76 :inverse_of => (:parent unless options[:polymorphic]),
77 }
78
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
80 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
82 end
83
84 has_many :children, has_many_children_options
54 acts_as_nested_set_relate_parent!
55 acts_as_nested_set_relate_children!
85 56
86 57 attr_accessor :skip_before_destroy
87 58
59 acts_as_nested_set_prevent_assignment_to_reserved_columns!
60 acts_as_nested_set_define_callbacks!
61 end
62
63 private
64 def acts_as_nested_set_define_callbacks!
65 # on creation, set automatically lft and rgt to the end of the tree
88 66 before_create :set_default_left_and_right
89 67 before_save :store_new_parent
90 68 after_save :move_to_new_parent, :set_depth!
91 69 before_destroy :destroy_descendants
92 70
93 # no assignment to structure fields
94 [left_column_name, right_column_name, depth_column_name].each do |column|
95 module_eval <<-"end_eval", __FILE__, __LINE__
96 def #{column}=(x)
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
98 end
99 end_eval
100 end
101
102 71 define_model_callbacks :move
103 72 end
104 73
105 module Model
106 extend ActiveSupport::Concern
107
108 included do
109 delegate :quoted_table_name, :to => self
110 end
111
112 module ClassMethods
113 # Returns the first root
114 def root
115 roots.first
116 end
117
118 def roots
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
120 end
121
122 def leaves
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
124 end
125
126 def valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
128 end
129
130 def left_and_rights_valid?
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134 "parent ON " +
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
136 where(
137 "#{quoted_left_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
139 "#{quoted_left_column_full_name} >= " +
140 "#{quoted_right_column_full_name} OR " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
144 ).count == 0
145 end
146
147 def no_duplicates_for_columns?
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
149 connection.quote_column_name(c)
150 end.push(nil).join(", ")
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
152 # No duplicates
153 select("#{scope_string}#{column}, COUNT(#{column})").
154 group("#{scope_string}#{column}").
155 having("COUNT(#{column}) > 1").
156 first.nil?
157 end
158 end
159
160 # Wrapper for each_root_valid? that can deal with scope.
161 def all_roots_valid?
162 if acts_as_nested_set_options[:scope]
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
164 each_root_valid?(grouped_roots)
165 end
166 else
167 each_root_valid?(roots)
168 end
169 end
170
171 def each_root_valid?(roots_to_validate)
172 left = right = 0
173 roots_to_validate.all? do |root|
174 (root.left > left && root.right > right).tap do
175 left = root.left
176 right = root.right
177 end
178 end
179 end
180
181 # Rebuilds the left & rights if unset or invalid.
182 # Also very useful for converting from acts_as_tree.
183 def rebuild!(validate_nodes = true)
184 # default_scope with order may break database queries so we do all operation without scope
185 unscoped do
186 # Don't rebuild a valid tree.
187 return true if valid?
188
189 scope = lambda{|node|}
190 if acts_as_nested_set_options[:scope]
191 scope = lambda{|node|
192 scope_column_names.inject(""){|str, column_name|
193 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
194 }
195 }
196 end
197 indices = {}
198
199 set_left_and_rights = lambda do |node|
200 # set left
201 node[left_column_name] = indices[scope.call(node)] += 1
202 # find
203 where(["#{quoted_parent_column_full_name} = ? #{scope.call(node)}", node]).order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each{|n| set_left_and_rights.call(n) }
204 # set right
205 node[right_column_name] = indices[scope.call(node)] += 1
206 node.save!(:validate => validate_nodes)
207 end
208
209 # Find root node(s)
210 root_nodes = where("#{quoted_parent_column_full_name} IS NULL").order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each do |root_node|
211 # setup index for this scope
212 indices[scope.call(root_node)] ||= 0
213 set_left_and_rights.call(root_node)
214 end
215 end
216 end
217
218 # Iterates over tree elements and determines the current level in the tree.
219 # Only accepts default ordering, odering by an other column than lft
220 # does not work. This method is much more efficent than calling level
221 # because it doesn't require any additional database queries.
222 #
223 # Example:
224 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
225 #
226 def each_with_level(objects)
227 path = [nil]
228 objects.each do |o|
229 if o.parent_id != path.last
230 # we are on a new level, did we descend or ascend?
231 if path.include?(o.parent_id)
232 # remove wrong wrong tailing paths elements
233 path.pop while path.last != o.parent_id
234 else
235 path << o.parent_id
236 end
237 end
238 yield(o, path.length - 1)
239 end
240 end
241
242 # Same as each_with_level - Accepts a string as a second argument to sort the list
243 # Example:
244 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
245 def sorted_each_with_level(objects, order)
246 path = [nil]
247 children = []
248 objects.each do |o|
249 children << o if o.leaf?
250 if o.parent_id != path.last
251 if !children.empty? && !o.leaf?
252 children.sort_by! &order
253 children.each { |c| yield(c, path.length-1) }
254 children = []
255 end
256 # we are on a new level, did we decent or ascent?
257 if path.include?(o.parent_id)
258 # remove wrong wrong tailing paths elements
259 path.pop while path.last != o.parent_id
260 else
261 path << o.parent_id
262 end
263 end
264 yield(o,path.length-1) if !o.leaf?
265 end
266 if !children.empty?
267 children.sort_by! &order
268 children.each { |c| yield(c, path.length-1) }
269 end
270 end
271
272 def associate_parents(objects)
273 if objects.all?{|o| o.respond_to?(:association)}
274 id_indexed = objects.index_by(&:id)
275 objects.each do |object|
276 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
277 association.target = parent
278 association.set_inverse_instance(parent)
279 end
280 end
281 else
282 objects
283 end
284 end
285 end
286
287 # 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.
288 #
289 # category.self_and_descendants.count
290 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
291 # Value of the parent column
292 def parent_id
293 self[parent_column_name]
294 end
295
296 # Value of the left column
297 def left
298 self[left_column_name]
299 end
300
301 # Value of the right column
302 def right
303 self[right_column_name]
304 end
305
306 # Returns true if this is a root node.
307 def root?
308 parent_id.nil?
309 end
310
311 # Returns true if this is the end of a branch.
312 def leaf?
313 persisted? && right.to_i - left.to_i == 1
314 end
315
316 # Returns true is this is a child node
317 def child?
318 !root?
319 end
320
321 # Returns root
322 def root
323 if persisted?
324 self_and_ancestors.where(parent_column_name => nil).first
325 else
326 if parent_id && current_parent = nested_set_scope.find(parent_id)
327 current_parent.root
328 else
329 self
330 end
331 end
332 end
333
334 # Returns the array of all parents and self
335 def self_and_ancestors
336 nested_set_scope.where([
337 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
338 ])
339 end
340
341 # Returns an array of all parents
342 def ancestors
343 without_self self_and_ancestors
344 end
345
346 # Returns the array of all children of the parent, including self
347 def self_and_siblings
348 nested_set_scope.where(parent_column_name => parent_id)
349 end
350
351 # Returns the array of all children of the parent, except self
352 def siblings
353 without_self self_and_siblings
354 end
355
356 # Returns a set of all of its nested children which do not have children
357 def leaves
358 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
359 end
360
361 # Returns the level of this object in the tree
362 # root level is 0
363 def level
364 parent_id.nil? ? 0 : compute_level
365 end
366
367 # Returns a set of itself and all of its nested children
368 def self_and_descendants
369 nested_set_scope.where([
370 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
371 # using _left_ for both sides here lets us benefit from an index on that column if one exists
372 ])
373 end
374
375 # Returns a set of all of its children and nested children
376 def descendants
377 without_self self_and_descendants
378 end
379
380 def is_descendant_of?(other)
381 other.left < self.left && self.left < other.right && same_scope?(other)
382 end
383
384 def is_or_is_descendant_of?(other)
385 other.left <= self.left && self.left < other.right && same_scope?(other)
386 end
387
388 def is_ancestor_of?(other)
389 self.left < other.left && other.left < self.right && same_scope?(other)
390 end
391
392 def is_or_is_ancestor_of?(other)
393 self.left <= other.left && other.left < self.right && same_scope?(other)
394 end
395
396 # Check if other model is in the same scope
397 def same_scope?(other)
398 Array(acts_as_nested_set_options[:scope]).all? do |attr|
399 self.send(attr) == other.send(attr)
400 end
401 end
402
403 # Find the first sibling to the left
404 def left_sibling
405 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
406 order("#{quoted_left_column_full_name} DESC").last
407 end
408
409 # Find the first sibling to the right
410 def right_sibling
411 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
412 end
413
414 # Shorthand method for finding the left sibling and moving to the left of it.
415 def move_left
416 move_to_left_of left_sibling
417 end
418
419 # Shorthand method for finding the right sibling and moving to the right of it.
420 def move_right
421 move_to_right_of right_sibling
422 end
423
424 # Move the node to the left of another node (you can pass id only)
425 def move_to_left_of(node)
426 move_to node, :left
427 end
428
429 # Move the node to the left of another node (you can pass id only)
430 def move_to_right_of(node)
431 move_to node, :right
432 end
433
434 # Move the node to the child of another node (you can pass id only)
435 def move_to_child_of(node)
436 move_to node, :child
437 end
438
439 # Move the node to the child of another node with specify index (you can pass id only)
440 def move_to_child_with_index(node, index)
441 if node.children.empty?
442 move_to_child_of(node)
443 elsif node.children.count == index
444 move_to_right_of(node.children.last)
445 else
446 move_to_left_of(node.children[index])
447 end
448 end
449
450 # Move the node to root nodes
451 def move_to_root
452 move_to nil, :root
453 end
454
455 # Order children in a nested set by an attribute
456 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
457 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
458 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
459 self.move_to_root and return unless parent
460 left = nil # This is needed, at least for the tests.
461 parent.children.each do |n| # Find the node immediately to the left of this node.
462 if ascending
463 left = n if n.send(order_attribute) < self.send(order_attribute)
464 else
465 left = n if n.send(order_attribute) > self.send(order_attribute)
466 end
467 end
468 self.move_to_child_of(parent)
469 return unless parent.children.count > 1 # Only need to order if there are multiple children.
470 if left # Self has a left neighbor.
471 self.move_to_right_of(left)
472 else # Self is the left most node.
473 self.move_to_left_of(parent.children[0])
474 end
475 end
476
477 def move_possible?(target)
478 self != target && # Can't target self
479 same_scope?(target) && # can't be in different scopes
480 # !(left..right).include?(target.left..target.right) # this needs tested more
481 # detect impossible move
482 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
483 end
484
485 def to_text
486 self_and_descendants.map do |node|
487 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
488 end.join("\n")
489 end
490
491 protected
492 def compute_level
493 node, nesting = self, 0
494 while (association = node.association(:parent)).loaded? && association.target
495 nesting += 1
496 node = node.parent
497 end if node.respond_to? :association
498 node == self ? ancestors.count : node.level + nesting
499 end
500
501 def without_self(scope)
502 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
503 end
504
505 # All nested set queries should use this nested_set_scope, which performs finds on
506 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
507 # declaration.
508 def nested_set_scope(options = {})
509 options = {:order => quoted_left_column_full_name}.merge(options)
510 scopes = Array(acts_as_nested_set_options[:scope])
511 options[:conditions] = scopes.inject({}) do |conditions,attr|
512 conditions.merge attr => self[attr]
513 end unless scopes.empty?
514 self.class.base_class.unscoped.scoped options
515 end
516
517 def store_new_parent
518 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
519 true # force callback to return true
520 end
521
522 def move_to_new_parent
523 if @move_to_new_parent_id.nil?
524 move_to_root
525 elsif @move_to_new_parent_id
526 move_to_child_of(@move_to_new_parent_id)
527 end
528 end
529
530 def set_depth!
531 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
532 in_tenacious_transaction do
533 reload
534
535 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
536 end
537 self[depth_column_name.to_sym] = self.level
538 end
539 end
540
541 def right_most_bound
542 right_most_node =
543 self.class.base_class.unscoped.
544 order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first
545 right_most_node ? (right_most_node[right_column_name] || 0) : 0
546 end
547
548 # on creation, set automatically lft and rgt to the end of the tree
549 def set_default_left_and_right
550 # adds the new node to the right of all existing nodes
551 self[left_column_name] = right_most_bound + 1
552 self[right_column_name] = right_most_bound + 2
553 end
554
555 def in_tenacious_transaction(&block)
556 retry_count = 0
557 begin
558 transaction(&block)
559 rescue ActiveRecord::StatementInvalid => error
560 raise unless connection.open_transactions.zero?
561 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
562 raise unless retry_count < 10
563 retry_count += 1
564 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
565 sleep(rand(retry_count)*0.1) # Aloha protocol
566 retry
567 end
568 end
569
570 # Prunes a branch off of the tree, shifting all of the elements on the right
571 # back to the left so the counts still work.
572 def destroy_descendants
573 return if right.nil? || left.nil? || skip_before_destroy
574
575 in_tenacious_transaction do
576 reload_nested_set
577 # select the rows in the model that extend past the deletion point and apply a lock
578 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
579 select(id).lock(true)
580
581 if acts_as_nested_set_options[:dependent] == :destroy
582 descendants.each do |model|
583 model.skip_before_destroy = true
584 model.destroy
585 end
586 else
587 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
588 delete_all
589 end
590
591 # update lefts and rights for remaining nodes
592 diff = right - left + 1
593 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
594 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
595 )
596
597 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
598 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
599 )
600
601 # Don't allow multiple calls to destroy to corrupt the set
602 self.skip_before_destroy = true
603 end
604 end
605
606 # reload left, right, and parent
607 def reload_nested_set
608 reload(
609 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
610 :lock => true
611 )
612 end
613
614 def move_to(target, position)
615 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
616 run_callbacks :move do
617 in_tenacious_transaction do
618 if target.is_a? self.class.base_class
619 target.reload_nested_set
620 elsif position != :root
621 # load object if node is not an object
622 target = nested_set_scope.find(target)
623 end
624 self.reload_nested_set
625
626 unless position == :root || move_possible?(target)
627 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
628 end
629
630 bound = case position
631 when :child; target[right_column_name]
632 when :left; target[left_column_name]
633 when :right; target[right_column_name] + 1
634 when :root; 1
635 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
636 end
637
638 if bound > self[right_column_name]
639 bound = bound - 1
640 other_bound = self[right_column_name] + 1
641 else
642 other_bound = self[left_column_name] - 1
643 end
644
645 # there would be no change
646 return if bound == self[right_column_name] || bound == self[left_column_name]
647
648 # we have defined the boundaries of two non-overlapping intervals,
649 # so sorting puts both the intervals and their boundaries in order
650 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
651
652 # select the rows in the model between a and d, and apply a lock
653 self.class.base_class.select('id').lock(true).where(
654 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
655 )
656
657 new_parent = case position
658 when :child; target.id
659 when :root; nil
660 else target[parent_column_name]
661 end
662
663 where_statement = ["not (#{quoted_left_column_name} = CASE " +
664 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
665 "THEN #{quoted_left_column_name} + :d - :b " +
666 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
667 "THEN #{quoted_left_column_name} + :a - :c " +
668 "ELSE #{quoted_left_column_name} END AND " +
669 "#{quoted_right_column_name} = CASE " +
670 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
671 "THEN #{quoted_right_column_name} + :d - :b " +
672 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
673 "THEN #{quoted_right_column_name} + :a - :c " +
674 "ELSE #{quoted_right_column_name} END AND " +
675 "#{quoted_parent_column_name} = CASE " +
676 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
677 "ELSE #{quoted_parent_column_name} END)" ,
678 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent} ]
679
680
681
74 def acts_as_nested_set_relate_children!
75 has_many_children_options = {
76 :class_name => self.base_class.to_s,
77 :foreign_key => parent_column_name,
78 :order => quoted_order_column_name,
79 :inverse_of => (:parent unless acts_as_nested_set_options[:polymorphic]),
80 }
682 81
683 self.nested_set_scope.where(*where_statement).update_all([
684 "#{quoted_left_column_name} = CASE " +
685 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
686 "THEN #{quoted_left_column_name} + :d - :b " +
687 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
688 "THEN #{quoted_left_column_name} + :a - :c " +
689 "ELSE #{quoted_left_column_name} END, " +
690 "#{quoted_right_column_name} = CASE " +
691 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
692 "THEN #{quoted_right_column_name} + :d - :b " +
693 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
694 "THEN #{quoted_right_column_name} + :a - :c " +
695 "ELSE #{quoted_right_column_name} END, " +
696 "#{quoted_parent_column_name} = CASE " +
697 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
698 "ELSE #{quoted_parent_column_name} END",
699 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
700 ])
701 end
702 target.reload_nested_set if target
703 self.set_depth!
704 self.descendants.each(&:save)
705 self.reload_nested_set
706 end
82 # Add callbacks, if they were supplied.. otherwise, we don't want them.
83 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
84 has_many_children_options.update(ar_callback => acts_as_nested_set_options[ar_callback]) if acts_as_nested_set_options[ar_callback]
707 85 end
708 86
87 has_many :children, has_many_children_options
709 88 end
710 89
711 # Mixed into both classes and instances to provide easy access to the column names
712 module Columns
713 def left_column_name
714 acts_as_nested_set_options[:left_column]
715 end
716
717 def right_column_name
718 acts_as_nested_set_options[:right_column]
719 end
720
721 def depth_column_name
722 acts_as_nested_set_options[:depth_column]
723 end
724
725 def parent_column_name
726 acts_as_nested_set_options[:parent_column]
727 end
728
729 def order_column
730 acts_as_nested_set_options[:order_column] || left_column_name
731 end
732
733 def scope_column_names
734 Array(acts_as_nested_set_options[:scope])
735 end
736
737 def quoted_left_column_name
738 connection.quote_column_name(left_column_name)
739 end
740
741 def quoted_right_column_name
742 connection.quote_column_name(right_column_name)
743 end
744
745 def quoted_depth_column_name
746 connection.quote_column_name(depth_column_name)
747 end
90 def acts_as_nested_set_relate_parent!
91 belongs_to :parent, :class_name => self.base_class.to_s,
92 :foreign_key => parent_column_name,
93 :counter_cache => acts_as_nested_set_options[:counter_cache],
94 :inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]),
95 :polymorphic => acts_as_nested_set_options[:polymorphic]
96 end
748 97
749 def quoted_parent_column_name
750 connection.quote_column_name(parent_column_name)
751 end
98 def acts_as_nested_set_default_options
99 {
100 :parent_column => 'parent_id',
101 :left_column => 'lft',
102 :right_column => 'rgt',
103 :depth_column => 'depth',
104 :dependent => :delete_all, # or :destroy
105 :polymorphic => false,
106 :counter_cache => false
107 }.freeze
108 end
752 109
753 def quoted_scope_column_names
754 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
755 end
110 def acts_as_nested_set_parse_options!(options)
111 options = acts_as_nested_set_default_options.merge(options)
756 112
757 def quoted_left_column_full_name
758 "#{quoted_table_name}.#{quoted_left_column_name}"
113 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
114 options[:scope] = "#{options[:scope]}_id".intern
759 115 end
760 116
761 def quoted_right_column_full_name
762 "#{quoted_table_name}.#{quoted_right_column_name}"
763 end
117 class_attribute :acts_as_nested_set_options
118 self.acts_as_nested_set_options = options
119 end
764 120
765 def quoted_parent_column_full_name
766 "#{quoted_table_name}.#{quoted_parent_column_name}"
121 def acts_as_nested_set_prevent_assignment_to_reserved_columns!
122 # no assignment to structure fields
123 [left_column_name, right_column_name, depth_column_name].each do |column|
124 module_eval <<-"end_eval", __FILE__, __LINE__
125 def #{column}=(x)
126 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
127 end
128 end_eval
767 129 end
768 130 end
769
770 131 end
771 132 end
772 133 end
@@ -38,51 +38,6 module CollectiveIdea #:nodoc:
38 38 end
39 39 result
40 40 end
41
42 # Returns options for select as nested_set_options, sorted by an specific column
43 # It requires passing a string with the name of the column to sort the set with
44 # You can exclude some items from the tree.
45 # You can pass a block receiving an item and returning the string displayed in the select.
46 #
47 # == Params
48 # * +class_or_item+ - Class name or top level times
49 # * +:column+ - Column to sort the set (this will sort each children for all root elements)
50 # * +mover+ - The item that is being move, used to exlude impossible moves
51 # * +&block+ - a block that will be used to display: { |item| ... item.name }
52 #
53 # == Usage
54 #
55 # <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
56 # "#{'–' * i.level} #{i.name}"
57 # }) %>
58 #
59 def sorted_nested_set_options(class_or_item, order, mover = nil)
60 if class_or_item.is_a? Array
61 items = class_or_item.reject { |e| !e.root? }
62 else
63 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
64 items = Array(class_or_item)
65 end
66 result = []
67 children = []
68 items.each do |root|
69 root.class.associate_parents(root.self_and_descendants).map do |i|
70 if mover.nil? || mover.new_record? || mover.move_possible?(i)
71 if !i.leaf?
72 children.sort_by! &order
73 children.each { |c| result << [yield(c), c.id] }
74 children = []
75 result << [yield(i), i.id]
76 else
77 children << i
78 end
79 end
80 end.compact
81 end
82 children.sort_by! &order
83 children.each { |c| result << [yield(c), c.id] }
84 result
85 end
86 41 end
87 42 end
88 43 end
@@ -1,3 +1,3
1 1 module AwesomeNestedSet
2 VERSION = '2.1.6' unless defined?(::AwesomeNestedSet::VERSION)
2 VERSION = '2.1.7' unless defined?(::AwesomeNestedSet::VERSION)
3 3 end
@@ -77,7 +77,10 describe "Helper" do
77 77 actual = nested_set_options(Category.all) do |c|
78 78 "#{'-' * c.level} #{c.name}"
79 79 end
80 actual.should == expected
80 actual.length.should == expected.length
81 expected.flatten.each do |node|
82 actual.flatten.should include(node)
83 end
81 84 end
82 85
83 86 it "test_nested_set_options_with_array_as_argument_with_mover" do
@@ -90,7 +93,10 describe "Helper" do
90 93 actual = nested_set_options(Category.all, categories(:child_2)) do |c|
91 94 "#{'-' * c.level} #{c.name}"
92 95 end
93 actual.should == expected
96 actual.length.should == expected.length
97 expected.flatten.each do |node|
98 actual.flatten.should include(node)
99 end
94 100 end
95 101 end
96 102 end
@@ -81,6 +81,12 describe "AwesomeNestedSet" do
81 81 Default.new.quoted_depth_column_name.should == quoted
82 82 end
83 83
84 it "quoted_order_column_name" do
85 quoted = Default.connection.quote_column_name('lft')
86 Default.quoted_order_column_name.should == quoted
87 Default.new.quoted_order_column_name.should == quoted
88 end
89
84 90 it "left_column_protected_from_assignment" do
85 91 lambda {
86 92 Category.new.lft = 1
@@ -104,7 +110,12 describe "AwesomeNestedSet" do
104 110 end
105 111
106 112 it "roots_class_method" do
107 Category.roots.should == Category.find_all_by_parent_id(nil)
113 found_by_us = Category.where(:parent_id => nil).to_a
114 found_by_roots = Category.roots.to_a
115 found_by_us.length.should == found_by_roots.length
116 found_by_us.each do |root|
117 found_by_roots.should include(root)
118 end
108 119 end
109 120
110 121 it "root_class_method" do
@@ -131,7 +142,6 describe "AwesomeNestedSet" do
131 142 end
132 143
133 144 it "leaves_class_method" do
134 Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1").should == Category.leaves
135 145 Category.leaves.count.should == 4
136 146 Category.leaves.should include(categories(:child_1))
137 147 Category.leaves.should include(categories(:child_2_1))
@@ -158,7 +168,7 describe "AwesomeNestedSet" do
158 168 it "self_and_ancestors" do
159 169 child = categories(:child_2_1)
160 170 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
161 self_and_ancestors.should == child.self_and_ancestors
171 child.self_and_ancestors.should == self_and_ancestors
162 172 end
163 173
164 174 it "ancestors" do
@@ -433,8 +443,8 describe "AwesomeNestedSet" do
433 443 categories(:child_2).parent.should be_nil
434 444 categories(:child_2).level.should == 0
435 445 categories(:child_2_1).level.should == 1
436 categories(:child_2).left.should == 1
437 categories(:child_2).right.should == 4
446 categories(:child_2).left.should == 7
447 categories(:child_2).right.should == 10
438 448 Category.valid?.should be_true
439 449 end
440 450
@@ -775,14 +785,14 describe "AwesomeNestedSet" do
775 785
776 786 it "quoting_of_multi_scope_column_names" do
777 787 ## Proper Array Assignment for different DBs as per their quoting column behavior
778 if Note.connection.adapter_name.match(/Oracle/)
788 if Note.connection.adapter_name.match(/oracle/i)
779 789 expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""]
780 elsif Note.connection.adapter_name.match(/Mysql/)
790 elsif Note.connection.adapter_name.match(/mysql/i)
781 791 expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
782 792 else
783 793 expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
784 794 end
785 expected_quoted_scope_column_names.should == Note.quoted_scope_column_names
795 Note.quoted_scope_column_names.should == expected_quoted_scope_column_names
786 796 end
787 797
788 798 it "equal_in_same_scope" do
@@ -2,6 +2,7 plugin_test_dir = File.dirname(__FILE__)
2 2
3 3 require 'rubygems'
4 4 require 'bundler/setup'
5 require 'pry'
5 6
6 7 require 'logger'
7 8 require 'active_record'
@@ -22,6 +23,7 require 'support/models'
22 23
23 24 require 'action_controller'
24 25 require 'rspec/rails'
26 require 'database_cleaner'
25 27 RSpec.configure do |config|
26 28 config.fixture_path = "#{plugin_test_dir}/fixtures"
27 29 config.use_transactional_fixtures = true
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now