##// END OF EJS Templates
apply Redmine awesome_nested_set 2.1.5 modification...
Toshi MARUYAMA -
r12404:bcec29d5e42b
parent child
Show More
@@ -1,744 +1,748
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4
4
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
7 # descendants with a single query. The drawback is that insertion or move need some complex
7 # descendants with a single query. The drawback is that insertion or move need some complex
8 # sql queries. But everything is done here by this module!
8 # sql queries. But everything is done here by this module!
9 #
9 #
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 #
12 #
13 # == API
13 # == API
14 #
14 #
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 # by another easier.
16 # by another easier.
17 #
17 #
18 # item.children.create(:name => "child1")
18 # item.children.create(:name => "child1")
19 #
19 #
20
20
21 # Configuration options are:
21 # Configuration options are:
22 #
22 #
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 # * +:left_column+ - column name for left boundry data, default "lft"
24 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:depth_column+ - column name for the depth data, default "depth"
26 # * +:depth_column+ - column name for the depth data, default "depth"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
28 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # (if it hasn't been already) and use that as the foreign key restriction. You
29 # can also pass an array to scope by multiple attributes.
29 # can also pass an array to scope by multiple attributes.
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
32 # child objects are destroyed alongside this object by calling their destroy
32 # child objects are destroyed alongside this object by calling their destroy
33 # method. If set to :delete_all (default), all the child objects are deleted
33 # method. If set to :delete_all (default), all the child objects are deleted
34 # without calling their destroy method.
34 # without calling their destroy method.
35 # * +:counter_cache+ adds a counter cache for the number of children.
35 # * +:counter_cache+ adds a counter cache for the number of children.
36 # defaults to false.
36 # defaults to false.
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
40 #
40 #
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
43 # to acts_as_nested_set models
43 # to acts_as_nested_set models
44 def acts_as_nested_set(options = {})
44 def acts_as_nested_set(options = {})
45 options = {
45 options = {
46 :parent_column => 'parent_id',
46 :parent_column => 'parent_id',
47 :left_column => 'lft',
47 :left_column => 'lft',
48 :right_column => 'rgt',
48 :right_column => 'rgt',
49 :depth_column => 'depth',
49 :depth_column => 'depth',
50 :dependent => :delete_all, # or :destroy
50 :dependent => :delete_all, # or :destroy
51 :polymorphic => false,
51 :polymorphic => false,
52 :counter_cache => false
52 :counter_cache => false
53 }.merge(options)
53 }.merge(options)
54
54
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
56 options[:scope] = "#{options[:scope]}_id".intern
56 options[:scope] = "#{options[:scope]}_id".intern
57 end
57 end
58
58
59 class_attribute :acts_as_nested_set_options
59 class_attribute :acts_as_nested_set_options
60 self.acts_as_nested_set_options = options
60 self.acts_as_nested_set_options = options
61
61
62 include CollectiveIdea::Acts::NestedSet::Model
62 include CollectiveIdea::Acts::NestedSet::Model
63 include Columns
63 include Columns
64 extend Columns
64 extend Columns
65
65
66 belongs_to :parent, :class_name => self.base_class.to_s,
66 belongs_to :parent, :class_name => self.base_class.to_s,
67 :foreign_key => parent_column_name,
67 :foreign_key => parent_column_name,
68 :counter_cache => options[:counter_cache],
68 :counter_cache => options[:counter_cache],
69 :inverse_of => (:children unless options[:polymorphic]),
69 :inverse_of => (:children unless options[:polymorphic]),
70 :polymorphic => options[:polymorphic]
70 :polymorphic => options[:polymorphic]
71
71
72 has_many_children_options = {
72 has_many_children_options = {
73 :class_name => self.base_class.to_s,
73 :class_name => self.base_class.to_s,
74 :foreign_key => parent_column_name,
74 :foreign_key => parent_column_name,
75 :order => order_column,
75 :order => order_column,
76 :inverse_of => (:parent unless options[:polymorphic]),
76 :inverse_of => (:parent unless options[:polymorphic]),
77 }
77 }
78
78
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
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|
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]
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
82 end
82 end
83
83
84 has_many :children, has_many_children_options
84 has_many :children, has_many_children_options
85
85
86 attr_accessor :skip_before_destroy
86 attr_accessor :skip_before_destroy
87
87
88 before_create :set_default_left_and_right
88 before_create :set_default_left_and_right
89 before_save :store_new_parent
89 before_save :store_new_parent
90 after_save :move_to_new_parent, :set_depth!
90 after_save :move_to_new_parent, :set_depth!
91 before_destroy :destroy_descendants
91 before_destroy :destroy_descendants
92
92
93 # no assignment to structure fields
93 # no assignment to structure fields
94 [left_column_name, right_column_name, depth_column_name].each do |column|
94 [left_column_name, right_column_name, depth_column_name].each do |column|
95 module_eval <<-"end_eval", __FILE__, __LINE__
95 module_eval <<-"end_eval", __FILE__, __LINE__
96 def #{column}=(x)
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."
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
98 end
99 end_eval
99 end_eval
100 end
100 end
101
101
102 define_model_callbacks :move
102 define_model_callbacks :move
103 end
103 end
104
104
105 module Model
105 module Model
106 extend ActiveSupport::Concern
106 extend ActiveSupport::Concern
107
107
108 included do
108 included do
109 delegate :quoted_table_name, :to => self
109 delegate :quoted_table_name, :to => self
110 end
110 end
111
111
112 module ClassMethods
112 module ClassMethods
113 # Returns the first root
113 # Returns the first root
114 def root
114 def root
115 roots.first
115 roots.first
116 end
116 end
117
117
118 def roots
118 def roots
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
120 end
120 end
121
121
122 def leaves
122 def leaves
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
124 end
124 end
125
125
126 def valid?
126 def valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
128 end
128 end
129
129
130 def left_and_rights_valid?
130 def left_and_rights_valid?
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134 "parent ON " +
134 "parent ON " +
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
136 where(
136 where(
137 "#{quoted_left_column_full_name} IS NULL OR " +
137 "#{quoted_left_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
139 "#{quoted_left_column_full_name} >= " +
139 "#{quoted_left_column_full_name} >= " +
140 "#{quoted_right_column_full_name} OR " +
140 "#{quoted_right_column_full_name} OR " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
144 ).count == 0
144 ).count == 0
145 end
145 end
146
146
147 def no_duplicates_for_columns?
147 def no_duplicates_for_columns?
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
149 connection.quote_column_name(c)
149 connection.quote_column_name(c)
150 end.push(nil).join(", ")
150 end.push(nil).join(", ")
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
152 # No duplicates
152 # No duplicates
153 select("#{scope_string}#{column}, COUNT(#{column})").
153 select("#{scope_string}#{column}, COUNT(#{column})").
154 group("#{scope_string}#{column}").
154 group("#{scope_string}#{column}").
155 having("COUNT(#{column}) > 1").
155 having("COUNT(#{column}) > 1").
156 first.nil?
156 first.nil?
157 end
157 end
158 end
158 end
159
159
160 # Wrapper for each_root_valid? that can deal with scope.
160 # Wrapper for each_root_valid? that can deal with scope.
161 def all_roots_valid?
161 def all_roots_valid?
162 if acts_as_nested_set_options[:scope]
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|
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)
164 each_root_valid?(grouped_roots)
165 end
165 end
166 else
166 else
167 each_root_valid?(roots)
167 each_root_valid?(roots)
168 end
168 end
169 end
169 end
170
170
171 def each_root_valid?(roots_to_validate)
171 def each_root_valid?(roots_to_validate)
172 left = right = 0
172 left = right = 0
173 roots_to_validate.all? do |root|
173 roots_to_validate.all? do |root|
174 (root.left > left && root.right > right).tap do
174 (root.left > left && root.right > right).tap do
175 left = root.left
175 left = root.left
176 right = root.right
176 right = root.right
177 end
177 end
178 end
178 end
179 end
179 end
180
180
181 # Rebuilds the left & rights if unset or invalid.
181 # Rebuilds the left & rights if unset or invalid.
182 # Also very useful for converting from acts_as_tree.
182 # Also very useful for converting from acts_as_tree.
183 def rebuild!(validate_nodes = true)
183 def rebuild!(validate_nodes = true)
184 # Don't rebuild a valid tree.
184 # Don't rebuild a valid tree.
185 return true if valid?
185 return true if valid?
186
186
187 scope = lambda{|node|}
187 scope = lambda{|node|}
188 if acts_as_nested_set_options[:scope]
188 if acts_as_nested_set_options[:scope]
189 scope = lambda{|node|
189 scope = lambda{|node|
190 scope_column_names.inject(""){|str, column_name|
190 scope_column_names.inject(""){|str, column_name|
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
192 }
192 }
193 }
193 }
194 end
194 end
195 indices = {}
195 indices = {}
196
196
197 set_left_and_rights = lambda do |node|
197 set_left_and_rights = lambda do |node|
198 # set left
198 # set left
199 node[left_column_name] = indices[scope.call(node)] += 1
199 node[left_column_name] = indices[scope.call(node)] += 1
200 # find
200 # find
201 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) }
201 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).
202 order(acts_as_nested_set_options[:order]).
203 each{|n| set_left_and_rights.call(n) }
202 # set right
204 # set right
203 node[right_column_name] = indices[scope.call(node)] += 1
205 node[right_column_name] = indices[scope.call(node)] += 1
204 node.save!(:validate => validate_nodes)
206 node.save!(:validate => validate_nodes)
205 end
207 end
206
208
207 # Find root node(s)
209 # Find root node(s)
208 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|
210 root_nodes = where("#{quoted_parent_column_name} IS NULL").
211 order(acts_as_nested_set_options[:order]).each do |root_node|
209 # setup index for this scope
212 # setup index for this scope
210 indices[scope.call(root_node)] ||= 0
213 indices[scope.call(root_node)] ||= 0
211 set_left_and_rights.call(root_node)
214 set_left_and_rights.call(root_node)
212 end
215 end
213 end
216 end
214
217
215 # Iterates over tree elements and determines the current level in the tree.
218 # Iterates over tree elements and determines the current level in the tree.
216 # Only accepts default ordering, odering by an other column than lft
219 # Only accepts default ordering, odering by an other column than lft
217 # does not work. This method is much more efficent than calling level
220 # does not work. This method is much more efficent than calling level
218 # because it doesn't require any additional database queries.
221 # because it doesn't require any additional database queries.
219 #
222 #
220 # Example:
223 # Example:
221 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
224 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
222 #
225 #
223 def each_with_level(objects)
226 def each_with_level(objects)
224 path = [nil]
227 path = [nil]
225 objects.each do |o|
228 objects.each do |o|
226 if o.parent_id != path.last
229 if o.parent_id != path.last
227 # we are on a new level, did we descend or ascend?
230 # we are on a new level, did we descend or ascend?
228 if path.include?(o.parent_id)
231 if path.include?(o.parent_id)
229 # remove wrong wrong tailing paths elements
232 # remove wrong wrong tailing paths elements
230 path.pop while path.last != o.parent_id
233 path.pop while path.last != o.parent_id
231 else
234 else
232 path << o.parent_id
235 path << o.parent_id
233 end
236 end
234 end
237 end
235 yield(o, path.length - 1)
238 yield(o, path.length - 1)
236 end
239 end
237 end
240 end
238
241
239 # Same as each_with_level - Accepts a string as a second argument to sort the list
242 # Same as each_with_level - Accepts a string as a second argument to sort the list
240 # Example:
243 # Example:
241 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
244 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
242 def sorted_each_with_level(objects, order)
245 def sorted_each_with_level(objects, order)
243 path = [nil]
246 path = [nil]
244 children = []
247 children = []
245 objects.each do |o|
248 objects.each do |o|
246 children << o if o.leaf?
249 children << o if o.leaf?
247 if o.parent_id != path.last
250 if o.parent_id != path.last
248 if !children.empty? && !o.leaf?
251 if !children.empty? && !o.leaf?
249 children.sort_by! &order
252 children.sort_by! &order
250 children.each { |c| yield(c, path.length-1) }
253 children.each { |c| yield(c, path.length-1) }
251 children = []
254 children = []
252 end
255 end
253 # we are on a new level, did we decent or ascent?
256 # we are on a new level, did we decent or ascent?
254 if path.include?(o.parent_id)
257 if path.include?(o.parent_id)
255 # remove wrong wrong tailing paths elements
258 # remove wrong wrong tailing paths elements
256 path.pop while path.last != o.parent_id
259 path.pop while path.last != o.parent_id
257 else
260 else
258 path << o.parent_id
261 path << o.parent_id
259 end
262 end
260 end
263 end
261 yield(o,path.length-1) if !o.leaf?
264 yield(o,path.length-1) if !o.leaf?
262 end
265 end
263 if !children.empty?
266 if !children.empty?
264 children.sort_by! &order
267 children.sort_by! &order
265 children.each { |c| yield(c, path.length-1) }
268 children.each { |c| yield(c, path.length-1) }
266 end
269 end
267 end
270 end
268
271
269 def associate_parents(objects)
272 def associate_parents(objects)
270 if objects.all?{|o| o.respond_to?(:association)}
273 if objects.all?{|o| o.respond_to?(:association)}
271 id_indexed = objects.index_by(&:id)
274 id_indexed = objects.index_by(&:id)
272 objects.each do |object|
275 objects.each do |object|
273 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
276 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
274 association.target = parent
277 association.target = parent
275 association.set_inverse_instance(parent)
278 association.set_inverse_instance(parent)
276 end
279 end
277 end
280 end
278 else
281 else
279 objects
282 objects
280 end
283 end
281 end
284 end
282 end
285 end
283
286
284 # 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.
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.
285 #
288 #
286 # category.self_and_descendants.count
289 # category.self_and_descendants.count
287 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
290 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
288 # Value of the parent column
291 # Value of the parent column
289 def parent_id
292 def parent_id
290 self[parent_column_name]
293 self[parent_column_name]
291 end
294 end
292
295
293 # Value of the left column
296 # Value of the left column
294 def left
297 def left
295 self[left_column_name]
298 self[left_column_name]
296 end
299 end
297
300
298 # Value of the right column
301 # Value of the right column
299 def right
302 def right
300 self[right_column_name]
303 self[right_column_name]
301 end
304 end
302
305
303 # Returns true if this is a root node.
306 # Returns true if this is a root node.
304 def root?
307 def root?
305 parent_id.nil?
308 parent_id.nil?
306 end
309 end
307
310
308 # Returns true if this is the end of a branch.
311 # Returns true if this is the end of a branch.
309 def leaf?
312 def leaf?
310 persisted? && right.to_i - left.to_i == 1
313 new_record? || (persisted? && right.to_i - left.to_i == 1)
311 end
314 end
312
315
313 # Returns true is this is a child node
316 # Returns true is this is a child node
314 def child?
317 def child?
315 !root?
318 !root?
316 end
319 end
317
320
318 # Returns root
321 # Returns root
319 def root
322 def root
320 if persisted?
323 if persisted?
321 self_and_ancestors.where(parent_column_name => nil).first
324 self_and_ancestors.where(parent_column_name => nil).first
322 else
325 else
323 if parent_id && current_parent = nested_set_scope.find(parent_id)
326 if parent_id && current_parent = nested_set_scope.find(parent_id)
324 current_parent.root
327 current_parent.root
325 else
328 else
326 self
329 self
327 end
330 end
328 end
331 end
329 end
332 end
330
333
331 # Returns the array of all parents and self
334 # Returns the array of all parents and self
332 def self_and_ancestors
335 def self_and_ancestors
333 nested_set_scope.where([
336 nested_set_scope.where([
334 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
337 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
335 ])
338 ])
336 end
339 end
337
340
338 # Returns an array of all parents
341 # Returns an array of all parents
339 def ancestors
342 def ancestors
340 without_self self_and_ancestors
343 without_self self_and_ancestors
341 end
344 end
342
345
343 # Returns the array of all children of the parent, including self
346 # Returns the array of all children of the parent, including self
344 def self_and_siblings
347 def self_and_siblings
345 nested_set_scope.where(parent_column_name => parent_id)
348 nested_set_scope.where(parent_column_name => parent_id)
346 end
349 end
347
350
348 # Returns the array of all children of the parent, except self
351 # Returns the array of all children of the parent, except self
349 def siblings
352 def siblings
350 without_self self_and_siblings
353 without_self self_and_siblings
351 end
354 end
352
355
353 # Returns a set of all of its nested children which do not have children
356 # Returns a set of all of its nested children which do not have children
354 def leaves
357 def leaves
355 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
358 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
356 end
359 end
357
360
358 # Returns the level of this object in the tree
361 # Returns the level of this object in the tree
359 # root level is 0
362 # root level is 0
360 def level
363 def level
361 parent_id.nil? ? 0 : compute_level
364 parent_id.nil? ? 0 : compute_level
362 end
365 end
363
366
364 # Returns a set of itself and all of its nested children
367 # Returns a set of itself and all of its nested children
365 def self_and_descendants
368 def self_and_descendants
366 nested_set_scope.where([
369 nested_set_scope.where([
367 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
370 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
368 # using _left_ for both sides here lets us benefit from an index on that column if one exists
371 # using _left_ for both sides here lets us benefit from an index on that column if one exists
369 ])
372 ])
370 end
373 end
371
374
372 # Returns a set of all of its children and nested children
375 # Returns a set of all of its children and nested children
373 def descendants
376 def descendants
374 without_self self_and_descendants
377 without_self self_and_descendants
375 end
378 end
376
379
377 def is_descendant_of?(other)
380 def is_descendant_of?(other)
378 other.left < self.left && self.left < other.right && same_scope?(other)
381 other.left < self.left && self.left < other.right && same_scope?(other)
379 end
382 end
380
383
381 def is_or_is_descendant_of?(other)
384 def is_or_is_descendant_of?(other)
382 other.left <= self.left && self.left < other.right && same_scope?(other)
385 other.left <= self.left && self.left < other.right && same_scope?(other)
383 end
386 end
384
387
385 def is_ancestor_of?(other)
388 def is_ancestor_of?(other)
386 self.left < other.left && other.left < self.right && same_scope?(other)
389 self.left < other.left && other.left < self.right && same_scope?(other)
387 end
390 end
388
391
389 def is_or_is_ancestor_of?(other)
392 def is_or_is_ancestor_of?(other)
390 self.left <= other.left && other.left < self.right && same_scope?(other)
393 self.left <= other.left && other.left < self.right && same_scope?(other)
391 end
394 end
392
395
393 # Check if other model is in the same scope
396 # Check if other model is in the same scope
394 def same_scope?(other)
397 def same_scope?(other)
395 Array(acts_as_nested_set_options[:scope]).all? do |attr|
398 Array(acts_as_nested_set_options[:scope]).all? do |attr|
396 self.send(attr) == other.send(attr)
399 self.send(attr) == other.send(attr)
397 end
400 end
398 end
401 end
399
402
400 # Find the first sibling to the left
403 # Find the first sibling to the left
401 def left_sibling
404 def left_sibling
402 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
405 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
403 order("#{quoted_left_column_full_name} DESC").last
406 order("#{quoted_left_column_full_name} DESC").last
404 end
407 end
405
408
406 # Find the first sibling to the right
409 # Find the first sibling to the right
407 def right_sibling
410 def right_sibling
408 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
411 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
409 end
412 end
410
413
411 # Shorthand method for finding the left sibling and moving to the left of it.
414 # Shorthand method for finding the left sibling and moving to the left of it.
412 def move_left
415 def move_left
413 move_to_left_of left_sibling
416 move_to_left_of left_sibling
414 end
417 end
415
418
416 # Shorthand method for finding the right sibling and moving to the right of it.
419 # Shorthand method for finding the right sibling and moving to the right of it.
417 def move_right
420 def move_right
418 move_to_right_of right_sibling
421 move_to_right_of right_sibling
419 end
422 end
420
423
421 # Move the node to the left of another node (you can pass id only)
424 # Move the node to the left of another node (you can pass id only)
422 def move_to_left_of(node)
425 def move_to_left_of(node)
423 move_to node, :left
426 move_to node, :left
424 end
427 end
425
428
426 # Move the node to the left of another node (you can pass id only)
429 # Move the node to the left of another node (you can pass id only)
427 def move_to_right_of(node)
430 def move_to_right_of(node)
428 move_to node, :right
431 move_to node, :right
429 end
432 end
430
433
431 # Move the node to the child of another node (you can pass id only)
434 # Move the node to the child of another node (you can pass id only)
432 def move_to_child_of(node)
435 def move_to_child_of(node)
433 move_to node, :child
436 move_to node, :child
434 end
437 end
435
438
436 # Move the node to the child of another node with specify index (you can pass id only)
439 # Move the node to the child of another node with specify index (you can pass id only)
437 def move_to_child_with_index(node, index)
440 def move_to_child_with_index(node, index)
438 if node.children.empty?
441 if node.children.empty?
439 move_to_child_of(node)
442 move_to_child_of(node)
440 elsif node.children.count == index
443 elsif node.children.count == index
441 move_to_right_of(node.children.last)
444 move_to_right_of(node.children.last)
442 else
445 else
443 move_to_left_of(node.children[index])
446 move_to_left_of(node.children[index])
444 end
447 end
445 end
448 end
446
449
447 # Move the node to root nodes
450 # Move the node to root nodes
448 def move_to_root
451 def move_to_root
449 move_to nil, :root
452 move_to nil, :root
450 end
453 end
451
454
452 # Order children in a nested set by an attribute
455 # Order children in a nested set by an attribute
453 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
456 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
454 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
457 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
455 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
458 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
456 self.move_to_root and return unless parent
459 self.move_to_root and return unless parent
457 left = nil # This is needed, at least for the tests.
460 left = nil # This is needed, at least for the tests.
458 parent.children.each do |n| # Find the node immediately to the left of this node.
461 parent.children.each do |n| # Find the node immediately to the left of this node.
459 if ascending
462 if ascending
460 left = n if n.send(order_attribute) < self.send(order_attribute)
463 left = n if n.send(order_attribute) < self.send(order_attribute)
461 else
464 else
462 left = n if n.send(order_attribute) > self.send(order_attribute)
465 left = n if n.send(order_attribute) > self.send(order_attribute)
463 end
466 end
464 end
467 end
465 self.move_to_child_of(parent)
468 self.move_to_child_of(parent)
466 return unless parent.children.count > 1 # Only need to order if there are multiple children.
469 return unless parent.children.count > 1 # Only need to order if there are multiple children.
467 if left # Self has a left neighbor.
470 if left # Self has a left neighbor.
468 self.move_to_right_of(left)
471 self.move_to_right_of(left)
469 else # Self is the left most node.
472 else # Self is the left most node.
470 self.move_to_left_of(parent.children[0])
473 self.move_to_left_of(parent.children[0])
471 end
474 end
472 end
475 end
473
476
474 def move_possible?(target)
477 def move_possible?(target)
475 self != target && # Can't target self
478 self != target && # Can't target self
476 same_scope?(target) && # can't be in different scopes
479 same_scope?(target) && # can't be in different scopes
477 # !(left..right).include?(target.left..target.right) # this needs tested more
480 # !(left..right).include?(target.left..target.right) # this needs tested more
478 # detect impossible move
481 # detect impossible move
479 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
482 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
480 end
483 end
481
484
482 def to_text
485 def to_text
483 self_and_descendants.map do |node|
486 self_and_descendants.map do |node|
484 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
487 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
485 end.join("\n")
488 end.join("\n")
486 end
489 end
487
490
488 protected
491 protected
489 def compute_level
492 def compute_level
490 node, nesting = self, 0
493 node, nesting = self, 0
491 while (association = node.association(:parent)).loaded? && association.target
494 while (association = node.association(:parent)).loaded? && association.target
492 nesting += 1
495 nesting += 1
493 node = node.parent
496 node = node.parent
494 end if node.respond_to? :association
497 end if node.respond_to? :association
495 node == self ? ancestors.count : node.level + nesting
498 node == self ? ancestors.count : node.level + nesting
496 end
499 end
497
500
498 def without_self(scope)
501 def without_self(scope)
499 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
502 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
500 end
503 end
501
504
502 # All nested set queries should use this nested_set_scope, which performs finds on
505 # All nested set queries should use this nested_set_scope, which performs finds on
503 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
506 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
504 # declaration.
507 # declaration.
505 def nested_set_scope(options = {})
508 def nested_set_scope(options = {})
506 options = {:order => quoted_left_column_full_name}.merge(options)
509 options = {:order => quoted_left_column_full_name}.merge(options)
507 scopes = Array(acts_as_nested_set_options[:scope])
510 scopes = Array(acts_as_nested_set_options[:scope])
508 options[:conditions] = scopes.inject({}) do |conditions,attr|
511 options[:conditions] = scopes.inject({}) do |conditions,attr|
509 conditions.merge attr => self[attr]
512 conditions.merge attr => self[attr]
510 end unless scopes.empty?
513 end unless scopes.empty?
511 self.class.base_class.unscoped.scoped options
514 self.class.base_class.unscoped.scoped options
512 end
515 end
513
516
514 def store_new_parent
517 def store_new_parent
515 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
518 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
516 true # force callback to return true
519 true # force callback to return true
517 end
520 end
518
521
519 def move_to_new_parent
522 def move_to_new_parent
520 if @move_to_new_parent_id.nil?
523 if @move_to_new_parent_id.nil?
521 move_to_root
524 move_to_root
522 elsif @move_to_new_parent_id
525 elsif @move_to_new_parent_id
523 move_to_child_of(@move_to_new_parent_id)
526 move_to_child_of(@move_to_new_parent_id)
524 end
527 end
525 end
528 end
526
529
527 def set_depth!
530 def set_depth!
528 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
531 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
529 in_tenacious_transaction do
532 in_tenacious_transaction do
530 reload
533 reload
531
534
532 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
535 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
533 end
536 end
534 self[depth_column_name.to_sym] = self.level
537 self[depth_column_name.to_sym] = self.level
535 end
538 end
536 end
539 end
537
540
538 # on creation, set automatically lft and rgt to the end of the tree
541 # on creation, set automatically lft and rgt to the end of the tree
539 def set_default_left_and_right
542 def set_default_left_and_right
540 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
543 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
541 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
544 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
542 # adds the new node to the right of all existing nodes
545 # adds the new node to the right of all existing nodes
543 self[left_column_name] = maxright + 1
546 self[left_column_name] = maxright + 1
544 self[right_column_name] = maxright + 2
547 self[right_column_name] = maxright + 2
545 end
548 end
546
549
547 def in_tenacious_transaction(&block)
550 def in_tenacious_transaction(&block)
548 retry_count = 0
551 retry_count = 0
549 begin
552 begin
550 transaction(&block)
553 transaction(&block)
551 rescue ActiveRecord::StatementInvalid => error
554 rescue ActiveRecord::StatementInvalid => error
552 raise unless connection.open_transactions.zero?
555 raise unless connection.open_transactions.zero?
553 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
556 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
554 raise unless retry_count < 10
557 raise unless retry_count < 10
555 retry_count += 1
558 retry_count += 1
556 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
559 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
557 sleep(rand(retry_count)*0.1) # Aloha protocol
560 sleep(rand(retry_count)*0.1) # Aloha protocol
558 retry
561 retry
559 end
562 end
560 end
563 end
561
564
562 # Prunes a branch off of the tree, shifting all of the elements on the right
565 # Prunes a branch off of the tree, shifting all of the elements on the right
563 # back to the left so the counts still work.
566 # back to the left so the counts still work.
564 def destroy_descendants
567 def destroy_descendants
565 return if right.nil? || left.nil? || skip_before_destroy
568 return if right.nil? || left.nil? || skip_before_destroy
566
569
567 in_tenacious_transaction do
570 in_tenacious_transaction do
568 reload_nested_set
571 reload_nested_set
569 # select the rows in the model that extend past the deletion point and apply a lock
572 # select the rows in the model that extend past the deletion point and apply a lock
570 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
573 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
571 select(id).lock(true)
574 select(id).lock(true)
572
575
573 if acts_as_nested_set_options[:dependent] == :destroy
576 if acts_as_nested_set_options[:dependent] == :destroy
574 descendants.each do |model|
577 descendants.each do |model|
575 model.skip_before_destroy = true
578 model.skip_before_destroy = true
576 model.destroy
579 model.destroy
577 end
580 end
578 else
581 else
579 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
582 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
580 delete_all
583 delete_all
581 end
584 end
582
585
583 # update lefts and rights for remaining nodes
586 # update lefts and rights for remaining nodes
584 diff = right - left + 1
587 diff = right - left + 1
585 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
588 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
586 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
589 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
587 )
590 )
588
591
589 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
592 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
590 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
593 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
591 )
594 )
592
595
596 reload
593 # Don't allow multiple calls to destroy to corrupt the set
597 # Don't allow multiple calls to destroy to corrupt the set
594 self.skip_before_destroy = true
598 self.skip_before_destroy = true
595 end
599 end
596 end
600 end
597
601
598 # reload left, right, and parent
602 # reload left, right, and parent
599 def reload_nested_set
603 def reload_nested_set
600 reload(
604 reload(
601 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
605 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
602 :lock => true
606 :lock => true
603 )
607 )
604 end
608 end
605
609
606 def move_to(target, position)
610 def move_to(target, position)
607 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
611 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
608 run_callbacks :move do
612 run_callbacks :move do
609 in_tenacious_transaction do
613 in_tenacious_transaction do
610 if target.is_a? self.class.base_class
614 if target.is_a? self.class.base_class
611 target.reload_nested_set
615 target.reload_nested_set
612 elsif position != :root
616 elsif position != :root
613 # load object if node is not an object
617 # load object if node is not an object
614 target = nested_set_scope.find(target)
618 target = nested_set_scope.find(target)
615 end
619 end
616 self.reload_nested_set
620 self.reload_nested_set
617
621
618 unless position == :root || move_possible?(target)
622 unless position == :root || move_possible?(target)
619 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
623 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
620 end
624 end
621
625
622 bound = case position
626 bound = case position
623 when :child; target[right_column_name]
627 when :child; target[right_column_name]
624 when :left; target[left_column_name]
628 when :left; target[left_column_name]
625 when :right; target[right_column_name] + 1
629 when :right; target[right_column_name] + 1
626 when :root; 1
630 when :root; 1
627 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
631 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
628 end
632 end
629
633
630 if bound > self[right_column_name]
634 if bound > self[right_column_name]
631 bound = bound - 1
635 bound = bound - 1
632 other_bound = self[right_column_name] + 1
636 other_bound = self[right_column_name] + 1
633 else
637 else
634 other_bound = self[left_column_name] - 1
638 other_bound = self[left_column_name] - 1
635 end
639 end
636
640
637 # there would be no change
641 # there would be no change
638 return if bound == self[right_column_name] || bound == self[left_column_name]
642 return if bound == self[right_column_name] || bound == self[left_column_name]
639
643
640 # we have defined the boundaries of two non-overlapping intervals,
644 # we have defined the boundaries of two non-overlapping intervals,
641 # so sorting puts both the intervals and their boundaries in order
645 # so sorting puts both the intervals and their boundaries in order
642 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
646 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
643
647
644 # select the rows in the model between a and d, and apply a lock
648 # select the rows in the model between a and d, and apply a lock
645 self.class.base_class.select('id').lock(true).where(
649 self.class.base_class.select('id').lock(true).where(
646 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
650 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
647 )
651 )
648
652
649 new_parent = case position
653 new_parent = case position
650 when :child; target.id
654 when :child; target.id
651 when :root; nil
655 when :root; nil
652 else target[parent_column_name]
656 else target[parent_column_name]
653 end
657 end
654
658
655 self.nested_set_scope.update_all([
659 self.nested_set_scope.update_all([
656 "#{quoted_left_column_name} = CASE " +
660 "#{quoted_left_column_name} = CASE " +
657 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
661 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
658 "THEN #{quoted_left_column_name} + :d - :b " +
662 "THEN #{quoted_left_column_name} + :d - :b " +
659 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
663 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
660 "THEN #{quoted_left_column_name} + :a - :c " +
664 "THEN #{quoted_left_column_name} + :a - :c " +
661 "ELSE #{quoted_left_column_name} END, " +
665 "ELSE #{quoted_left_column_name} END, " +
662 "#{quoted_right_column_name} = CASE " +
666 "#{quoted_right_column_name} = CASE " +
663 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
667 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
664 "THEN #{quoted_right_column_name} + :d - :b " +
668 "THEN #{quoted_right_column_name} + :d - :b " +
665 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
669 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
666 "THEN #{quoted_right_column_name} + :a - :c " +
670 "THEN #{quoted_right_column_name} + :a - :c " +
667 "ELSE #{quoted_right_column_name} END, " +
671 "ELSE #{quoted_right_column_name} END, " +
668 "#{quoted_parent_column_name} = CASE " +
672 "#{quoted_parent_column_name} = CASE " +
669 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
673 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
670 "ELSE #{quoted_parent_column_name} END",
674 "ELSE #{quoted_parent_column_name} END",
671 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
675 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
672 ])
676 ])
673 end
677 end
674 target.reload_nested_set if target
678 target.reload_nested_set if target
675 self.set_depth!
679 self.set_depth!
676 self.descendants.each(&:save)
680 self.descendants.each(&:save)
677 self.reload_nested_set
681 self.reload_nested_set
678 end
682 end
679 end
683 end
680
684
681 end
685 end
682
686
683 # Mixed into both classes and instances to provide easy access to the column names
687 # Mixed into both classes and instances to provide easy access to the column names
684 module Columns
688 module Columns
685 def left_column_name
689 def left_column_name
686 acts_as_nested_set_options[:left_column]
690 acts_as_nested_set_options[:left_column]
687 end
691 end
688
692
689 def right_column_name
693 def right_column_name
690 acts_as_nested_set_options[:right_column]
694 acts_as_nested_set_options[:right_column]
691 end
695 end
692
696
693 def depth_column_name
697 def depth_column_name
694 acts_as_nested_set_options[:depth_column]
698 acts_as_nested_set_options[:depth_column]
695 end
699 end
696
700
697 def parent_column_name
701 def parent_column_name
698 acts_as_nested_set_options[:parent_column]
702 acts_as_nested_set_options[:parent_column]
699 end
703 end
700
704
701 def order_column
705 def order_column
702 acts_as_nested_set_options[:order_column] || left_column_name
706 acts_as_nested_set_options[:order_column] || left_column_name
703 end
707 end
704
708
705 def scope_column_names
709 def scope_column_names
706 Array(acts_as_nested_set_options[:scope])
710 Array(acts_as_nested_set_options[:scope])
707 end
711 end
708
712
709 def quoted_left_column_name
713 def quoted_left_column_name
710 connection.quote_column_name(left_column_name)
714 connection.quote_column_name(left_column_name)
711 end
715 end
712
716
713 def quoted_right_column_name
717 def quoted_right_column_name
714 connection.quote_column_name(right_column_name)
718 connection.quote_column_name(right_column_name)
715 end
719 end
716
720
717 def quoted_depth_column_name
721 def quoted_depth_column_name
718 connection.quote_column_name(depth_column_name)
722 connection.quote_column_name(depth_column_name)
719 end
723 end
720
724
721 def quoted_parent_column_name
725 def quoted_parent_column_name
722 connection.quote_column_name(parent_column_name)
726 connection.quote_column_name(parent_column_name)
723 end
727 end
724
728
725 def quoted_scope_column_names
729 def quoted_scope_column_names
726 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
730 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
727 end
731 end
728
732
729 def quoted_left_column_full_name
733 def quoted_left_column_full_name
730 "#{quoted_table_name}.#{quoted_left_column_name}"
734 "#{quoted_table_name}.#{quoted_left_column_name}"
731 end
735 end
732
736
733 def quoted_right_column_full_name
737 def quoted_right_column_full_name
734 "#{quoted_table_name}.#{quoted_right_column_name}"
738 "#{quoted_table_name}.#{quoted_right_column_name}"
735 end
739 end
736
740
737 def quoted_parent_column_full_name
741 def quoted_parent_column_full_name
738 "#{quoted_table_name}.#{quoted_parent_column_name}"
742 "#{quoted_table_name}.#{quoted_parent_column_name}"
739 end
743 end
740 end
744 end
741
745
742 end
746 end
743 end
747 end
744 end
748 end
General Comments 0
You need to be logged in to leave comments. Login now