##// 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,22 +1,19
1 language: ruby
1 language: ruby
2 notifications:
3 email:
4 - parndt@gmail.com
5 script: bundle exec rspec spec
2 script: bundle exec rspec spec
6 env:
3 env:
7 - DB=sqlite3
4 - DB=sqlite3
8 - DB=sqlite3mem
5 - DB=sqlite3mem
9 - DB=postgresql
6 - DB=postgresql
10 - DB=mysql
7 - DB=mysql
11 rvm:
8 rvm:
12 - 2.0.0
9 - 2.0.0
13 - 1.9.3
10 - 1.9.3
14 - 1.8.7
15 - rbx-19mode
11 - rbx-19mode
16 - jruby-19mode
12 - jruby-19mode
13 - 1.8.7
17 - rbx-18mode
14 - rbx-18mode
18 - jruby-18mode
15 - jruby-18mode
19 gemfile:
16 gemfile:
20 - gemfiles/Gemfile.rails-3.0.rb
17 - gemfiles/Gemfile.rails-3.0.rb
21 - gemfiles/Gemfile.rails-3.1.rb
18 - gemfiles/Gemfile.rails-3.1.rb
22 - gemfiles/Gemfile.rails-3.2.rb
19 - gemfiles/Gemfile.rails-3.2.rb
@@ -1,31 +1,32
1 gem 'combustion', :github => 'pat/combustion'
1 gem 'combustion', :github => 'pat/combustion', :branch => 'master'
2
2
3 source 'https://rubygems.org'
3 source 'https://rubygems.org'
4
4
5 gemspec :path => File.expand_path('../', __FILE__)
5 gemspec :path => File.expand_path('../', __FILE__)
6
6
7 platforms :jruby do
7 platforms :jruby do
8 gem 'activerecord-jdbcsqlite3-adapter'
8 gem 'activerecord-jdbcsqlite3-adapter'
9 gem 'activerecord-jdbcmysql-adapter'
9 gem 'activerecord-jdbcmysql-adapter'
10 gem 'jdbc-mysql'
10 gem 'activerecord-jdbcpostgresql-adapter'
11 gem 'activerecord-jdbcpostgresql-adapter'
11 gem 'jruby-openssl'
12 gem 'jruby-openssl'
12 end
13 end
13
14
14 platforms :ruby do
15 platforms :ruby do
15 gem 'sqlite3'
16 gem 'sqlite3'
16 gem 'mysql2', (MYSQL2_VERSION if defined? MYSQL2_VERSION)
17 gem 'mysql2', (MYSQL2_VERSION if defined? MYSQL2_VERSION)
17 gem 'pg'
18 gem 'pg'
18 end
19 end
19
20
20 RAILS_VERSION = nil unless defined? RAILS_VERSION
21 RAILS_VERSION = nil unless defined? RAILS_VERSION
21 gem 'railties', RAILS_VERSION
22 gem 'railties', RAILS_VERSION
22 gem 'activerecord', RAILS_VERSION
23 gem 'activerecord', RAILS_VERSION
23 gem 'actionpack', RAILS_VERSION
24 gem 'actionpack', RAILS_VERSION
24
25
25 # Add Oracle Adapters
26 # Add Oracle Adapters
26 # gem 'ruby-oci8'
27 # gem 'ruby-oci8'
27 # gem 'activerecord-oracle_enhanced-adapter'
28 # gem 'activerecord-oracle_enhanced-adapter'
28
29
29 # Debuggers
30 # Debuggers
30 # gem 'pry'
31 gem 'pry'
31 # gem 'pry-nav'
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 Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2.
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 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 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.
22 ## Usage
20 You can also have an optional field: depth:
21
23
22 class CreateCategories < ActiveRecord::Migration
24 To make use of `awesome_nested_set`, your model needs to have 3 fields:
23 def self.up
25 `lft`, `rgt`, and `parent_id`. The names of these fields are configurable.
24 create_table :categories do |t|
26 You can also have an optional field, `depth`:
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
32
27
33 def self.down
28 ```ruby
34 drop_table :categories
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 end
37 end
36 end
38 end
37
39
38 Enable the nested set functionality by declaring acts_as_nested_set on your model
40 def self.down
39
41 drop_table :categories
40 class Category < ActiveRecord::Base
41 acts_as_nested_set
42 end
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
56 ## Callbacks
51 acts_as_nested_set
52
57
53 after_move :rebuild_slug
58 There are three callbacks called when moving a node:
54 around_move :da_fancy_things_around
59 `before_move`, `after_move` and `around_move`.
55
60
56 private
61 ```ruby
57
62 class Category < ActiveRecord::Base
58 def rebuild_slug
63 acts_as_nested_set
59 # do whatever
60 end
61
64
62 def da_fancy_things_around
65 after_move :rebuild_slug
63 # do something...
66 around_move :da_fancy_things_around
64 yield # actually moves
67
65 # do something else...
68 private
66 end
69
70 def rebuild_slug
71 # do whatever
67 end
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 Beside this there are also hooks to act on the newly added or removed children.
82 Beside this there are also hooks to act on the newly added or removed children.
70
83
71 class Category < ActiveRecord::Base
84 ```ruby
72 acts_as_nested_set :before_add => :do_before_add_stuff,
85 class Category < ActiveRecord::Base
73 :after_add => :do_after_add_stuff,
86 acts_as_nested_set :before_add => :do_before_add_stuff,
74 :before_remove => :do_before_remove_stuff,
87 :after_add => :do_after_add_stuff,
75 :after_remove => :do_after_remove_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)
93 def do_before_add_stuff(child_node)
80 # do whatever with the child
94 # do whatever with the child
81 end
95 end
82
83 def do_after_add_stuff(child_node)
84 # do whatever with the child
85 end
86
96
87 def do_before_remove_stuff(child_node)
97 def do_after_add_stuff(child_node)
88 # do whatever with the child
98 # do whatever with the child
89 end
99 end
90
100
91 def do_after_remove_stuff(child_node)
101 def do_before_remove_stuff(child_node)
92 # do whatever with the child
102 # do whatever with the child
93 end
94 end
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
115 ```ruby
102 acts_as_nested_set
116 class Category < ActiveRecord::Base
103 attr_accessible :name, :parent_id
117 acts_as_nested_set
104 end
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
124 ```ruby
109 acts_as_nested_set
125 class Category < ActiveRecord::Base
110 attr_protected :lft, :rgt
126 acts_as_nested_set
111 end
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 Your tree will be converted to a valid nested set. Awesome!
139 Your tree will be converted to a valid nested set. Awesome!
120
140
121 == View Helper
141 ## View Helper
122
142
123 The view helper is called #nested_set_options.
143 The view helper is called #nested_set_options.
124
144
125 Example usage:
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 You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
157 You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
136
158
137 == How to contribute
159 ## 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:
146
160
147 1. Fork the project on GitHub.
161 Please see the ['Contributing' document](CONTRIBUTING.md).
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.
152
162
153 Copyright ©2008 Collective Idea, released under the MIT license
163 Copyright © 2008 - 2013 Collective Idea, released under the MIT license
@@ -1,24 +1,24
1 # -*- encoding: utf-8 -*-
1 # -*- encoding: utf-8 -*-
2 require File.expand_path('../lib/awesome_nested_set/version', __FILE__)
2 require File.expand_path('../lib/awesome_nested_set/version', __FILE__)
3
3
4 Gem::Specification.new do |s|
4 Gem::Specification.new do |s|
5 s.name = %q{awesome_nested_set}
5 s.name = %q{awesome_nested_set}
6 s.version = ::AwesomeNestedSet::VERSION
6 s.version = ::AwesomeNestedSet::VERSION
7 s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
7 s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
8 s.description = %q{An awesome nested set implementation for Active Record}
8 s.description = %q{An awesome nested set implementation for Active Record}
9 s.email = %q{info@collectiveidea.com}
9 s.email = %q{info@collectiveidea.com}
10 s.extra_rdoc_files = %w[README.rdoc]
10 s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.md CHANGELOG)
11 s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG)
12 s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
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 s.require_paths = ["lib"]
13 s.require_paths = ["lib"]
15 s.rubygems_version = %q{1.3.6}
14 s.rubygems_version = %q{1.3.6}
16 s.summary = %q{An awesome nested set implementation for Active Record}
15 s.summary = %q{An awesome nested set implementation for Active Record}
17 s.license = %q{MIT}
16 s.license = %q{MIT}
18
17
19 s.add_runtime_dependency 'activerecord', '>= 3.0.0'
18 s.add_runtime_dependency 'activerecord', '>= 3.0.0'
20
19
21 s.add_development_dependency 'rspec-rails', '~> 2.12'
20 s.add_development_dependency 'rspec-rails', '~> 2.12'
22 s.add_development_dependency 'rake', '~> 10'
21 s.add_development_dependency 'rake', '~> 10'
23 s.add_development_dependency 'combustion', '>= 0.3.3'
22 s.add_development_dependency 'combustion', '>= 0.3.3'
23 s.add_development_dependency 'database_cleaner'
24 end
24 end
@@ -1,8 +1,8
1 require 'awesome_nested_set/awesome_nested_set'
1 require 'awesome_nested_set/awesome_nested_set'
2 require 'active_record'
2 require 'active_record'
3 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
3 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
4
4
5 if defined?(ActionView)
5 if defined?(ActionView)
6 require 'awesome_nested_set/helper'
6 require 'awesome_nested_set/helper'
7 ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
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,772 +1,133
1 require 'awesome_nested_set/columns'
2 require 'awesome_nested_set/model'
3
1 module CollectiveIdea #:nodoc:
4 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
5 module Acts #:nodoc:
3 module NestedSet #:nodoc:
6 module NestedSet #:nodoc:
4
7
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
8 # 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
9 # 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
10 # 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!
11 # sql queries. But everything is done here by this module!
9 #
12 #
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
13 # 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).
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 #
15 #
13 # == API
16 # == API
14 #
17 #
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
18 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 # by another easier.
19 # by another easier.
17 #
20 #
18 # item.children.create(:name => "child1")
21 # item.children.create(:name => "child1")
19 #
22 #
20
23
21 # Configuration options are:
24 # Configuration options are:
22 #
25 #
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
26 # * +: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"
27 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
28 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:depth_column+ - column name for the depth data, default "depth"
29 # * +:depth_column+ - column name for the depth data, default "depth"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
30 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
28 # (if it hasn't been already) and use that as the foreign key restriction. You
31 # (if it hasn't been already) and use that as the foreign key restriction. You
29 # can also pass an array to scope by multiple attributes.
32 # can also pass an array to scope by multiple attributes.
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
33 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
34 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
32 # child objects are destroyed alongside this object by calling their destroy
35 # child objects are destroyed alongside this object by calling their destroy
33 # method. If set to :delete_all (default), all the child objects are deleted
36 # method. If set to :delete_all (default), all the child objects are deleted
34 # without calling their destroy method.
37 # without calling their destroy method.
35 # * +:counter_cache+ adds a counter cache for the number of children.
38 # * +:counter_cache+ adds a counter cache for the number of children.
36 # defaults to false.
39 # defaults to false.
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
40 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
41 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
42 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
40 #
43 #
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
44 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
45 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
43 # to acts_as_nested_set models
46 # to acts_as_nested_set models
44 def acts_as_nested_set(options = {})
47 def acts_as_nested_set(options = {})
45 options = {
48 acts_as_nested_set_parse_options! 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
61
49
62 include CollectiveIdea::Acts::NestedSet::Model
50 include Model
63 include Columns
51 include Columns
64 extend Columns
52 extend Columns
65
53
66 belongs_to :parent, :class_name => self.base_class.to_s,
54 acts_as_nested_set_relate_parent!
67 :foreign_key => parent_column_name,
55 acts_as_nested_set_relate_children!
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
85
56
86 attr_accessor :skip_before_destroy
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 before_create :set_default_left_and_right
66 before_create :set_default_left_and_right
89 before_save :store_new_parent
67 before_save :store_new_parent
90 after_save :move_to_new_parent, :set_depth!
68 after_save :move_to_new_parent, :set_depth!
91 before_destroy :destroy_descendants
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 define_model_callbacks :move
71 define_model_callbacks :move
103 end
72 end
104
73
105 module Model
74 def acts_as_nested_set_relate_children!
106 extend ActiveSupport::Concern
75 has_many_children_options = {
107
76 :class_name => self.base_class.to_s,
108 included do
77 :foreign_key => parent_column_name,
109 delegate :quoted_table_name, :to => self
78 :order => quoted_order_column_name,
110 end
79 :inverse_of => (:parent unless acts_as_nested_set_options[:polymorphic]),
111
80 }
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
682
81
683 self.nested_set_scope.where(*where_statement).update_all([
82 # Add callbacks, if they were supplied.. otherwise, we don't want them.
684 "#{quoted_left_column_name} = CASE " +
83 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
685 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
84 has_many_children_options.update(ar_callback => acts_as_nested_set_options[ar_callback]) if acts_as_nested_set_options[ar_callback]
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
707 end
85 end
708
86
87 has_many :children, has_many_children_options
709 end
88 end
710
89
711 # Mixed into both classes and instances to provide easy access to the column names
90 def acts_as_nested_set_relate_parent!
712 module Columns
91 belongs_to :parent, :class_name => self.base_class.to_s,
713 def left_column_name
92 :foreign_key => parent_column_name,
714 acts_as_nested_set_options[:left_column]
93 :counter_cache => acts_as_nested_set_options[:counter_cache],
715 end
94 :inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]),
716
95 :polymorphic => acts_as_nested_set_options[:polymorphic]
717 def right_column_name
96 end
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
748
97
749 def quoted_parent_column_name
98 def acts_as_nested_set_default_options
750 connection.quote_column_name(parent_column_name)
99 {
751 end
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
110 def acts_as_nested_set_parse_options!(options)
754 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
111 options = acts_as_nested_set_default_options.merge(options)
755 end
756
112
757 def quoted_left_column_full_name
113 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
758 "#{quoted_table_name}.#{quoted_left_column_name}"
114 options[:scope] = "#{options[:scope]}_id".intern
759 end
115 end
760
116
761 def quoted_right_column_full_name
117 class_attribute :acts_as_nested_set_options
762 "#{quoted_table_name}.#{quoted_right_column_name}"
118 self.acts_as_nested_set_options = options
763 end
119 end
764
120
765 def quoted_parent_column_full_name
121 def acts_as_nested_set_prevent_assignment_to_reserved_columns!
766 "#{quoted_table_name}.#{quoted_parent_column_name}"
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 end
129 end
768 end
130 end
769
770 end
131 end
771 end
132 end
772 end
133 end
@@ -1,89 +1,44
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 module CollectiveIdea #:nodoc:
2 module CollectiveIdea #:nodoc:
3 module Acts #:nodoc:
3 module Acts #:nodoc:
4 module NestedSet #:nodoc:
4 module NestedSet #:nodoc:
5 # This module provides some helpers for the model classes using acts_as_nested_set.
5 # This module provides some helpers for the model classes using acts_as_nested_set.
6 # It is included by default in all views.
6 # It is included by default in all views.
7 #
7 #
8 module Helper
8 module Helper
9 # Returns options for select.
9 # Returns options for select.
10 # You can exclude some items from the tree.
10 # You can exclude some items from the tree.
11 # You can pass a block receiving an item and returning the string displayed in the select.
11 # You can pass a block receiving an item and returning the string displayed in the select.
12 #
12 #
13 # == Params
13 # == Params
14 # * +class_or_item+ - Class name or top level times
14 # * +class_or_item+ - Class name or top level times
15 # * +mover+ - The item that is being move, used to exlude impossible moves
15 # * +mover+ - The item that is being move, used to exlude impossible moves
16 # * +&block+ - a block that will be used to display: { |item| ... item.name }
16 # * +&block+ - a block that will be used to display: { |item| ... item.name }
17 #
17 #
18 # == Usage
18 # == Usage
19 #
19 #
20 # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
20 # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
21 # "#{'–' * i.level} #{i.name}"
21 # "#{'–' * i.level} #{i.name}"
22 # }) %>
22 # }) %>
23 #
23 #
24 def nested_set_options(class_or_item, mover = nil)
24 def nested_set_options(class_or_item, mover = nil)
25 if class_or_item.is_a? Array
25 if class_or_item.is_a? Array
26 items = class_or_item.reject { |e| !e.root? }
26 items = class_or_item.reject { |e| !e.root? }
27 else
27 else
28 class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
28 class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
29 items = Array(class_or_item)
29 items = Array(class_or_item)
30 end
30 end
31 result = []
31 result = []
32 items.each do |root|
32 items.each do |root|
33 result += root.class.associate_parents(root.self_and_descendants).map do |i|
33 result += root.class.associate_parents(root.self_and_descendants).map do |i|
34 if mover.nil? || mover.new_record? || mover.move_possible?(i)
34 if mover.nil? || mover.new_record? || mover.move_possible?(i)
35 [yield(i), i.id]
35 [yield(i), i.id]
36 end
36 end
37 end.compact
37 end.compact
38 end
38 end
39 result
39 result
40 end
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 end
41 end
87 end
42 end
88 end
43 end
89 end
44 end
@@ -1,3 +1,3
1 module AwesomeNestedSet
1 module AwesomeNestedSet
2 VERSION = '2.1.6' unless defined?(::AwesomeNestedSet::VERSION)
2 VERSION = '2.1.7' unless defined?(::AwesomeNestedSet::VERSION)
3 end
3 end
@@ -1,96 +1,102
1 require 'spec_helper'
1 require 'spec_helper'
2 require 'awesome_nested_set/helper'
2 require 'awesome_nested_set/helper'
3
3
4 describe "Helper" do
4 describe "Helper" do
5 include CollectiveIdea::Acts::NestedSet::Helper
5 include CollectiveIdea::Acts::NestedSet::Helper
6
6
7 before(:all) do
7 before(:all) do
8 self.class.fixtures :categories
8 self.class.fixtures :categories
9 end
9 end
10
10
11 describe "nested_set_options" do
11 describe "nested_set_options" do
12 it "test_nested_set_options" do
12 it "test_nested_set_options" do
13 expected = [
13 expected = [
14 [" Top Level", 1],
14 [" Top Level", 1],
15 ["- Child 1", 2],
15 ["- Child 1", 2],
16 ['- Child 2', 3],
16 ['- Child 2', 3],
17 ['-- Child 2.1', 4],
17 ['-- Child 2.1', 4],
18 ['- Child 3', 5],
18 ['- Child 3', 5],
19 [" Top Level 2", 6]
19 [" Top Level 2", 6]
20 ]
20 ]
21 actual = nested_set_options(Category.scoped) do |c|
21 actual = nested_set_options(Category.scoped) do |c|
22 "#{'-' * c.level} #{c.name}"
22 "#{'-' * c.level} #{c.name}"
23 end
23 end
24 actual.should == expected
24 actual.should == expected
25 end
25 end
26
26
27 it "test_nested_set_options_with_mover" do
27 it "test_nested_set_options_with_mover" do
28 expected = [
28 expected = [
29 [" Top Level", 1],
29 [" Top Level", 1],
30 ["- Child 1", 2],
30 ["- Child 1", 2],
31 ['- Child 3', 5],
31 ['- Child 3', 5],
32 [" Top Level 2", 6]
32 [" Top Level 2", 6]
33 ]
33 ]
34 actual = nested_set_options(Category.scoped, categories(:child_2)) do |c|
34 actual = nested_set_options(Category.scoped, categories(:child_2)) do |c|
35 "#{'-' * c.level} #{c.name}"
35 "#{'-' * c.level} #{c.name}"
36 end
36 end
37 actual.should == expected
37 actual.should == expected
38 end
38 end
39
39
40 it "test_nested_set_options_with_class_as_argument" do
40 it "test_nested_set_options_with_class_as_argument" do
41 expected = [
41 expected = [
42 [" Top Level", 1],
42 [" Top Level", 1],
43 ["- Child 1", 2],
43 ["- Child 1", 2],
44 ['- Child 2', 3],
44 ['- Child 2', 3],
45 ['-- Child 2.1', 4],
45 ['-- Child 2.1', 4],
46 ['- Child 3', 5],
46 ['- Child 3', 5],
47 [" Top Level 2", 6]
47 [" Top Level 2", 6]
48 ]
48 ]
49 actual = nested_set_options(Category) do |c|
49 actual = nested_set_options(Category) do |c|
50 "#{'-' * c.level} #{c.name}"
50 "#{'-' * c.level} #{c.name}"
51 end
51 end
52 actual.should == expected
52 actual.should == expected
53 end
53 end
54
54
55 it "test_nested_set_options_with_class_as_argument_with_mover" do
55 it "test_nested_set_options_with_class_as_argument_with_mover" do
56 expected = [
56 expected = [
57 [" Top Level", 1],
57 [" Top Level", 1],
58 ["- Child 1", 2],
58 ["- Child 1", 2],
59 ['- Child 3', 5],
59 ['- Child 3', 5],
60 [" Top Level 2", 6]
60 [" Top Level 2", 6]
61 ]
61 ]
62 actual = nested_set_options(Category, categories(:child_2)) do |c|
62 actual = nested_set_options(Category, categories(:child_2)) do |c|
63 "#{'-' * c.level} #{c.name}"
63 "#{'-' * c.level} #{c.name}"
64 end
64 end
65 actual.should == expected
65 actual.should == expected
66 end
66 end
67
67
68 it "test_nested_set_options_with_array_as_argument_without_mover" do
68 it "test_nested_set_options_with_array_as_argument_without_mover" do
69 expected = [
69 expected = [
70 [" Top Level", 1],
70 [" Top Level", 1],
71 ["- Child 1", 2],
71 ["- Child 1", 2],
72 ['- Child 2', 3],
72 ['- Child 2', 3],
73 ['-- Child 2.1', 4],
73 ['-- Child 2.1', 4],
74 ['- Child 3', 5],
74 ['- Child 3', 5],
75 [" Top Level 2", 6]
75 [" Top Level 2", 6]
76 ]
76 ]
77 actual = nested_set_options(Category.all) do |c|
77 actual = nested_set_options(Category.all) do |c|
78 "#{'-' * c.level} #{c.name}"
78 "#{'-' * c.level} #{c.name}"
79 end
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 end
84 end
82
85
83 it "test_nested_set_options_with_array_as_argument_with_mover" do
86 it "test_nested_set_options_with_array_as_argument_with_mover" do
84 expected = [
87 expected = [
85 [" Top Level", 1],
88 [" Top Level", 1],
86 ["- Child 1", 2],
89 ["- Child 1", 2],
87 ['- Child 3', 5],
90 ['- Child 3', 5],
88 [" Top Level 2", 6]
91 [" Top Level 2", 6]
89 ]
92 ]
90 actual = nested_set_options(Category.all, categories(:child_2)) do |c|
93 actual = nested_set_options(Category.all, categories(:child_2)) do |c|
91 "#{'-' * c.level} #{c.name}"
94 "#{'-' * c.level} #{c.name}"
92 end
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 end
100 end
95 end
101 end
96 end
102 end
@@ -1,1088 +1,1098
1 require 'spec_helper'
1 require 'spec_helper'
2
2
3 describe "AwesomeNestedSet" do
3 describe "AwesomeNestedSet" do
4 before(:all) do
4 before(:all) do
5 self.class.fixtures :categories, :departments, :notes, :things, :brokens
5 self.class.fixtures :categories, :departments, :notes, :things, :brokens
6 end
6 end
7
7
8 describe "defaults" do
8 describe "defaults" do
9 it "should have left_column_default" do
9 it "should have left_column_default" do
10 Default.acts_as_nested_set_options[:left_column].should == 'lft'
10 Default.acts_as_nested_set_options[:left_column].should == 'lft'
11 end
11 end
12
12
13 it "should have right_column_default" do
13 it "should have right_column_default" do
14 Default.acts_as_nested_set_options[:right_column].should == 'rgt'
14 Default.acts_as_nested_set_options[:right_column].should == 'rgt'
15 end
15 end
16
16
17 it "should have parent_column_default" do
17 it "should have parent_column_default" do
18 Default.acts_as_nested_set_options[:parent_column].should == 'parent_id'
18 Default.acts_as_nested_set_options[:parent_column].should == 'parent_id'
19 end
19 end
20
20
21 it "should have scope_default" do
21 it "should have scope_default" do
22 Default.acts_as_nested_set_options[:scope].should be_nil
22 Default.acts_as_nested_set_options[:scope].should be_nil
23 end
23 end
24
24
25 it "should have left_column_name" do
25 it "should have left_column_name" do
26 Default.left_column_name.should == 'lft'
26 Default.left_column_name.should == 'lft'
27 Default.new.left_column_name.should == 'lft'
27 Default.new.left_column_name.should == 'lft'
28 RenamedColumns.left_column_name.should == 'red'
28 RenamedColumns.left_column_name.should == 'red'
29 RenamedColumns.new.left_column_name.should == 'red'
29 RenamedColumns.new.left_column_name.should == 'red'
30 end
30 end
31
31
32 it "should have right_column_name" do
32 it "should have right_column_name" do
33 Default.right_column_name.should == 'rgt'
33 Default.right_column_name.should == 'rgt'
34 Default.new.right_column_name.should == 'rgt'
34 Default.new.right_column_name.should == 'rgt'
35 RenamedColumns.right_column_name.should == 'black'
35 RenamedColumns.right_column_name.should == 'black'
36 RenamedColumns.new.right_column_name.should == 'black'
36 RenamedColumns.new.right_column_name.should == 'black'
37 end
37 end
38
38
39 it "has a depth_column_name" do
39 it "has a depth_column_name" do
40 Default.depth_column_name.should == 'depth'
40 Default.depth_column_name.should == 'depth'
41 Default.new.depth_column_name.should == 'depth'
41 Default.new.depth_column_name.should == 'depth'
42 RenamedColumns.depth_column_name.should == 'pitch'
42 RenamedColumns.depth_column_name.should == 'pitch'
43 RenamedColumns.depth_column_name.should == 'pitch'
43 RenamedColumns.depth_column_name.should == 'pitch'
44 end
44 end
45
45
46 it "should have parent_column_name" do
46 it "should have parent_column_name" do
47 Default.parent_column_name.should == 'parent_id'
47 Default.parent_column_name.should == 'parent_id'
48 Default.new.parent_column_name.should == 'parent_id'
48 Default.new.parent_column_name.should == 'parent_id'
49 RenamedColumns.parent_column_name.should == 'mother_id'
49 RenamedColumns.parent_column_name.should == 'mother_id'
50 RenamedColumns.new.parent_column_name.should == 'mother_id'
50 RenamedColumns.new.parent_column_name.should == 'mother_id'
51 end
51 end
52 end
52 end
53
53
54 it "creation_with_altered_column_names" do
54 it "creation_with_altered_column_names" do
55 lambda {
55 lambda {
56 RenamedColumns.create!()
56 RenamedColumns.create!()
57 }.should_not raise_exception
57 }.should_not raise_exception
58 end
58 end
59
59
60 it "creation when existing record has nil left column" do
60 it "creation when existing record has nil left column" do
61 assert_nothing_raised do
61 assert_nothing_raised do
62 Broken.create!
62 Broken.create!
63 end
63 end
64 end
64 end
65
65
66 it "quoted_left_column_name" do
66 it "quoted_left_column_name" do
67 quoted = Default.connection.quote_column_name('lft')
67 quoted = Default.connection.quote_column_name('lft')
68 Default.quoted_left_column_name.should == quoted
68 Default.quoted_left_column_name.should == quoted
69 Default.new.quoted_left_column_name.should == quoted
69 Default.new.quoted_left_column_name.should == quoted
70 end
70 end
71
71
72 it "quoted_right_column_name" do
72 it "quoted_right_column_name" do
73 quoted = Default.connection.quote_column_name('rgt')
73 quoted = Default.connection.quote_column_name('rgt')
74 Default.quoted_right_column_name.should == quoted
74 Default.quoted_right_column_name.should == quoted
75 Default.new.quoted_right_column_name.should == quoted
75 Default.new.quoted_right_column_name.should == quoted
76 end
76 end
77
77
78 it "quoted_depth_column_name" do
78 it "quoted_depth_column_name" do
79 quoted = Default.connection.quote_column_name('depth')
79 quoted = Default.connection.quote_column_name('depth')
80 Default.quoted_depth_column_name.should == quoted
80 Default.quoted_depth_column_name.should == quoted
81 Default.new.quoted_depth_column_name.should == quoted
81 Default.new.quoted_depth_column_name.should == quoted
82 end
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 it "left_column_protected_from_assignment" do
90 it "left_column_protected_from_assignment" do
85 lambda {
91 lambda {
86 Category.new.lft = 1
92 Category.new.lft = 1
87 }.should raise_exception(ActiveRecord::ActiveRecordError)
93 }.should raise_exception(ActiveRecord::ActiveRecordError)
88 end
94 end
89
95
90 it "right_column_protected_from_assignment" do
96 it "right_column_protected_from_assignment" do
91 lambda {
97 lambda {
92 Category.new.rgt = 1
98 Category.new.rgt = 1
93 }.should raise_exception(ActiveRecord::ActiveRecordError)
99 }.should raise_exception(ActiveRecord::ActiveRecordError)
94 end
100 end
95
101
96 it "depth_column_protected_from_assignment" do
102 it "depth_column_protected_from_assignment" do
97 lambda {
103 lambda {
98 Category.new.depth = 1
104 Category.new.depth = 1
99 }.should raise_exception(ActiveRecord::ActiveRecordError)
105 }.should raise_exception(ActiveRecord::ActiveRecordError)
100 end
106 end
101
107
102 it "scoped_appends_id" do
108 it "scoped_appends_id" do
103 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
109 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
104 end
110 end
105
111
106 it "roots_class_method" do
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 end
119 end
109
120
110 it "root_class_method" do
121 it "root_class_method" do
111 Category.root.should == categories(:top_level)
122 Category.root.should == categories(:top_level)
112 end
123 end
113
124
114 it "root" do
125 it "root" do
115 categories(:child_3).root.should == categories(:top_level)
126 categories(:child_3).root.should == categories(:top_level)
116 end
127 end
117
128
118 it "root when not persisted and parent_column_name value is self" do
129 it "root when not persisted and parent_column_name value is self" do
119 new_category = Category.new
130 new_category = Category.new
120 new_category.root.should == new_category
131 new_category.root.should == new_category
121 end
132 end
122
133
123 it "root when not persisted and parent_column_name value is set" do
134 it "root when not persisted and parent_column_name value is set" do
124 last_category = Category.last
135 last_category = Category.last
125 Category.new(Default.parent_column_name => last_category.id).root.should == last_category.root
136 Category.new(Default.parent_column_name => last_category.id).root.should == last_category.root
126 end
137 end
127
138
128 it "root?" do
139 it "root?" do
129 categories(:top_level).root?.should be_true
140 categories(:top_level).root?.should be_true
130 categories(:top_level_2).root?.should be_true
141 categories(:top_level_2).root?.should be_true
131 end
142 end
132
143
133 it "leaves_class_method" do
144 it "leaves_class_method" do
134 Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1").should == Category.leaves
135 Category.leaves.count.should == 4
145 Category.leaves.count.should == 4
136 Category.leaves.should include(categories(:child_1))
146 Category.leaves.should include(categories(:child_1))
137 Category.leaves.should include(categories(:child_2_1))
147 Category.leaves.should include(categories(:child_2_1))
138 Category.leaves.should include(categories(:child_3))
148 Category.leaves.should include(categories(:child_3))
139 Category.leaves.should include(categories(:top_level_2))
149 Category.leaves.should include(categories(:top_level_2))
140 end
150 end
141
151
142 it "leaf" do
152 it "leaf" do
143 categories(:child_1).leaf?.should be_true
153 categories(:child_1).leaf?.should be_true
144 categories(:child_2_1).leaf?.should be_true
154 categories(:child_2_1).leaf?.should be_true
145 categories(:child_3).leaf?.should be_true
155 categories(:child_3).leaf?.should be_true
146 categories(:top_level_2).leaf?.should be_true
156 categories(:top_level_2).leaf?.should be_true
147
157
148 categories(:top_level).leaf?.should be_false
158 categories(:top_level).leaf?.should be_false
149 categories(:child_2).leaf?.should be_false
159 categories(:child_2).leaf?.should be_false
150 Category.new.leaf?.should be_false
160 Category.new.leaf?.should be_false
151 end
161 end
152
162
153
163
154 it "parent" do
164 it "parent" do
155 categories(:child_2_1).parent.should == categories(:child_2)
165 categories(:child_2_1).parent.should == categories(:child_2)
156 end
166 end
157
167
158 it "self_and_ancestors" do
168 it "self_and_ancestors" do
159 child = categories(:child_2_1)
169 child = categories(:child_2_1)
160 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
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 end
172 end
163
173
164 it "ancestors" do
174 it "ancestors" do
165 child = categories(:child_2_1)
175 child = categories(:child_2_1)
166 ancestors = [categories(:top_level), categories(:child_2)]
176 ancestors = [categories(:top_level), categories(:child_2)]
167 ancestors.should == child.ancestors
177 ancestors.should == child.ancestors
168 end
178 end
169
179
170 it "self_and_siblings" do
180 it "self_and_siblings" do
171 child = categories(:child_2)
181 child = categories(:child_2)
172 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
182 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
173 self_and_siblings.should == child.self_and_siblings
183 self_and_siblings.should == child.self_and_siblings
174 lambda do
184 lambda do
175 tops = [categories(:top_level), categories(:top_level_2)]
185 tops = [categories(:top_level), categories(:top_level_2)]
176 assert_equal tops, categories(:top_level).self_and_siblings
186 assert_equal tops, categories(:top_level).self_and_siblings
177 end.should_not raise_exception
187 end.should_not raise_exception
178 end
188 end
179
189
180 it "siblings" do
190 it "siblings" do
181 child = categories(:child_2)
191 child = categories(:child_2)
182 siblings = [categories(:child_1), categories(:child_3)]
192 siblings = [categories(:child_1), categories(:child_3)]
183 siblings.should == child.siblings
193 siblings.should == child.siblings
184 end
194 end
185
195
186 it "leaves" do
196 it "leaves" do
187 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3)]
197 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3)]
188 categories(:top_level).leaves.should == leaves
198 categories(:top_level).leaves.should == leaves
189 end
199 end
190
200
191 describe "level" do
201 describe "level" do
192 it "returns the correct level" do
202 it "returns the correct level" do
193 categories(:top_level).level.should == 0
203 categories(:top_level).level.should == 0
194 categories(:child_1).level.should == 1
204 categories(:child_1).level.should == 1
195 categories(:child_2_1).level.should == 2
205 categories(:child_2_1).level.should == 2
196 end
206 end
197
207
198 context "given parent associations are loaded" do
208 context "given parent associations are loaded" do
199 it "returns the correct level" do
209 it "returns the correct level" do
200 child = categories(:child_1)
210 child = categories(:child_1)
201 if child.respond_to?(:association)
211 if child.respond_to?(:association)
202 child.association(:parent).load_target
212 child.association(:parent).load_target
203 child.parent.association(:parent).load_target
213 child.parent.association(:parent).load_target
204 child.level.should == 1
214 child.level.should == 1
205 else
215 else
206 pending 'associations not used where child#association is not a method'
216 pending 'associations not used where child#association is not a method'
207 end
217 end
208 end
218 end
209 end
219 end
210 end
220 end
211
221
212 describe "depth" do
222 describe "depth" do
213 let(:lawyers) { Category.create!(:name => "lawyers") }
223 let(:lawyers) { Category.create!(:name => "lawyers") }
214 let(:us) { Category.create!(:name => "United States") }
224 let(:us) { Category.create!(:name => "United States") }
215 let(:new_york) { Category.create!(:name => "New York") }
225 let(:new_york) { Category.create!(:name => "New York") }
216 let(:patent) { Category.create!(:name => "Patent Law") }
226 let(:patent) { Category.create!(:name => "Patent Law") }
217
227
218 before(:each) do
228 before(:each) do
219 # lawyers > us > new_york > patent
229 # lawyers > us > new_york > patent
220 us.move_to_child_of(lawyers)
230 us.move_to_child_of(lawyers)
221 new_york.move_to_child_of(us)
231 new_york.move_to_child_of(us)
222 patent.move_to_child_of(new_york)
232 patent.move_to_child_of(new_york)
223 [lawyers, us, new_york, patent].each(&:reload)
233 [lawyers, us, new_york, patent].each(&:reload)
224 end
234 end
225
235
226 it "updates depth when moved into child position" do
236 it "updates depth when moved into child position" do
227 lawyers.depth.should == 0
237 lawyers.depth.should == 0
228 us.depth.should == 1
238 us.depth.should == 1
229 new_york.depth.should == 2
239 new_york.depth.should == 2
230 patent.depth.should == 3
240 patent.depth.should == 3
231 end
241 end
232
242
233 it "updates depth of all descendants when parent is moved" do
243 it "updates depth of all descendants when parent is moved" do
234 # lawyers
244 # lawyers
235 # us > new_york > patent
245 # us > new_york > patent
236 us.move_to_right_of(lawyers)
246 us.move_to_right_of(lawyers)
237 [lawyers, us, new_york, patent].each(&:reload)
247 [lawyers, us, new_york, patent].each(&:reload)
238 us.depth.should == 0
248 us.depth.should == 0
239 new_york.depth.should == 1
249 new_york.depth.should == 1
240 patent.depth.should == 2
250 patent.depth.should == 2
241 end
251 end
242 end
252 end
243
253
244 it "depth is magic and does not apply when column is missing" do
254 it "depth is magic and does not apply when column is missing" do
245 lambda { NoDepth.create!(:name => "shallow") }.should_not raise_error
255 lambda { NoDepth.create!(:name => "shallow") }.should_not raise_error
246 lambda { NoDepth.first.save }.should_not raise_error
256 lambda { NoDepth.first.save }.should_not raise_error
247 lambda { NoDepth.rebuild! }.should_not raise_error
257 lambda { NoDepth.rebuild! }.should_not raise_error
248
258
249 NoDepth.method_defined?(:depth).should be_false
259 NoDepth.method_defined?(:depth).should be_false
250 NoDepth.first.respond_to?(:depth).should be_false
260 NoDepth.first.respond_to?(:depth).should be_false
251 end
261 end
252
262
253 it "has_children?" do
263 it "has_children?" do
254 categories(:child_2_1).children.empty?.should be_true
264 categories(:child_2_1).children.empty?.should be_true
255 categories(:child_2).children.empty?.should be_false
265 categories(:child_2).children.empty?.should be_false
256 categories(:top_level).children.empty?.should be_false
266 categories(:top_level).children.empty?.should be_false
257 end
267 end
258
268
259 it "self_and_descendants" do
269 it "self_and_descendants" do
260 parent = categories(:top_level)
270 parent = categories(:top_level)
261 self_and_descendants = [
271 self_and_descendants = [
262 parent,
272 parent,
263 categories(:child_1),
273 categories(:child_1),
264 categories(:child_2),
274 categories(:child_2),
265 categories(:child_2_1),
275 categories(:child_2_1),
266 categories(:child_3)
276 categories(:child_3)
267 ]
277 ]
268 self_and_descendants.should == parent.self_and_descendants
278 self_and_descendants.should == parent.self_and_descendants
269 self_and_descendants.count.should == parent.self_and_descendants.count
279 self_and_descendants.count.should == parent.self_and_descendants.count
270 end
280 end
271
281
272 it "descendants" do
282 it "descendants" do
273 lawyers = Category.create!(:name => "lawyers")
283 lawyers = Category.create!(:name => "lawyers")
274 us = Category.create!(:name => "United States")
284 us = Category.create!(:name => "United States")
275 us.move_to_child_of(lawyers)
285 us.move_to_child_of(lawyers)
276 patent = Category.create!(:name => "Patent Law")
286 patent = Category.create!(:name => "Patent Law")
277 patent.move_to_child_of(us)
287 patent.move_to_child_of(us)
278 lawyers.reload
288 lawyers.reload
279
289
280 lawyers.children.size.should == 1
290 lawyers.children.size.should == 1
281 us.children.size.should == 1
291 us.children.size.should == 1
282 lawyers.descendants.size.should == 2
292 lawyers.descendants.size.should == 2
283 end
293 end
284
294
285 it "self_and_descendants" do
295 it "self_and_descendants" do
286 parent = categories(:top_level)
296 parent = categories(:top_level)
287 descendants = [
297 descendants = [
288 categories(:child_1),
298 categories(:child_1),
289 categories(:child_2),
299 categories(:child_2),
290 categories(:child_2_1),
300 categories(:child_2_1),
291 categories(:child_3)
301 categories(:child_3)
292 ]
302 ]
293 descendants.should == parent.descendants
303 descendants.should == parent.descendants
294 end
304 end
295
305
296 it "children" do
306 it "children" do
297 category = categories(:top_level)
307 category = categories(:top_level)
298 category.children.each {|c| category.id.should == c.parent_id }
308 category.children.each {|c| category.id.should == c.parent_id }
299 end
309 end
300
310
301 it "order_of_children" do
311 it "order_of_children" do
302 categories(:child_2).move_left
312 categories(:child_2).move_left
303 categories(:child_2).should == categories(:top_level).children[0]
313 categories(:child_2).should == categories(:top_level).children[0]
304 categories(:child_1).should == categories(:top_level).children[1]
314 categories(:child_1).should == categories(:top_level).children[1]
305 categories(:child_3).should == categories(:top_level).children[2]
315 categories(:child_3).should == categories(:top_level).children[2]
306 end
316 end
307
317
308 it "is_or_is_ancestor_of?" do
318 it "is_or_is_ancestor_of?" do
309 categories(:top_level).is_or_is_ancestor_of?(categories(:child_1)).should be_true
319 categories(:top_level).is_or_is_ancestor_of?(categories(:child_1)).should be_true
310 categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
320 categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
311 categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
321 categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
312 categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
322 categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
313 categories(:child_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
323 categories(:child_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
314 categories(:child_1).is_or_is_ancestor_of?(categories(:child_1)).should be_true
324 categories(:child_1).is_or_is_ancestor_of?(categories(:child_1)).should be_true
315 end
325 end
316
326
317 it "is_ancestor_of?" do
327 it "is_ancestor_of?" do
318 categories(:top_level).is_ancestor_of?(categories(:child_1)).should be_true
328 categories(:top_level).is_ancestor_of?(categories(:child_1)).should be_true
319 categories(:top_level).is_ancestor_of?(categories(:child_2_1)).should be_true
329 categories(:top_level).is_ancestor_of?(categories(:child_2_1)).should be_true
320 categories(:child_2).is_ancestor_of?(categories(:child_2_1)).should be_true
330 categories(:child_2).is_ancestor_of?(categories(:child_2_1)).should be_true
321 categories(:child_2_1).is_ancestor_of?(categories(:child_2)).should be_false
331 categories(:child_2_1).is_ancestor_of?(categories(:child_2)).should be_false
322 categories(:child_1).is_ancestor_of?(categories(:child_2)).should be_false
332 categories(:child_1).is_ancestor_of?(categories(:child_2)).should be_false
323 categories(:child_1).is_ancestor_of?(categories(:child_1)).should be_false
333 categories(:child_1).is_ancestor_of?(categories(:child_1)).should be_false
324 end
334 end
325
335
326 it "is_or_is_ancestor_of_with_scope" do
336 it "is_or_is_ancestor_of_with_scope" do
327 root = ScopedCategory.root
337 root = ScopedCategory.root
328 child = root.children.first
338 child = root.children.first
329 root.is_or_is_ancestor_of?(child).should be_true
339 root.is_or_is_ancestor_of?(child).should be_true
330 child.update_attribute :organization_id, 'different'
340 child.update_attribute :organization_id, 'different'
331 root.is_or_is_ancestor_of?(child).should be_false
341 root.is_or_is_ancestor_of?(child).should be_false
332 end
342 end
333
343
334 it "is_or_is_descendant_of?" do
344 it "is_or_is_descendant_of?" do
335 categories(:child_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
345 categories(:child_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
336 categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
346 categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
337 categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2)).should be_true
347 categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2)).should be_true
338 categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1)).should be_false
348 categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1)).should be_false
339 categories(:child_2).is_or_is_descendant_of?(categories(:child_1)).should be_false
349 categories(:child_2).is_or_is_descendant_of?(categories(:child_1)).should be_false
340 categories(:child_1).is_or_is_descendant_of?(categories(:child_1)).should be_true
350 categories(:child_1).is_or_is_descendant_of?(categories(:child_1)).should be_true
341 end
351 end
342
352
343 it "is_descendant_of?" do
353 it "is_descendant_of?" do
344 categories(:child_1).is_descendant_of?(categories(:top_level)).should be_true
354 categories(:child_1).is_descendant_of?(categories(:top_level)).should be_true
345 categories(:child_2_1).is_descendant_of?(categories(:top_level)).should be_true
355 categories(:child_2_1).is_descendant_of?(categories(:top_level)).should be_true
346 categories(:child_2_1).is_descendant_of?(categories(:child_2)).should be_true
356 categories(:child_2_1).is_descendant_of?(categories(:child_2)).should be_true
347 categories(:child_2).is_descendant_of?(categories(:child_2_1)).should be_false
357 categories(:child_2).is_descendant_of?(categories(:child_2_1)).should be_false
348 categories(:child_2).is_descendant_of?(categories(:child_1)).should be_false
358 categories(:child_2).is_descendant_of?(categories(:child_1)).should be_false
349 categories(:child_1).is_descendant_of?(categories(:child_1)).should be_false
359 categories(:child_1).is_descendant_of?(categories(:child_1)).should be_false
350 end
360 end
351
361
352 it "is_or_is_descendant_of_with_scope" do
362 it "is_or_is_descendant_of_with_scope" do
353 root = ScopedCategory.root
363 root = ScopedCategory.root
354 child = root.children.first
364 child = root.children.first
355 child.is_or_is_descendant_of?(root).should be_true
365 child.is_or_is_descendant_of?(root).should be_true
356 child.update_attribute :organization_id, 'different'
366 child.update_attribute :organization_id, 'different'
357 child.is_or_is_descendant_of?(root).should be_false
367 child.is_or_is_descendant_of?(root).should be_false
358 end
368 end
359
369
360 it "same_scope?" do
370 it "same_scope?" do
361 root = ScopedCategory.root
371 root = ScopedCategory.root
362 child = root.children.first
372 child = root.children.first
363 child.same_scope?(root).should be_true
373 child.same_scope?(root).should be_true
364 child.update_attribute :organization_id, 'different'
374 child.update_attribute :organization_id, 'different'
365 child.same_scope?(root).should be_false
375 child.same_scope?(root).should be_false
366 end
376 end
367
377
368 it "left_sibling" do
378 it "left_sibling" do
369 categories(:child_1).should == categories(:child_2).left_sibling
379 categories(:child_1).should == categories(:child_2).left_sibling
370 categories(:child_2).should == categories(:child_3).left_sibling
380 categories(:child_2).should == categories(:child_3).left_sibling
371 end
381 end
372
382
373 it "left_sibling_of_root" do
383 it "left_sibling_of_root" do
374 categories(:top_level).left_sibling.should be_nil
384 categories(:top_level).left_sibling.should be_nil
375 end
385 end
376
386
377 it "left_sibling_without_siblings" do
387 it "left_sibling_without_siblings" do
378 categories(:child_2_1).left_sibling.should be_nil
388 categories(:child_2_1).left_sibling.should be_nil
379 end
389 end
380
390
381 it "left_sibling_of_leftmost_node" do
391 it "left_sibling_of_leftmost_node" do
382 categories(:child_1).left_sibling.should be_nil
392 categories(:child_1).left_sibling.should be_nil
383 end
393 end
384
394
385 it "right_sibling" do
395 it "right_sibling" do
386 categories(:child_3).should == categories(:child_2).right_sibling
396 categories(:child_3).should == categories(:child_2).right_sibling
387 categories(:child_2).should == categories(:child_1).right_sibling
397 categories(:child_2).should == categories(:child_1).right_sibling
388 end
398 end
389
399
390 it "right_sibling_of_root" do
400 it "right_sibling_of_root" do
391 categories(:top_level_2).should == categories(:top_level).right_sibling
401 categories(:top_level_2).should == categories(:top_level).right_sibling
392 categories(:top_level_2).right_sibling.should be_nil
402 categories(:top_level_2).right_sibling.should be_nil
393 end
403 end
394
404
395 it "right_sibling_without_siblings" do
405 it "right_sibling_without_siblings" do
396 categories(:child_2_1).right_sibling.should be_nil
406 categories(:child_2_1).right_sibling.should be_nil
397 end
407 end
398
408
399 it "right_sibling_of_rightmost_node" do
409 it "right_sibling_of_rightmost_node" do
400 categories(:child_3).right_sibling.should be_nil
410 categories(:child_3).right_sibling.should be_nil
401 end
411 end
402
412
403 it "move_left" do
413 it "move_left" do
404 categories(:child_2).move_left
414 categories(:child_2).move_left
405 categories(:child_2).left_sibling.should be_nil
415 categories(:child_2).left_sibling.should be_nil
406 categories(:child_1).should == categories(:child_2).right_sibling
416 categories(:child_1).should == categories(:child_2).right_sibling
407 Category.valid?.should be_true
417 Category.valid?.should be_true
408 end
418 end
409
419
410 it "move_right" do
420 it "move_right" do
411 categories(:child_2).move_right
421 categories(:child_2).move_right
412 categories(:child_2).right_sibling.should be_nil
422 categories(:child_2).right_sibling.should be_nil
413 categories(:child_3).should == categories(:child_2).left_sibling
423 categories(:child_3).should == categories(:child_2).left_sibling
414 Category.valid?.should be_true
424 Category.valid?.should be_true
415 end
425 end
416
426
417 it "move_to_left_of" do
427 it "move_to_left_of" do
418 categories(:child_3).move_to_left_of(categories(:child_1))
428 categories(:child_3).move_to_left_of(categories(:child_1))
419 categories(:child_3).left_sibling.should be_nil
429 categories(:child_3).left_sibling.should be_nil
420 categories(:child_1).should == categories(:child_3).right_sibling
430 categories(:child_1).should == categories(:child_3).right_sibling
421 Category.valid?.should be_true
431 Category.valid?.should be_true
422 end
432 end
423
433
424 it "move_to_right_of" do
434 it "move_to_right_of" do
425 categories(:child_1).move_to_right_of(categories(:child_3))
435 categories(:child_1).move_to_right_of(categories(:child_3))
426 categories(:child_1).right_sibling.should be_nil
436 categories(:child_1).right_sibling.should be_nil
427 categories(:child_3).should == categories(:child_1).left_sibling
437 categories(:child_3).should == categories(:child_1).left_sibling
428 Category.valid?.should be_true
438 Category.valid?.should be_true
429 end
439 end
430
440
431 it "move_to_root" do
441 it "move_to_root" do
432 categories(:child_2).move_to_root
442 categories(:child_2).move_to_root
433 categories(:child_2).parent.should be_nil
443 categories(:child_2).parent.should be_nil
434 categories(:child_2).level.should == 0
444 categories(:child_2).level.should == 0
435 categories(:child_2_1).level.should == 1
445 categories(:child_2_1).level.should == 1
436 categories(:child_2).left.should == 1
446 categories(:child_2).left.should == 7
437 categories(:child_2).right.should == 4
447 categories(:child_2).right.should == 10
438 Category.valid?.should be_true
448 Category.valid?.should be_true
439 end
449 end
440
450
441 it "move_to_child_of" do
451 it "move_to_child_of" do
442 categories(:child_1).move_to_child_of(categories(:child_3))
452 categories(:child_1).move_to_child_of(categories(:child_3))
443 categories(:child_3).id.should == categories(:child_1).parent_id
453 categories(:child_3).id.should == categories(:child_1).parent_id
444 Category.valid?.should be_true
454 Category.valid?.should be_true
445 end
455 end
446
456
447 describe "#move_to_child_with_index" do
457 describe "#move_to_child_with_index" do
448 it "move to a node without child" do
458 it "move to a node without child" do
449 categories(:child_1).move_to_child_with_index(categories(:child_3), 0)
459 categories(:child_1).move_to_child_with_index(categories(:child_3), 0)
450 categories(:child_3).id.should == categories(:child_1).parent_id
460 categories(:child_3).id.should == categories(:child_1).parent_id
451 categories(:child_1).left.should == 7
461 categories(:child_1).left.should == 7
452 categories(:child_1).right.should == 8
462 categories(:child_1).right.should == 8
453 categories(:child_3).left.should == 6
463 categories(:child_3).left.should == 6
454 categories(:child_3).right.should == 9
464 categories(:child_3).right.should == 9
455 Category.valid?.should be_true
465 Category.valid?.should be_true
456 end
466 end
457
467
458 it "move to a node to the left child" do
468 it "move to a node to the left child" do
459 categories(:child_1).move_to_child_with_index(categories(:child_2), 0)
469 categories(:child_1).move_to_child_with_index(categories(:child_2), 0)
460 categories(:child_1).parent_id.should == categories(:child_2).id
470 categories(:child_1).parent_id.should == categories(:child_2).id
461 categories(:child_2_1).left.should == 5
471 categories(:child_2_1).left.should == 5
462 categories(:child_2_1).right.should == 6
472 categories(:child_2_1).right.should == 6
463 categories(:child_1).left.should == 3
473 categories(:child_1).left.should == 3
464 categories(:child_1).right.should == 4
474 categories(:child_1).right.should == 4
465 categories(:child_2).reload
475 categories(:child_2).reload
466 categories(:child_2).left.should == 2
476 categories(:child_2).left.should == 2
467 categories(:child_2).right.should == 7
477 categories(:child_2).right.should == 7
468 end
478 end
469
479
470 it "move to a node to the right child" do
480 it "move to a node to the right child" do
471 categories(:child_1).move_to_child_with_index(categories(:child_2), 1)
481 categories(:child_1).move_to_child_with_index(categories(:child_2), 1)
472 categories(:child_1).parent_id.should == categories(:child_2).id
482 categories(:child_1).parent_id.should == categories(:child_2).id
473 categories(:child_2_1).left.should == 3
483 categories(:child_2_1).left.should == 3
474 categories(:child_2_1).right.should == 4
484 categories(:child_2_1).right.should == 4
475 categories(:child_1).left.should == 5
485 categories(:child_1).left.should == 5
476 categories(:child_1).right.should == 6
486 categories(:child_1).right.should == 6
477 categories(:child_2).reload
487 categories(:child_2).reload
478 categories(:child_2).left.should == 2
488 categories(:child_2).left.should == 2
479 categories(:child_2).right.should == 7
489 categories(:child_2).right.should == 7
480 end
490 end
481
491
482 end
492 end
483
493
484 it "move_to_child_of_appends_to_end" do
494 it "move_to_child_of_appends_to_end" do
485 child = Category.create! :name => 'New Child'
495 child = Category.create! :name => 'New Child'
486 child.move_to_child_of categories(:top_level)
496 child.move_to_child_of categories(:top_level)
487 child.should == categories(:top_level).children.last
497 child.should == categories(:top_level).children.last
488 end
498 end
489
499
490 it "subtree_move_to_child_of" do
500 it "subtree_move_to_child_of" do
491 categories(:child_2).left.should == 4
501 categories(:child_2).left.should == 4
492 categories(:child_2).right.should == 7
502 categories(:child_2).right.should == 7
493
503
494 categories(:child_1).left.should == 2
504 categories(:child_1).left.should == 2
495 categories(:child_1).right.should == 3
505 categories(:child_1).right.should == 3
496
506
497 categories(:child_2).move_to_child_of(categories(:child_1))
507 categories(:child_2).move_to_child_of(categories(:child_1))
498 Category.valid?.should be_true
508 Category.valid?.should be_true
499 categories(:child_1).id.should == categories(:child_2).parent_id
509 categories(:child_1).id.should == categories(:child_2).parent_id
500
510
501 categories(:child_2).left.should == 3
511 categories(:child_2).left.should == 3
502 categories(:child_2).right.should == 6
512 categories(:child_2).right.should == 6
503 categories(:child_1).left.should == 2
513 categories(:child_1).left.should == 2
504 categories(:child_1).right.should == 7
514 categories(:child_1).right.should == 7
505 end
515 end
506
516
507 it "slightly_difficult_move_to_child_of" do
517 it "slightly_difficult_move_to_child_of" do
508 categories(:top_level_2).left.should == 11
518 categories(:top_level_2).left.should == 11
509 categories(:top_level_2).right.should == 12
519 categories(:top_level_2).right.should == 12
510
520
511 # create a new top-level node and move single-node top-level tree inside it.
521 # create a new top-level node and move single-node top-level tree inside it.
512 new_top = Category.create(:name => 'New Top')
522 new_top = Category.create(:name => 'New Top')
513 new_top.left.should == 13
523 new_top.left.should == 13
514 new_top.right.should == 14
524 new_top.right.should == 14
515
525
516 categories(:top_level_2).move_to_child_of(new_top)
526 categories(:top_level_2).move_to_child_of(new_top)
517
527
518 Category.valid?.should be_true
528 Category.valid?.should be_true
519 new_top.id.should == categories(:top_level_2).parent_id
529 new_top.id.should == categories(:top_level_2).parent_id
520
530
521 categories(:top_level_2).left.should == 12
531 categories(:top_level_2).left.should == 12
522 categories(:top_level_2).right.should == 13
532 categories(:top_level_2).right.should == 13
523 new_top.left.should == 11
533 new_top.left.should == 11
524 new_top.right.should == 14
534 new_top.right.should == 14
525 end
535 end
526
536
527 it "difficult_move_to_child_of" do
537 it "difficult_move_to_child_of" do
528 categories(:top_level).left.should == 1
538 categories(:top_level).left.should == 1
529 categories(:top_level).right.should == 10
539 categories(:top_level).right.should == 10
530 categories(:child_2_1).left.should == 5
540 categories(:child_2_1).left.should == 5
531 categories(:child_2_1).right.should == 6
541 categories(:child_2_1).right.should == 6
532
542
533 # create a new top-level node and move an entire top-level tree inside it.
543 # create a new top-level node and move an entire top-level tree inside it.
534 new_top = Category.create(:name => 'New Top')
544 new_top = Category.create(:name => 'New Top')
535 categories(:top_level).move_to_child_of(new_top)
545 categories(:top_level).move_to_child_of(new_top)
536 categories(:child_2_1).reload
546 categories(:child_2_1).reload
537 Category.valid?.should be_true
547 Category.valid?.should be_true
538 new_top.id.should == categories(:top_level).parent_id
548 new_top.id.should == categories(:top_level).parent_id
539
549
540 categories(:top_level).left.should == 4
550 categories(:top_level).left.should == 4
541 categories(:top_level).right.should == 13
551 categories(:top_level).right.should == 13
542 categories(:child_2_1).left.should == 8
552 categories(:child_2_1).left.should == 8
543 categories(:child_2_1).right.should == 9
553 categories(:child_2_1).right.should == 9
544 end
554 end
545
555
546 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
556 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
547 it "move_to_child_more_than_once_per_parent_rebuild" do
557 it "move_to_child_more_than_once_per_parent_rebuild" do
548 root1 = Category.create(:name => 'Root1')
558 root1 = Category.create(:name => 'Root1')
549 root2 = Category.create(:name => 'Root2')
559 root2 = Category.create(:name => 'Root2')
550 root3 = Category.create(:name => 'Root3')
560 root3 = Category.create(:name => 'Root3')
551
561
552 root2.move_to_child_of root1
562 root2.move_to_child_of root1
553 root3.move_to_child_of root1
563 root3.move_to_child_of root1
554
564
555 output = Category.roots.last.to_text
565 output = Category.roots.last.to_text
556 Category.update_all('lft = null, rgt = null')
566 Category.update_all('lft = null, rgt = null')
557 Category.rebuild!
567 Category.rebuild!
558
568
559 Category.roots.last.to_text.should == output
569 Category.roots.last.to_text.should == output
560 end
570 end
561
571
562 # doing move_to_child twice onto same parent from the furthest right first
572 # doing move_to_child twice onto same parent from the furthest right first
563 it "move_to_child_more_than_once_per_parent_outside_in" do
573 it "move_to_child_more_than_once_per_parent_outside_in" do
564 node1 = Category.create(:name => 'Node-1')
574 node1 = Category.create(:name => 'Node-1')
565 node2 = Category.create(:name => 'Node-2')
575 node2 = Category.create(:name => 'Node-2')
566 node3 = Category.create(:name => 'Node-3')
576 node3 = Category.create(:name => 'Node-3')
567
577
568 node2.move_to_child_of node1
578 node2.move_to_child_of node1
569 node3.move_to_child_of node1
579 node3.move_to_child_of node1
570
580
571 output = Category.roots.last.to_text
581 output = Category.roots.last.to_text
572 Category.update_all('lft = null, rgt = null')
582 Category.update_all('lft = null, rgt = null')
573 Category.rebuild!
583 Category.rebuild!
574
584
575 Category.roots.last.to_text.should == output
585 Category.roots.last.to_text.should == output
576 end
586 end
577
587
578 it "should_move_to_ordered_child" do
588 it "should_move_to_ordered_child" do
579 node1 = Category.create(:name => 'Node-1')
589 node1 = Category.create(:name => 'Node-1')
580 node2 = Category.create(:name => 'Node-2')
590 node2 = Category.create(:name => 'Node-2')
581 node3 = Category.create(:name => 'Node-3')
591 node3 = Category.create(:name => 'Node-3')
582
592
583 node2.move_to_ordered_child_of(node1, "name")
593 node2.move_to_ordered_child_of(node1, "name")
584
594
585 assert_equal node1, node2.parent
595 assert_equal node1, node2.parent
586 assert_equal 1, node1.children.count
596 assert_equal 1, node1.children.count
587
597
588 node3.move_to_ordered_child_of(node1, "name", true) # acending
598 node3.move_to_ordered_child_of(node1, "name", true) # acending
589
599
590 assert_equal node1, node3.parent
600 assert_equal node1, node3.parent
591 assert_equal 2, node1.children.count
601 assert_equal 2, node1.children.count
592 assert_equal node2.name, node1.children[0].name
602 assert_equal node2.name, node1.children[0].name
593 assert_equal node3.name, node1.children[1].name
603 assert_equal node3.name, node1.children[1].name
594
604
595 node3.move_to_ordered_child_of(node1, "name", false) # decending
605 node3.move_to_ordered_child_of(node1, "name", false) # decending
596 node1.reload
606 node1.reload
597
607
598 assert_equal node1, node3.parent
608 assert_equal node1, node3.parent
599 assert_equal 2, node1.children.count
609 assert_equal 2, node1.children.count
600 assert_equal node3.name, node1.children[0].name
610 assert_equal node3.name, node1.children[0].name
601 assert_equal node2.name, node1.children[1].name
611 assert_equal node2.name, node1.children[1].name
602 end
612 end
603
613
604 it "should be able to rebuild without validating each record" do
614 it "should be able to rebuild without validating each record" do
605 root1 = Category.create(:name => 'Root1')
615 root1 = Category.create(:name => 'Root1')
606 root2 = Category.create(:name => 'Root2')
616 root2 = Category.create(:name => 'Root2')
607 root3 = Category.create(:name => 'Root3')
617 root3 = Category.create(:name => 'Root3')
608
618
609 root2.move_to_child_of root1
619 root2.move_to_child_of root1
610 root3.move_to_child_of root1
620 root3.move_to_child_of root1
611
621
612 root2.name = nil
622 root2.name = nil
613 root2.save!(:validate => false)
623 root2.save!(:validate => false)
614
624
615 output = Category.roots.last.to_text
625 output = Category.roots.last.to_text
616 Category.update_all('lft = null, rgt = null')
626 Category.update_all('lft = null, rgt = null')
617 Category.rebuild!(false)
627 Category.rebuild!(false)
618
628
619 Category.roots.last.to_text.should == output
629 Category.roots.last.to_text.should == output
620 end
630 end
621
631
622 it "valid_with_null_lefts" do
632 it "valid_with_null_lefts" do
623 Category.valid?.should be_true
633 Category.valid?.should be_true
624 Category.update_all('lft = null')
634 Category.update_all('lft = null')
625 Category.valid?.should be_false
635 Category.valid?.should be_false
626 end
636 end
627
637
628 it "valid_with_null_rights" do
638 it "valid_with_null_rights" do
629 Category.valid?.should be_true
639 Category.valid?.should be_true
630 Category.update_all('rgt = null')
640 Category.update_all('rgt = null')
631 Category.valid?.should be_false
641 Category.valid?.should be_false
632 end
642 end
633
643
634 it "valid_with_missing_intermediate_node" do
644 it "valid_with_missing_intermediate_node" do
635 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
645 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
636 Category.valid?.should be_true
646 Category.valid?.should be_true
637 Category.delete(categories(:child_2).id)
647 Category.delete(categories(:child_2).id)
638 Category.valid?.should be_true
648 Category.valid?.should be_true
639 end
649 end
640
650
641 it "valid_with_overlapping_and_rights" do
651 it "valid_with_overlapping_and_rights" do
642 Category.valid?.should be_true
652 Category.valid?.should be_true
643 categories(:top_level_2)['lft'] = 0
653 categories(:top_level_2)['lft'] = 0
644 categories(:top_level_2).save
654 categories(:top_level_2).save
645 Category.valid?.should be_false
655 Category.valid?.should be_false
646 end
656 end
647
657
648 it "rebuild" do
658 it "rebuild" do
649 Category.valid?.should be_true
659 Category.valid?.should be_true
650 before_text = Category.root.to_text
660 before_text = Category.root.to_text
651 Category.update_all('lft = null, rgt = null')
661 Category.update_all('lft = null, rgt = null')
652 Category.rebuild!
662 Category.rebuild!
653 Category.valid?.should be_true
663 Category.valid?.should be_true
654 before_text.should == Category.root.to_text
664 before_text.should == Category.root.to_text
655 end
665 end
656
666
657 it "move_possible_for_sibling" do
667 it "move_possible_for_sibling" do
658 categories(:child_2).move_possible?(categories(:child_1)).should be_true
668 categories(:child_2).move_possible?(categories(:child_1)).should be_true
659 end
669 end
660
670
661 it "move_not_possible_to_self" do
671 it "move_not_possible_to_self" do
662 categories(:top_level).move_possible?(categories(:top_level)).should be_false
672 categories(:top_level).move_possible?(categories(:top_level)).should be_false
663 end
673 end
664
674
665 it "move_not_possible_to_parent" do
675 it "move_not_possible_to_parent" do
666 categories(:top_level).descendants.each do |descendant|
676 categories(:top_level).descendants.each do |descendant|
667 categories(:top_level).move_possible?(descendant).should be_false
677 categories(:top_level).move_possible?(descendant).should be_false
668 descendant.move_possible?(categories(:top_level)).should be_true
678 descendant.move_possible?(categories(:top_level)).should be_true
669 end
679 end
670 end
680 end
671
681
672 it "is_or_is_ancestor_of?" do
682 it "is_or_is_ancestor_of?" do
673 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
683 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
674 categories(:top_level).is_or_is_ancestor_of?(categories(c)).should be_true
684 categories(:top_level).is_or_is_ancestor_of?(categories(c)).should be_true
675 end
685 end
676 categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2)).should be_false
686 categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2)).should be_false
677 end
687 end
678
688
679 it "left_and_rights_valid_with_blank_left" do
689 it "left_and_rights_valid_with_blank_left" do
680 Category.left_and_rights_valid?.should be_true
690 Category.left_and_rights_valid?.should be_true
681 categories(:child_2)[:lft] = nil
691 categories(:child_2)[:lft] = nil
682 categories(:child_2).save(:validate => false)
692 categories(:child_2).save(:validate => false)
683 Category.left_and_rights_valid?.should be_false
693 Category.left_and_rights_valid?.should be_false
684 end
694 end
685
695
686 it "left_and_rights_valid_with_blank_right" do
696 it "left_and_rights_valid_with_blank_right" do
687 Category.left_and_rights_valid?.should be_true
697 Category.left_and_rights_valid?.should be_true
688 categories(:child_2)[:rgt] = nil
698 categories(:child_2)[:rgt] = nil
689 categories(:child_2).save(:validate => false)
699 categories(:child_2).save(:validate => false)
690 Category.left_and_rights_valid?.should be_false
700 Category.left_and_rights_valid?.should be_false
691 end
701 end
692
702
693 it "left_and_rights_valid_with_equal" do
703 it "left_and_rights_valid_with_equal" do
694 Category.left_and_rights_valid?.should be_true
704 Category.left_and_rights_valid?.should be_true
695 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
705 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
696 categories(:top_level_2).save(:validate => false)
706 categories(:top_level_2).save(:validate => false)
697 Category.left_and_rights_valid?.should be_false
707 Category.left_and_rights_valid?.should be_false
698 end
708 end
699
709
700 it "left_and_rights_valid_with_left_equal_to_parent" do
710 it "left_and_rights_valid_with_left_equal_to_parent" do
701 Category.left_and_rights_valid?.should be_true
711 Category.left_and_rights_valid?.should be_true
702 categories(:child_2)[:lft] = categories(:top_level)[:lft]
712 categories(:child_2)[:lft] = categories(:top_level)[:lft]
703 categories(:child_2).save(:validate => false)
713 categories(:child_2).save(:validate => false)
704 Category.left_and_rights_valid?.should be_false
714 Category.left_and_rights_valid?.should be_false
705 end
715 end
706
716
707 it "left_and_rights_valid_with_right_equal_to_parent" do
717 it "left_and_rights_valid_with_right_equal_to_parent" do
708 Category.left_and_rights_valid?.should be_true
718 Category.left_and_rights_valid?.should be_true
709 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
719 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
710 categories(:child_2).save(:validate => false)
720 categories(:child_2).save(:validate => false)
711 Category.left_and_rights_valid?.should be_false
721 Category.left_and_rights_valid?.should be_false
712 end
722 end
713
723
714 it "moving_dirty_objects_doesnt_invalidate_tree" do
724 it "moving_dirty_objects_doesnt_invalidate_tree" do
715 r1 = Category.create :name => "Test 1"
725 r1 = Category.create :name => "Test 1"
716 r2 = Category.create :name => "Test 2"
726 r2 = Category.create :name => "Test 2"
717 r3 = Category.create :name => "Test 3"
727 r3 = Category.create :name => "Test 3"
718 r4 = Category.create :name => "Test 4"
728 r4 = Category.create :name => "Test 4"
719 nodes = [r1, r2, r3, r4]
729 nodes = [r1, r2, r3, r4]
720
730
721 r2.move_to_child_of(r1)
731 r2.move_to_child_of(r1)
722 Category.valid?.should be_true
732 Category.valid?.should be_true
723
733
724 r3.move_to_child_of(r1)
734 r3.move_to_child_of(r1)
725 Category.valid?.should be_true
735 Category.valid?.should be_true
726
736
727 r4.move_to_child_of(r2)
737 r4.move_to_child_of(r2)
728 Category.valid?.should be_true
738 Category.valid?.should be_true
729 end
739 end
730
740
731 it "multi_scoped_no_duplicates_for_columns?" do
741 it "multi_scoped_no_duplicates_for_columns?" do
732 lambda {
742 lambda {
733 Note.no_duplicates_for_columns?
743 Note.no_duplicates_for_columns?
734 }.should_not raise_exception
744 }.should_not raise_exception
735 end
745 end
736
746
737 it "multi_scoped_all_roots_valid?" do
747 it "multi_scoped_all_roots_valid?" do
738 lambda {
748 lambda {
739 Note.all_roots_valid?
749 Note.all_roots_valid?
740 }.should_not raise_exception
750 }.should_not raise_exception
741 end
751 end
742
752
743 it "multi_scoped" do
753 it "multi_scoped" do
744 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
754 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
745 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
755 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
746 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
756 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
747
757
748 [note1, note2].should == note1.self_and_siblings
758 [note1, note2].should == note1.self_and_siblings
749 [note3].should == note3.self_and_siblings
759 [note3].should == note3.self_and_siblings
750 end
760 end
751
761
752 it "multi_scoped_rebuild" do
762 it "multi_scoped_rebuild" do
753 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
763 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
754 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
764 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
755 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
765 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
756
766
757 child1.move_to_child_of root
767 child1.move_to_child_of root
758 child2.move_to_child_of root
768 child2.move_to_child_of root
759
769
760 Note.update_all('lft = null, rgt = null')
770 Note.update_all('lft = null, rgt = null')
761 Note.rebuild!
771 Note.rebuild!
762
772
763 Note.roots.find_by_body('A').should == root
773 Note.roots.find_by_body('A').should == root
764 [child1, child2].should == Note.roots.find_by_body('A').children
774 [child1, child2].should == Note.roots.find_by_body('A').children
765 end
775 end
766
776
767 it "same_scope_with_multi_scopes" do
777 it "same_scope_with_multi_scopes" do
768 lambda {
778 lambda {
769 notes(:scope1).same_scope?(notes(:child_1))
779 notes(:scope1).same_scope?(notes(:child_1))
770 }.should_not raise_exception
780 }.should_not raise_exception
771 notes(:scope1).same_scope?(notes(:child_1)).should be_true
781 notes(:scope1).same_scope?(notes(:child_1)).should be_true
772 notes(:child_1).same_scope?(notes(:scope1)).should be_true
782 notes(:child_1).same_scope?(notes(:scope1)).should be_true
773 notes(:scope1).same_scope?(notes(:scope2)).should be_false
783 notes(:scope1).same_scope?(notes(:scope2)).should be_false
774 end
784 end
775
785
776 it "quoting_of_multi_scope_column_names" do
786 it "quoting_of_multi_scope_column_names" do
777 ## Proper Array Assignment for different DBs as per their quoting column behavior
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 expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""]
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 expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
791 expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
782 else
792 else
783 expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
793 expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
784 end
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 end
796 end
787
797
788 it "equal_in_same_scope" do
798 it "equal_in_same_scope" do
789 notes(:scope1).should == notes(:scope1)
799 notes(:scope1).should == notes(:scope1)
790 notes(:scope1).should_not == notes(:child_1)
800 notes(:scope1).should_not == notes(:child_1)
791 end
801 end
792
802
793 it "equal_in_different_scopes" do
803 it "equal_in_different_scopes" do
794 notes(:scope1).should_not == notes(:scope2)
804 notes(:scope1).should_not == notes(:scope2)
795 end
805 end
796
806
797 it "delete_does_not_invalidate" do
807 it "delete_does_not_invalidate" do
798 Category.acts_as_nested_set_options[:dependent] = :delete
808 Category.acts_as_nested_set_options[:dependent] = :delete
799 categories(:child_2).destroy
809 categories(:child_2).destroy
800 Category.valid?.should be_true
810 Category.valid?.should be_true
801 end
811 end
802
812
803 it "destroy_does_not_invalidate" do
813 it "destroy_does_not_invalidate" do
804 Category.acts_as_nested_set_options[:dependent] = :destroy
814 Category.acts_as_nested_set_options[:dependent] = :destroy
805 categories(:child_2).destroy
815 categories(:child_2).destroy
806 Category.valid?.should be_true
816 Category.valid?.should be_true
807 end
817 end
808
818
809 it "destroy_multiple_times_does_not_invalidate" do
819 it "destroy_multiple_times_does_not_invalidate" do
810 Category.acts_as_nested_set_options[:dependent] = :destroy
820 Category.acts_as_nested_set_options[:dependent] = :destroy
811 categories(:child_2).destroy
821 categories(:child_2).destroy
812 categories(:child_2).destroy
822 categories(:child_2).destroy
813 Category.valid?.should be_true
823 Category.valid?.should be_true
814 end
824 end
815
825
816 it "assigning_parent_id_on_create" do
826 it "assigning_parent_id_on_create" do
817 category = Category.create!(:name => "Child", :parent_id => categories(:child_2).id)
827 category = Category.create!(:name => "Child", :parent_id => categories(:child_2).id)
818 categories(:child_2).should == category.parent
828 categories(:child_2).should == category.parent
819 categories(:child_2).id.should == category.parent_id
829 categories(:child_2).id.should == category.parent_id
820 category.left.should_not be_nil
830 category.left.should_not be_nil
821 category.right.should_not be_nil
831 category.right.should_not be_nil
822 Category.valid?.should be_true
832 Category.valid?.should be_true
823 end
833 end
824
834
825 it "assigning_parent_on_create" do
835 it "assigning_parent_on_create" do
826 category = Category.create!(:name => "Child", :parent => categories(:child_2))
836 category = Category.create!(:name => "Child", :parent => categories(:child_2))
827 categories(:child_2).should == category.parent
837 categories(:child_2).should == category.parent
828 categories(:child_2).id.should == category.parent_id
838 categories(:child_2).id.should == category.parent_id
829 category.left.should_not be_nil
839 category.left.should_not be_nil
830 category.right.should_not be_nil
840 category.right.should_not be_nil
831 Category.valid?.should be_true
841 Category.valid?.should be_true
832 end
842 end
833
843
834 it "assigning_parent_id_to_nil_on_create" do
844 it "assigning_parent_id_to_nil_on_create" do
835 category = Category.create!(:name => "New Root", :parent_id => nil)
845 category = Category.create!(:name => "New Root", :parent_id => nil)
836 category.parent.should be_nil
846 category.parent.should be_nil
837 category.parent_id.should be_nil
847 category.parent_id.should be_nil
838 category.left.should_not be_nil
848 category.left.should_not be_nil
839 category.right.should_not be_nil
849 category.right.should_not be_nil
840 Category.valid?.should be_true
850 Category.valid?.should be_true
841 end
851 end
842
852
843 it "assigning_parent_id_on_update" do
853 it "assigning_parent_id_on_update" do
844 category = categories(:child_2_1)
854 category = categories(:child_2_1)
845 category.parent_id = categories(:child_3).id
855 category.parent_id = categories(:child_3).id
846 category.save
856 category.save
847 category.reload
857 category.reload
848 categories(:child_3).reload
858 categories(:child_3).reload
849 categories(:child_3).should == category.parent
859 categories(:child_3).should == category.parent
850 categories(:child_3).id.should == category.parent_id
860 categories(:child_3).id.should == category.parent_id
851 Category.valid?.should be_true
861 Category.valid?.should be_true
852 end
862 end
853
863
854 it "assigning_parent_on_update" do
864 it "assigning_parent_on_update" do
855 category = categories(:child_2_1)
865 category = categories(:child_2_1)
856 category.parent = categories(:child_3)
866 category.parent = categories(:child_3)
857 category.save
867 category.save
858 category.reload
868 category.reload
859 categories(:child_3).reload
869 categories(:child_3).reload
860 categories(:child_3).should == category.parent
870 categories(:child_3).should == category.parent
861 categories(:child_3).id.should == category.parent_id
871 categories(:child_3).id.should == category.parent_id
862 Category.valid?.should be_true
872 Category.valid?.should be_true
863 end
873 end
864
874
865 it "assigning_parent_id_to_nil_on_update" do
875 it "assigning_parent_id_to_nil_on_update" do
866 category = categories(:child_2_1)
876 category = categories(:child_2_1)
867 category.parent_id = nil
877 category.parent_id = nil
868 category.save
878 category.save
869 category.parent.should be_nil
879 category.parent.should be_nil
870 category.parent_id.should be_nil
880 category.parent_id.should be_nil
871 Category.valid?.should be_true
881 Category.valid?.should be_true
872 end
882 end
873
883
874 it "creating_child_from_parent" do
884 it "creating_child_from_parent" do
875 category = categories(:child_2).children.create!(:name => "Child")
885 category = categories(:child_2).children.create!(:name => "Child")
876 categories(:child_2).should == category.parent
886 categories(:child_2).should == category.parent
877 categories(:child_2).id.should == category.parent_id
887 categories(:child_2).id.should == category.parent_id
878 category.left.should_not be_nil
888 category.left.should_not be_nil
879 category.right.should_not be_nil
889 category.right.should_not be_nil
880 Category.valid?.should be_true
890 Category.valid?.should be_true
881 end
891 end
882
892
883 def check_structure(entries, structure)
893 def check_structure(entries, structure)
884 structure = structure.dup
894 structure = structure.dup
885 Category.each_with_level(entries) do |category, level|
895 Category.each_with_level(entries) do |category, level|
886 expected_level, expected_name = structure.shift
896 expected_level, expected_name = structure.shift
887 expected_name.should == category.name
897 expected_name.should == category.name
888 expected_level.should == level
898 expected_level.should == level
889 end
899 end
890 end
900 end
891
901
892 it "each_with_level" do
902 it "each_with_level" do
893 levels = [
903 levels = [
894 [0, "Top Level"],
904 [0, "Top Level"],
895 [1, "Child 1"],
905 [1, "Child 1"],
896 [1, "Child 2"],
906 [1, "Child 2"],
897 [2, "Child 2.1"],
907 [2, "Child 2.1"],
898 [1, "Child 3" ]
908 [1, "Child 3" ]
899 ]
909 ]
900
910
901 check_structure(Category.root.self_and_descendants, levels)
911 check_structure(Category.root.self_and_descendants, levels)
902
912
903 # test some deeper structures
913 # test some deeper structures
904 category = Category.find_by_name("Child 1")
914 category = Category.find_by_name("Child 1")
905 c1 = Category.new(:name => "Child 1.1")
915 c1 = Category.new(:name => "Child 1.1")
906 c2 = Category.new(:name => "Child 1.1.1")
916 c2 = Category.new(:name => "Child 1.1.1")
907 c3 = Category.new(:name => "Child 1.1.1.1")
917 c3 = Category.new(:name => "Child 1.1.1.1")
908 c4 = Category.new(:name => "Child 1.2")
918 c4 = Category.new(:name => "Child 1.2")
909 [c1, c2, c3, c4].each(&:save!)
919 [c1, c2, c3, c4].each(&:save!)
910
920
911 c1.move_to_child_of(category)
921 c1.move_to_child_of(category)
912 c2.move_to_child_of(c1)
922 c2.move_to_child_of(c1)
913 c3.move_to_child_of(c2)
923 c3.move_to_child_of(c2)
914 c4.move_to_child_of(category)
924 c4.move_to_child_of(category)
915
925
916 levels = [
926 levels = [
917 [0, "Top Level"],
927 [0, "Top Level"],
918 [1, "Child 1"],
928 [1, "Child 1"],
919 [2, "Child 1.1"],
929 [2, "Child 1.1"],
920 [3, "Child 1.1.1"],
930 [3, "Child 1.1.1"],
921 [4, "Child 1.1.1.1"],
931 [4, "Child 1.1.1.1"],
922 [2, "Child 1.2"],
932 [2, "Child 1.2"],
923 [1, "Child 2"],
933 [1, "Child 2"],
924 [2, "Child 2.1"],
934 [2, "Child 2.1"],
925 [1, "Child 3" ]
935 [1, "Child 3" ]
926 ]
936 ]
927
937
928 check_structure(Category.root.self_and_descendants, levels)
938 check_structure(Category.root.self_and_descendants, levels)
929 end
939 end
930
940
931 it "should not error on a model with attr_accessible" do
941 it "should not error on a model with attr_accessible" do
932 model = Class.new(ActiveRecord::Base)
942 model = Class.new(ActiveRecord::Base)
933 model.table_name = 'categories'
943 model.table_name = 'categories'
934 model.attr_accessible :name
944 model.attr_accessible :name
935 lambda {
945 lambda {
936 model.acts_as_nested_set
946 model.acts_as_nested_set
937 model.new(:name => 'foo')
947 model.new(:name => 'foo')
938 }.should_not raise_exception
948 }.should_not raise_exception
939 end
949 end
940
950
941 describe "before_move_callback" do
951 describe "before_move_callback" do
942 it "should fire the callback" do
952 it "should fire the callback" do
943 categories(:child_2).should_receive(:custom_before_move)
953 categories(:child_2).should_receive(:custom_before_move)
944 categories(:child_2).move_to_root
954 categories(:child_2).move_to_root
945 end
955 end
946
956
947 it "should stop move when callback returns false" do
957 it "should stop move when callback returns false" do
948 Category.test_allows_move = false
958 Category.test_allows_move = false
949 categories(:child_3).move_to_root.should be_false
959 categories(:child_3).move_to_root.should be_false
950 categories(:child_3).root?.should be_false
960 categories(:child_3).root?.should be_false
951 end
961 end
952
962
953 it "should not halt save actions" do
963 it "should not halt save actions" do
954 Category.test_allows_move = false
964 Category.test_allows_move = false
955 categories(:child_3).parent_id = nil
965 categories(:child_3).parent_id = nil
956 categories(:child_3).save.should be_true
966 categories(:child_3).save.should be_true
957 end
967 end
958 end
968 end
959
969
960 describe "counter_cache" do
970 describe "counter_cache" do
961
971
962 it "should allow use of a counter cache for children" do
972 it "should allow use of a counter cache for children" do
963 note1 = things(:parent1)
973 note1 = things(:parent1)
964 note1.children.count.should == 2
974 note1.children.count.should == 2
965 end
975 end
966
976
967 it "should increment the counter cache on create" do
977 it "should increment the counter cache on create" do
968 note1 = things(:parent1)
978 note1 = things(:parent1)
969 note1.children.count.should == 2
979 note1.children.count.should == 2
970 note1[:children_count].should == 2
980 note1[:children_count].should == 2
971 note1.children.create :body => 'Child 3'
981 note1.children.create :body => 'Child 3'
972 note1.children.count.should == 3
982 note1.children.count.should == 3
973 note1.reload
983 note1.reload
974 note1[:children_count].should == 3
984 note1[:children_count].should == 3
975 end
985 end
976
986
977 it "should decrement the counter cache on destroy" do
987 it "should decrement the counter cache on destroy" do
978 note1 = things(:parent1)
988 note1 = things(:parent1)
979 note1.children.count.should == 2
989 note1.children.count.should == 2
980 note1[:children_count].should == 2
990 note1[:children_count].should == 2
981 note1.children.last.destroy
991 note1.children.last.destroy
982 note1.children.count.should == 1
992 note1.children.count.should == 1
983 note1.reload
993 note1.reload
984 note1[:children_count].should == 1
994 note1[:children_count].should == 1
985 end
995 end
986 end
996 end
987
997
988 describe "association callbacks on children" do
998 describe "association callbacks on children" do
989 it "should call the appropriate callbacks on the children :has_many association " do
999 it "should call the appropriate callbacks on the children :has_many association " do
990 root = DefaultWithCallbacks.create
1000 root = DefaultWithCallbacks.create
991 root.should_not be_new_record
1001 root.should_not be_new_record
992
1002
993 child = root.children.build
1003 child = root.children.build
994
1004
995 root.before_add.should == child
1005 root.before_add.should == child
996 root.after_add.should == child
1006 root.after_add.should == child
997
1007
998 root.before_remove.should_not == child
1008 root.before_remove.should_not == child
999 root.after_remove.should_not == child
1009 root.after_remove.should_not == child
1000
1010
1001 child.save.should be_true
1011 child.save.should be_true
1002 root.children.delete(child).should be_true
1012 root.children.delete(child).should be_true
1003
1013
1004 root.before_remove.should == child
1014 root.before_remove.should == child
1005 root.after_remove.should == child
1015 root.after_remove.should == child
1006 end
1016 end
1007 end
1017 end
1008
1018
1009 describe 'rebuilding tree with a default scope ordering' do
1019 describe 'rebuilding tree with a default scope ordering' do
1010 it "doesn't throw exception" do
1020 it "doesn't throw exception" do
1011 expect { Position.rebuild! }.not_to raise_error
1021 expect { Position.rebuild! }.not_to raise_error
1012 end
1022 end
1013 end
1023 end
1014
1024
1015 describe 'creating roots with a default scope ordering' do
1025 describe 'creating roots with a default scope ordering' do
1016 it "assigns rgt and lft correctly" do
1026 it "assigns rgt and lft correctly" do
1017 alpha = Order.create(:name => 'Alpha')
1027 alpha = Order.create(:name => 'Alpha')
1018 gamma = Order.create(:name => 'Gamma')
1028 gamma = Order.create(:name => 'Gamma')
1019 omega = Order.create(:name => 'Omega')
1029 omega = Order.create(:name => 'Omega')
1020
1030
1021 alpha.lft.should == 1
1031 alpha.lft.should == 1
1022 alpha.rgt.should == 2
1032 alpha.rgt.should == 2
1023 gamma.lft.should == 3
1033 gamma.lft.should == 3
1024 gamma.rgt.should == 4
1034 gamma.rgt.should == 4
1025 omega.lft.should == 5
1035 omega.lft.should == 5
1026 omega.rgt.should == 6
1036 omega.rgt.should == 6
1027 end
1037 end
1028 end
1038 end
1029
1039
1030 describe 'moving node from one scoped tree to another' do
1040 describe 'moving node from one scoped tree to another' do
1031 xit "moves single node correctly" do
1041 xit "moves single node correctly" do
1032 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1042 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1033 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1043 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1034 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1044 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1035 child1_1.move_to_child_of root1
1045 child1_1.move_to_child_of root1
1036 child1_2.move_to_child_of root1
1046 child1_2.move_to_child_of root1
1037
1047
1038 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1048 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1039 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1049 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1040 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1050 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1041 child2_1.move_to_child_of root2
1051 child2_1.move_to_child_of root2
1042 child2_2.move_to_child_of root2
1052 child2_2.move_to_child_of root2
1043
1053
1044 child1_1.update_attributes!(:notable_id => 5)
1054 child1_1.update_attributes!(:notable_id => 5)
1045 child1_1.move_to_child_of root2
1055 child1_1.move_to_child_of root2
1046
1056
1047 root1.children.should == [child1_2]
1057 root1.children.should == [child1_2]
1048 root2.children.should == [child2_1, child2_2, child1_1]
1058 root2.children.should == [child2_1, child2_2, child1_1]
1049
1059
1050 Note.valid?.should == true
1060 Note.valid?.should == true
1051 end
1061 end
1052
1062
1053 xit "moves node with children correctly" do
1063 xit "moves node with children correctly" do
1054 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1064 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1055 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1065 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1056 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1066 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1057 child1_1.move_to_child_of root1
1067 child1_1.move_to_child_of root1
1058 child1_2.move_to_child_of child1_1
1068 child1_2.move_to_child_of child1_1
1059
1069
1060 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1070 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1061 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1071 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1062 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1072 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1063 child2_1.move_to_child_of root2
1073 child2_1.move_to_child_of root2
1064 child2_2.move_to_child_of root2
1074 child2_2.move_to_child_of root2
1065
1075
1066 child1_1.update_attributes!(:notable_id => 5)
1076 child1_1.update_attributes!(:notable_id => 5)
1067 child1_1.move_to_child_of root2
1077 child1_1.move_to_child_of root2
1068
1078
1069 root1.children.should == []
1079 root1.children.should == []
1070 root2.children.should == [child2_1, child2_2, child1_1]
1080 root2.children.should == [child2_1, child2_2, child1_1]
1071 child1_1.children should == [child1_2]
1081 child1_1.children should == [child1_2]
1072 root2.siblings.should == [child2_1, child2_2, child1_1, child1_2]
1082 root2.siblings.should == [child2_1, child2_2, child1_1, child1_2]
1073
1083
1074 Note.valid?.should == true
1084 Note.valid?.should == true
1075 end
1085 end
1076 end
1086 end
1077
1087
1078 describe 'specifying custom sort column' do
1088 describe 'specifying custom sort column' do
1079 it "should sort by the default sort column" do
1089 it "should sort by the default sort column" do
1080 Category.order_column.should == 'lft'
1090 Category.order_column.should == 'lft'
1081 end
1091 end
1082
1092
1083 it "should sort by custom sort column" do
1093 it "should sort by custom sort column" do
1084 OrderedCategory.acts_as_nested_set_options[:order_column].should == 'name'
1094 OrderedCategory.acts_as_nested_set_options[:order_column].should == 'name'
1085 OrderedCategory.order_column.should == 'name'
1095 OrderedCategory.order_column.should == 'name'
1086 end
1096 end
1087 end
1097 end
1088 end
1098 end
@@ -1,33 +1,35
1 plugin_test_dir = File.dirname(__FILE__)
1 plugin_test_dir = File.dirname(__FILE__)
2
2
3 require 'rubygems'
3 require 'rubygems'
4 require 'bundler/setup'
4 require 'bundler/setup'
5 require 'pry'
5
6
6 require 'logger'
7 require 'logger'
7 require 'active_record'
8 require 'active_record'
8 ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
9 ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
9
10
10 require 'yaml'
11 require 'yaml'
11 require 'erb'
12 require 'erb'
12 ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
13 ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
13 ActiveRecord::Base.establish_connection(ENV["DB"] ||= "sqlite3mem")
14 ActiveRecord::Base.establish_connection(ENV["DB"] ||= "sqlite3mem")
14 ActiveRecord::Migration.verbose = false
15 ActiveRecord::Migration.verbose = false
15
16
16 require 'combustion/database'
17 require 'combustion/database'
17 Combustion::Database.create_database(ActiveRecord::Base.configurations[ENV["DB"]])
18 Combustion::Database.create_database(ActiveRecord::Base.configurations[ENV["DB"]])
18 load(File.join(plugin_test_dir, "db", "schema.rb"))
19 load(File.join(plugin_test_dir, "db", "schema.rb"))
19
20
20 require 'awesome_nested_set'
21 require 'awesome_nested_set'
21 require 'support/models'
22 require 'support/models'
22
23
23 require 'action_controller'
24 require 'action_controller'
24 require 'rspec/rails'
25 require 'rspec/rails'
26 require 'database_cleaner'
25 RSpec.configure do |config|
27 RSpec.configure do |config|
26 config.fixture_path = "#{plugin_test_dir}/fixtures"
28 config.fixture_path = "#{plugin_test_dir}/fixtures"
27 config.use_transactional_fixtures = true
29 config.use_transactional_fixtures = true
28 config.after(:suite) do
30 config.after(:suite) do
29 unless /sqlite/ === ENV['DB']
31 unless /sqlite/ === ENV['DB']
30 Combustion::Database.drop_database(ActiveRecord::Base.configurations[ENV['DB']])
32 Combustion::Database.drop_database(ActiveRecord::Base.configurations[ENV['DB']])
31 end
33 end
32 end
34 end
33 end
35 end
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now