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