##// END OF EJS Templates
awesome_nested_set: import git 2-1-stable branch revision 8eaab19868f326 (#6579)...
Toshi MARUYAMA -
r12870:9ea1ae48dbed
parent child
Show More
@@ -1,68 +1,72
1 1 # Mixed into both classes and instances to provide easy access to the column names
2 2 module CollectiveIdea #:nodoc:
3 3 module Acts #:nodoc:
4 4 module NestedSet #:nodoc:
5 5 module Columns
6 6 def left_column_name
7 7 acts_as_nested_set_options[:left_column]
8 8 end
9 9
10 10 def right_column_name
11 11 acts_as_nested_set_options[:right_column]
12 12 end
13 13
14 14 def depth_column_name
15 15 acts_as_nested_set_options[:depth_column]
16 16 end
17 17
18 18 def parent_column_name
19 19 acts_as_nested_set_options[:parent_column]
20 20 end
21 21
22 22 def order_column
23 23 acts_as_nested_set_options[:order_column] || left_column_name
24 24 end
25 25
26 26 def scope_column_names
27 27 Array(acts_as_nested_set_options[:scope])
28 28 end
29 29
30 30 def quoted_left_column_name
31 31 connection.quote_column_name(left_column_name)
32 32 end
33 33
34 34 def quoted_right_column_name
35 35 connection.quote_column_name(right_column_name)
36 36 end
37 37
38 38 def quoted_depth_column_name
39 39 connection.quote_column_name(depth_column_name)
40 40 end
41 41
42 42 def quoted_parent_column_name
43 43 connection.quote_column_name(parent_column_name)
44 44 end
45 45
46 46 def quoted_scope_column_names
47 47 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
48 48 end
49 49
50 50 def quoted_order_column_name
51 51 connection.quote_column_name(order_column)
52 52 end
53 53
54 def quoted_order_column_full_name
55 "#{quoted_table_name}.#{quoted_order_column_name}"
56 end
57
54 58 def quoted_left_column_full_name
55 59 "#{quoted_table_name}.#{quoted_left_column_name}"
56 60 end
57 61
58 62 def quoted_right_column_full_name
59 63 "#{quoted_table_name}.#{quoted_right_column_name}"
60 64 end
61 65
62 66 def quoted_parent_column_full_name
63 67 "#{quoted_table_name}.#{quoted_parent_column_name}"
64 68 end
65 69 end
66 70 end
67 71 end
68 72 end
@@ -1,212 +1,212
1 1 require 'awesome_nested_set/model/prunable'
2 2 require 'awesome_nested_set/model/movable'
3 3 require 'awesome_nested_set/model/transactable'
4 4 require 'awesome_nested_set/model/relatable'
5 5 require 'awesome_nested_set/model/rebuildable'
6 6 require 'awesome_nested_set/model/validatable'
7 7 require 'awesome_nested_set/iterator'
8 8
9 9 module CollectiveIdea #:nodoc:
10 10 module Acts #:nodoc:
11 11 module NestedSet #:nodoc:
12 12
13 13 module Model
14 14 extend ActiveSupport::Concern
15 15
16 16 included do
17 17 delegate :quoted_table_name, :arel_table, :to => self
18 18 extend Validatable
19 19 extend Rebuildable
20 20 include Movable
21 21 include Prunable
22 22 include Relatable
23 23 include Transactable
24 24 end
25 25
26 26 module ClassMethods
27 27 def associate_parents(objects)
28 28 return objects unless objects.all? {|o| o.respond_to?(:association)}
29 29
30 30 id_indexed = objects.index_by(&:id)
31 31 objects.each do |object|
32 32 association = object.association(:parent)
33 33 parent = id_indexed[object.parent_id]
34 34
35 35 if !association.loaded? && parent
36 36 association.target = parent
37 37 association.set_inverse_instance(parent)
38 38 end
39 39 end
40 40 end
41 41
42 42 def children_of(parent_id)
43 43 where arel_table[parent_column_name].eq(parent_id)
44 44 end
45 45
46 46 # Iterates over tree elements and determines the current level in the tree.
47 47 # Only accepts default ordering, odering by an other column than lft
48 48 # does not work. This method is much more efficent than calling level
49 49 # because it doesn't require any additional database queries.
50 50 #
51 51 # Example:
52 52 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
53 53 #
54 54 def each_with_level(objects, &block)
55 55 Iterator.new(objects).each_with_level(&block)
56 56 end
57 57
58 58 def leaves
59 59 nested_set_scope.where "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
60 60 end
61 61
62 62 def left_of(node)
63 63 where arel_table[left_column_name].lt(node)
64 64 end
65 65
66 66 def left_of_right_side(node)
67 67 where arel_table[right_column_name].lteq(node)
68 68 end
69 69
70 70 def right_of(node)
71 71 where arel_table[left_column_name].gteq(node)
72 72 end
73 73
74 74 def nested_set_scope(options = {})
75 options = {:order => quoted_order_column_name}.merge(options)
75 options = {:order => quoted_order_column_full_name}.merge(options)
76 76
77 77 order(options.delete(:order)).scoped options
78 78 end
79 79
80 80 def primary_key_scope(id)
81 81 where arel_table[primary_key].eq(id)
82 82 end
83 83
84 84 def root
85 85 roots.first
86 86 end
87 87
88 88 def roots
89 89 nested_set_scope.children_of nil
90 90 end
91 91 end # end class methods
92 92
93 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 94 #
95 95 # category.self_and_descendants.count
96 96 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
97 97 # Value of the parent column
98 98 def parent_id(target = self)
99 99 target[parent_column_name]
100 100 end
101 101
102 102 # Value of the left column
103 103 def left(target = self)
104 104 target[left_column_name]
105 105 end
106 106
107 107 # Value of the right column
108 108 def right(target = self)
109 109 target[right_column_name]
110 110 end
111 111
112 112 # Returns true if this is a root node.
113 113 def root?
114 114 parent_id.nil?
115 115 end
116 116
117 117 # Returns true is this is a child node
118 118 def child?
119 119 !root?
120 120 end
121 121
122 122 # Returns true if this is the end of a branch.
123 123 def leaf?
124 124 persisted? && right.to_i - left.to_i == 1
125 125 end
126 126
127 127 # All nested set queries should use this nested_set_scope, which
128 128 # performs finds on the base ActiveRecord class, using the :scope
129 129 # declared in the acts_as_nested_set declaration.
130 130 def nested_set_scope(options = {})
131 131 if (scopes = Array(acts_as_nested_set_options[:scope])).any?
132 132 options[:conditions] = scopes.inject({}) do |conditions,attr|
133 133 conditions.merge attr => self[attr]
134 134 end
135 135 end
136 136
137 137 self.class.nested_set_scope options
138 138 end
139 139
140 140 def to_text
141 141 self_and_descendants.map do |node|
142 142 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
143 143 end.join("\n")
144 144 end
145 145
146 146 protected
147 147
148 148 def without_self(scope)
149 149 return scope if new_record?
150 150 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
151 151 end
152 152
153 153 def store_new_parent
154 154 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
155 155 true # force callback to return true
156 156 end
157 157
158 158 def has_depth_column?
159 159 nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
160 160 end
161 161
162 162 def right_most_node
163 163 @right_most_node ||= self.class.base_class.unscoped.nested_set_scope(
164 164 :order => "#{quoted_right_column_full_name} desc"
165 165 ).first
166 166 end
167 167
168 168 def right_most_bound
169 169 @right_most_bound ||= begin
170 170 return 0 if right_most_node.nil?
171 171
172 172 right_most_node.lock!
173 173 right_most_node[right_column_name] || 0
174 174 end
175 175 end
176 176
177 177 def set_depth!
178 178 return unless has_depth_column?
179 179
180 180 in_tenacious_transaction do
181 181 reload
182 182 nested_set_scope.primary_key_scope(id).
183 183 update_all(["#{quoted_depth_column_name} = ?", level])
184 184 end
185 185 self[depth_column_name] = self.level
186 186 end
187 187
188 188 def set_default_left_and_right
189 189 # adds the new node to the right of all existing nodes
190 190 self[left_column_name] = right_most_bound + 1
191 191 self[right_column_name] = right_most_bound + 2
192 192 end
193 193
194 194 # reload left, right, and parent
195 195 def reload_nested_set
196 196 reload(
197 197 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
198 198 :lock => true
199 199 )
200 200 end
201 201
202 202 def reload_target(target)
203 203 if target.is_a? self.class.base_class
204 204 target.reload
205 205 else
206 206 nested_set_scope.find(target)
207 207 end
208 208 end
209 209 end
210 210 end
211 211 end
212 212 end
@@ -1,58 +1,61
1 1 module CollectiveIdea #:nodoc:
2 2 module Acts #:nodoc:
3 3 module NestedSet #:nodoc:
4 4 module Model
5 5 module Prunable
6 6
7 7 # Prunes a branch off of the tree, shifting all of the elements on the right
8 8 # back to the left so the counts still work.
9 9 def destroy_descendants
10 10 return if right.nil? || left.nil? || skip_before_destroy
11 11
12 12 in_tenacious_transaction do
13 13 reload_nested_set
14 14 # select the rows in the model that extend past the deletion point and apply a lock
15 15 nested_set_scope.right_of(left).select(id).lock(true)
16 16
17 17 destroy_or_delete_descendants
18 18
19 19 # update lefts and rights for remaining nodes
20 20 update_siblings_for_remaining_nodes
21 21
22 # Reload is needed because children may have updated their parent (self) during deletion.
23 reload
24
22 25 # Don't allow multiple calls to destroy to corrupt the set
23 26 self.skip_before_destroy = true
24 27 end
25 28 end
26 29
27 30 def destroy_or_delete_descendants
28 31 if acts_as_nested_set_options[:dependent] == :destroy
29 32 descendants.each do |model|
30 33 model.skip_before_destroy = true
31 34 model.destroy
32 35 end
33 36 else
34 37 descendants.delete_all
35 38 end
36 39 end
37 40
38 41 def update_siblings_for_remaining_nodes
39 42 update_siblings(:left)
40 43 update_siblings(:right)
41 44 end
42 45
43 46 def update_siblings(direction)
44 47 full_column_name = send("quoted_#{direction}_column_full_name")
45 48 column_name = send("quoted_#{direction}_column_name")
46 49
47 50 nested_set_scope.where(["#{full_column_name} > ?", right]).
48 51 update_all(["#{column_name} = (#{column_name} - ?)", diff])
49 52 end
50 53
51 54 def diff
52 55 right - left + 1
53 56 end
54 57 end
55 58 end
56 59 end
57 60 end
58 61 end
@@ -1,69 +1,69
1 1 require 'awesome_nested_set/set_validator'
2 2
3 3 module CollectiveIdea
4 4 module Acts
5 5 module NestedSet
6 6 module Model
7 7 module Validatable
8 8
9 9 def valid?
10 10 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
11 11 end
12 12
13 13 def left_and_rights_valid?
14 14 SetValidator.new(self).valid?
15 15 end
16 16
17 17 def no_duplicates_for_columns?
18 18 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
19 19 # No duplicates
20 select("#{scope_string}#{column}, COUNT(#{column})").
20 select("#{scope_string}#{column}, COUNT(#{column}) as _count").
21 21 group("#{scope_string}#{column}").
22 22 having("COUNT(#{column}) > 1").
23 23 first.nil?
24 24 end
25 25 end
26 26
27 27 # Wrapper for each_root_valid? that can deal with scope.
28 28 def all_roots_valid?
29 29 if acts_as_nested_set_options[:scope]
30 30 all_roots_valid_by_scope?(roots)
31 31 else
32 32 each_root_valid?(roots)
33 33 end
34 34 end
35 35
36 36 def all_roots_valid_by_scope?(roots_to_validate)
37 37 roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots|
38 38 each_root_valid?(grouped_roots)
39 39 end
40 40 end
41 41
42 42 def each_root_valid?(roots_to_validate)
43 43 left = right = 0
44 44 roots_to_validate.all? do |root|
45 45 (root.left > left && root.right > right).tap do
46 46 left = root.left
47 47 right = root.right
48 48 end
49 49 end
50 50 end
51 51
52 52 private
53 53 def roots_grouped_by_scope(roots_to_group)
54 54 roots_to_group.group_by {|record|
55 55 scope_column_names.collect {|col| record.send(col) }
56 56 }
57 57 end
58 58
59 59 def scope_string
60 60 Array(acts_as_nested_set_options[:scope]).map do |c|
61 61 connection.quote_column_name(c)
62 62 end.push(nil).join(", ")
63 63 end
64 64
65 65 end
66 66 end
67 67 end
68 68 end
69 69 end
@@ -1,117 +1,130
1 1 module CollectiveIdea #:nodoc:
2 2 module Acts #:nodoc:
3 3 module NestedSet #:nodoc:
4 4 class Move
5 5 attr_reader :target, :position, :instance
6 6
7 7 def initialize(target, position, instance)
8 8 @target = target
9 9 @position = position
10 10 @instance = instance
11 11 end
12 12
13 13 def move
14 14 prevent_impossible_move
15 15
16 16 bound, other_bound = get_boundaries
17 17
18 18 # there would be no change
19 19 return if bound == right || bound == left
20 20
21 21 # we have defined the boundaries of two non-overlapping intervals,
22 22 # so sorting puts both the intervals and their boundaries in order
23 23 a, b, c, d = [left, right, bound, other_bound].sort
24 24
25 25 lock_nodes_between! a, d
26 26
27 nested_set_scope.where(where_statement(a, d)).
28 update_all(conditions(a, b, c, d))
27 nested_set_scope.where(where_statement(a, d)).update_all(
28 conditions(a, b, c, d)
29 )
29 30 end
30 31
31 32 private
32 33
33 34 delegate :left, :right, :left_column_name, :right_column_name,
34 35 :quoted_left_column_name, :quoted_right_column_name,
35 36 :quoted_parent_column_name, :parent_column_name, :nested_set_scope,
36 37 :to => :instance
37 38
38 39 delegate :arel_table, :class, :to => :instance, :prefix => true
39 40 delegate :base_class, :to => :instance_class, :prefix => :instance
40 41
41 42 def where_statement(left_bound, right_bound)
42 43 instance_arel_table[left_column_name].in(left_bound..right_bound).
43 44 or(instance_arel_table[right_column_name].in(left_bound..right_bound))
44 45 end
45 46
46 47 def conditions(a, b, c, d)
48 _conditions = case_condition_for_direction(:quoted_left_column_name) +
49 case_condition_for_direction(:quoted_right_column_name) +
50 case_condition_for_parent
51
52 # We want the record to be 'touched' if it timestamps.
53 if @instance.respond_to?(:updated_at)
54 _conditions << ", updated_at = :timestamp"
55 end
56
47 57 [
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}
58 _conditions,
59 {
60 :a => a, :b => b, :c => c, :d => d,
61 :id => instance.id,
62 :new_parent => new_parent,
63 :timestamp => Time.now.utc
64 }
52 65 ]
53 66 end
54 67
55 68 def case_condition_for_direction(column_name)
56 69 column = send(column_name)
57 70 "#{column} = CASE " +
58 71 "WHEN #{column} BETWEEN :a AND :b " +
59 72 "THEN #{column} + :d - :b " +
60 73 "WHEN #{column} BETWEEN :c AND :d " +
61 74 "THEN #{column} + :a - :c " +
62 75 "ELSE #{column} END, "
63 76 end
64 77
65 78 def case_condition_for_parent
66 79 "#{quoted_parent_column_name} = CASE " +
67 80 "WHEN #{instance_base_class.primary_key} = :id THEN :new_parent " +
68 81 "ELSE #{quoted_parent_column_name} END"
69 82 end
70 83
71 84 def lock_nodes_between!(left_bound, right_bound)
72 85 # select the rows in the model between a and d, and apply a lock
73 86 instance_base_class.right_of(left_bound).left_of_right_side(right_bound).
74 87 select(:id).lock(true)
75 88 end
76 89
77 90 def root
78 91 position == :root
79 92 end
80 93
81 94 def new_parent
82 95 case position
83 96 when :child
84 97 target.id
85 98 else
86 99 target[parent_column_name]
87 100 end
88 101 end
89 102
90 103 def get_boundaries
91 104 if (bound = target_bound) > right
92 105 bound -= 1
93 106 other_bound = right + 1
94 107 else
95 108 other_bound = left - 1
96 109 end
97 110 [bound, other_bound]
98 111 end
99 112
100 113 def prevent_impossible_move
101 114 if !root && !instance.move_possible?(target)
102 115 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
103 116 end
104 117 end
105 118
106 119 def target_bound
107 120 case position
108 121 when :child; right(target)
109 122 when :left; left(target)
110 123 when :right; right(target) + 1
111 124 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
112 125 end
113 126 end
114 127 end
115 128 end
116 129 end
117 130 end
General Comments 0
You need to be logged in to leave comments. Login now