better_nested_set 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,75 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{better_nested_set}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Chris Bailey", "Jean-Christophe Michel", "Dirk Breuer"]
12
+ s.date = %q{2011-03-11}
13
+ s.description = %q{This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the object-relational mapping layer of the framework Ruby on Rails. The original nested set in Rails lacks many important features, such as moving branches within a tree.}
14
+ s.email = %q{dirk.breuer@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "LICENSE.txt",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "app/helpers/better_nested_set_helper.rb",
27
+ "better_nested_set.gemspec",
28
+ "lib/better_nested_set.rb",
29
+ "lib/symetrie_com/acts_as_better_nested_set.rb",
30
+ "pkg/better_nested_set-0.1.0.gem",
31
+ "test/RUNNING_UNIT_TESTS",
32
+ "test/abstract_unit.rb",
33
+ "test/acts_as_nested_set_test.rb",
34
+ "test/database.yml",
35
+ "test/fixtures/mixin.rb",
36
+ "test/fixtures/mixins.yml",
37
+ "test/mysql.rb",
38
+ "test/postgresql.rb",
39
+ "test/schema.rb",
40
+ "test/sqlite3.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/railsbros-dirk/better_nested_set}
43
+ s.licenses = ["MIT"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.5.0}
46
+ s.summary = %q{This plugin provides an ehanced acts_as_nested_set mixin for ActiveRecord}
47
+ s.test_files = [
48
+ "test/abstract_unit.rb",
49
+ "test/acts_as_nested_set_test.rb",
50
+ "test/fixtures/mixin.rb",
51
+ "test/mysql.rb",
52
+ "test/postgresql.rb",
53
+ "test/schema.rb",
54
+ "test/sqlite3.rb"
55
+ ]
56
+
57
+ if s.respond_to? :specification_version then
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
61
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
62
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
63
+ s.add_development_dependency(%q<rcov>, [">= 0"])
64
+ else
65
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
66
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
67
+ s.add_dependency(%q<rcov>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
72
+ s.add_dependency(%q<rcov>, [">= 0"])
73
+ end
74
+ end
75
+
@@ -0,0 +1,16 @@
1
+ module SymetrieCom
2
+
3
+ if Rails.version >= "3.0.0"
4
+ class Engine < Rails::Engine
5
+ end
6
+ else
7
+ ActiveSupport::Dependencies.load_paths << File.join(File.dirname(__FILE__), '..', 'app', "helpers")
8
+ end
9
+
10
+ end
11
+
12
+ require "symetrie_com/acts_as_better_nested_set"
13
+
14
+ ActiveRecord::Base.class_eval do
15
+ include SymetrieCom::Acts::NestedSet
16
+ end
@@ -0,0 +1,1130 @@
1
+ module SymetrieCom
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+ # This module provides an enhanced acts_as_nested_set mixin for ActiveRecord.
9
+ # Please see the README for background information, examples, and tips on usage.
10
+ module ClassMethods
11
+ # Configuration options are:
12
+ # * +dependent+ - behaviour for cascading destroy operations (default: :delete_all)
13
+ # * +parent_column+ - Column name for the parent/child foreign key (default: +parent_id+).
14
+ # * +left_column+ - Column name for the left index (default: +lft+).
15
+ # * +right_column+ - Column name for the right index (default: +rgt+). NOTE:
16
+ # Don't use +left+ and +right+, since these are reserved database words.
17
+ # * +scope+ - Restricts what is to be considered a tree. Given a symbol, it'll attach "_id"
18
+ # (if it isn't there already) and use that as the foreign key restriction. It's also possible
19
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
20
+ # Example: <tt>acts_as_nested_set :scope => 'tree_id = #{tree_id} AND completed = 0'</tt>
21
+ # * +text_column+ - Column name for the title field (optional). Used as default in the
22
+ # {your-class}_options_for_select helper method. If empty, will use the first string field
23
+ # of your model class.
24
+ def acts_as_nested_set(options = {})
25
+
26
+ extend(SingletonMethods) unless respond_to?(:find_in_nestedset)
27
+
28
+ options[:scope] = "#{options[:scope]}_id".intern if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
29
+
30
+ write_inheritable_attribute(:acts_as_nested_set_options,
31
+ { :parent_column => (options[:parent_column] || 'parent_id'),
32
+ :left_column => (options[:left_column] || 'lft'),
33
+ :right_column => (options[:right_column] || 'rgt'),
34
+ :scope => (options[:scope] || '1 = 1'),
35
+ :text_column => (options[:text_column] || columns.collect{|c| (c.type == :string) ? c.name : nil }.compact.first),
36
+ :class => self, # for single-table inheritance
37
+ :dependent => (options[:dependent] || :delete_all) # accepts :delete_all and :destroy
38
+ } )
39
+
40
+ class_inheritable_reader :acts_as_nested_set_options
41
+
42
+ base_set_class.class_inheritable_accessor :acts_as_nested_set_scope_enabled
43
+ base_set_class.acts_as_nested_set_scope_enabled = true
44
+
45
+ if acts_as_nested_set_options[:scope].is_a?(Symbol)
46
+ scope_condition_method = %(
47
+ def scope_condition
48
+ if #{acts_as_nested_set_options[:scope].to_s}.nil?
49
+ self.class.use_scope_condition? ? "#{table_name}.#{acts_as_nested_set_options[:scope].to_s} IS NULL" : "(1 = 1)"
50
+ else
51
+ self.class.use_scope_condition? ? "#{table_name}.#{acts_as_nested_set_options[:scope].to_s} = \#{#{acts_as_nested_set_options[:scope].to_s}}" : "(1 = 1)"
52
+ end
53
+ end
54
+ )
55
+ else
56
+ scope_condition_method = "def scope_condition(); self.class.use_scope_condition? ? \"#{acts_as_nested_set_options[:scope]}\" : \"(1 = 1)\"; end"
57
+ end
58
+
59
+ # skip recursive destroy calls
60
+ attr_accessor :skip_before_destroy
61
+
62
+ # no bulk assignment
63
+ attr_protected acts_as_nested_set_options[:left_column].intern,
64
+ acts_as_nested_set_options[:right_column].intern,
65
+ acts_as_nested_set_options[:parent_column].intern
66
+ # no assignment to structure fields
67
+ class_eval <<-EOV
68
+ before_create :set_left_right
69
+ before_destroy :destroy_descendants
70
+ include SymetrieCom::Acts::NestedSet::InstanceMethods
71
+
72
+ def #{acts_as_nested_set_options[:left_column]}=(x)
73
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:left_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
74
+ end
75
+ def #{acts_as_nested_set_options[:right_column]}=(x)
76
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:right_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
77
+ end
78
+ def #{acts_as_nested_set_options[:parent_column]}=(x)
79
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:parent_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
80
+ end
81
+ #{scope_condition_method}
82
+ EOV
83
+ end
84
+
85
+ module SingletonMethods
86
+
87
+ # Most query methods are wrapped in with_scope to provide further filtering
88
+ # find_in_nested_set(what, outer_scope, inner_scope)
89
+ # inner scope is user supplied, while outer_scope is the normal query
90
+ # this way the user can override most scope attributes, except :conditions
91
+ # which is merged; use :reverse => true to sort result in reverse direction
92
+ def find_in_nested_set(*args)
93
+ what, outer_scope, inner_scope = case args.length
94
+ when 3 then [args[0], args[1], args[2]]
95
+ when 2 then [args[0], nil, args[1]]
96
+ when 1 then [args[0], nil, nil]
97
+ else [:all, nil, nil]
98
+ end
99
+ if inner_scope && outer_scope && inner_scope.delete(:reverse) && outer_scope[:order] == "#{prefixed_left_col_name}"
100
+ outer_scope[:order] = "#{prefixed_right_col_name} DESC"
101
+ end
102
+ acts_as_nested_set_options[:class].with_scope(:find => (outer_scope || {})) do
103
+ acts_as_nested_set_options[:class].find(what, inner_scope || {})
104
+ end
105
+ end
106
+
107
+ # Count wrapped in with_scope
108
+ def count_in_nested_set(*args)
109
+ outer_scope, inner_scope = case args.length
110
+ when 2 then [args[0], args[1]]
111
+ when 1 then [nil, args[0]]
112
+ else [nil, nil]
113
+ end
114
+ acts_as_nested_set_options[:class].with_scope(:find => (outer_scope || {})) do
115
+ acts_as_nested_set_options[:class].count(inner_scope || {})
116
+ end
117
+ end
118
+
119
+ # Loop through set using block
120
+ # pass :nested => false when result is not fully parent-child relational
121
+ # for example with filtered result sets
122
+ # Set options[:sort_on] to the name of a column you want to sort on (optional).
123
+ def recurse_result_set(result, options = {}, &block)
124
+ return result unless block_given?
125
+ inner_recursion = options.delete(:inner_recursion)
126
+ result_set = inner_recursion ? result : result.dup
127
+
128
+ parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
129
+ options[:level] ||= 0
130
+ options[:nested] = true unless options.key?(:nested)
131
+
132
+ siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
133
+ siblings.sort! {|a,b| a.send(options[:sort_on]) <=> b.send(options[:sort_on])} if options[:sort_on]
134
+ siblings.each do |sibling|
135
+ result_set.delete(sibling)
136
+ block.call(sibling, options[:level])
137
+ opts = { :parent_id => sibling.id, :level => options[:level] + 1, :inner_recursion => true, :sort_on => options[:sort_on]}
138
+ recurse_result_set(result_set, opts, &block) if options[:nested]
139
+ end
140
+ result_set.each { |orphan| block.call(orphan, options[:level]) } unless inner_recursion
141
+ end
142
+
143
+ # Loop and create a nested array of hashes (with children property)
144
+ # pass :nested => false when result is not fully parent-child relational
145
+ # for example with filtered result sets
146
+ def result_to_array(result, options = {}, &block)
147
+ array = []
148
+ inner_recursion = options.delete(:inner_recursion)
149
+ result_set = inner_recursion ? result : result.dup
150
+
151
+ parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
152
+ level = options[:level] || 0
153
+ options[:children] ||= 'children'
154
+ options[:methods] ||= []
155
+ options[:nested] = true unless options.key?(:nested)
156
+ options[:symbolize_keys] = true unless options.key?(:symbolize_keys)
157
+
158
+ if options[:only].blank? && options[:except].blank?
159
+ options[:except] = [:left_column, :right_column, :parent_column].inject([]) do |ex, opt|
160
+ column = acts_as_nested_set_options[opt].to_sym
161
+ ex << column unless ex.include?(column)
162
+ ex
163
+ end
164
+ end
165
+
166
+ siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
167
+ siblings.each do |sibling|
168
+ result_set.delete(sibling)
169
+ node = block_given? ? block.call(sibling, level) : sibling.attributes(:only => options[:only], :except => options[:except])
170
+ options[:methods].inject(node) { |enum, m| enum[m.to_s] = sibling.send(m) if sibling.respond_to?(m); enum }
171
+ if options[:nested]
172
+ opts = options.merge(:parent_id => sibling.id, :level => level + 1, :inner_recursion => true)
173
+ childnodes = result_to_array(result_set, opts, &block)
174
+ node[ options[:children] ] = childnodes if !childnodes.empty? && node.respond_to?(:[]=)
175
+ end
176
+ array << (options[:symbolize_keys] && node.respond_to?(:symbolize_keys) ? node.symbolize_keys : node)
177
+ end
178
+ unless inner_recursion
179
+ result_set.each do |orphan|
180
+ node = (block_given? ? block.call(orphan, level) : orphan.attributes(:only => options[:only], :except => options[:except]))
181
+ options[:methods].inject(node) { |enum, m| enum[m.to_s] = orphan.send(m) if orphan.respond_to?(m); enum }
182
+ array << (options[:symbolize_keys] && node.respond_to?(:symbolize_keys) ? node.symbolize_keys : node)
183
+ end
184
+ end
185
+ array
186
+ end
187
+
188
+ # Loop and create an xml structure. The following options are available
189
+ # :root sets the root tag, :children sets the siblings tag
190
+ # :record sets the node item tag, if given
191
+ # see also: result_to_array and ActiveRecord::XmlSerialization
192
+ def result_to_xml(result, options = {}, &block)
193
+ inner_recursion = options.delete(:inner_recursion)
194
+ result_set = inner_recursion ? result : result.dup
195
+
196
+ parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
197
+ options[:nested] = true unless options.key?(:nested)
198
+
199
+ options[:except] ||= []
200
+ [:left_column, :right_column, :parent_column].each do |opt|
201
+ column = acts_as_nested_set_options[opt].intern
202
+ options[:except] << column unless options[:except].include?(column)
203
+ end
204
+
205
+ options[:indent] ||= 2
206
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
207
+ options[:builder].instruct! unless options.delete(:skip_instruct)
208
+
209
+ record = options.delete(:record)
210
+ root = options.delete(:root) || :nodes
211
+ children = options.delete(:children) || :children
212
+
213
+ attrs = {}
214
+ attrs[:xmlns] = options[:namespace] if options[:namespace]
215
+
216
+ siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
217
+ options[:builder].tag!(root, attrs) do
218
+ siblings.each do |sibling|
219
+ result_set.delete(sibling) if options[:nested]
220
+ procs = options[:procs] ? options[:procs].dup : []
221
+ procs << Proc.new { |opts| block.call(opts, sibling) } if block_given?
222
+ if options[:nested]
223
+ proc = Proc.new do |opts|
224
+ proc_opts = opts.merge(:parent_id => sibling.id, :root => children, :record => record, :inner_recursion => true)
225
+ proc_opts[:procs] ||= options[:procs] if options[:procs]
226
+ proc_opts[:methods] ||= options[:methods] if options[:methods]
227
+ sibling.class.result_to_xml(result_set, proc_opts, &block)
228
+ end
229
+ procs << proc
230
+ end
231
+ opts = options.merge(:procs => procs, :skip_instruct => true, :root => record)
232
+ sibling.to_xml(opts)
233
+ end
234
+ end
235
+ options[:builder].target!
236
+ end
237
+
238
+ # Loop and create a nested xml representation of nodes with attributes
239
+ # pass :nested => false when result is not fully parent-child relational
240
+ # for example with filtered result sets
241
+ def result_to_attributes_xml(result, options = {}, &block)
242
+ inner_recursion = options.delete(:inner_recursion)
243
+ result_set = inner_recursion ? result : result.dup
244
+
245
+ parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
246
+ level = options[:level] || 0
247
+ options[:methods] ||= []
248
+ options[:nested] = true unless options.key?(:nested)
249
+ options[:dasherize] = true unless options.key?(:dasherize)
250
+
251
+ if options[:only].blank? && options[:except].blank?
252
+ options[:except] = [:left_column, :right_column, :parent_column].inject([]) do |ex, opt|
253
+ column = acts_as_nested_set_options[opt].to_sym
254
+ ex << column unless ex.include?(column)
255
+ ex
256
+ end
257
+ end
258
+
259
+ options[:indent] ||= 2
260
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
261
+ options[:builder].instruct! unless options.delete(:skip_instruct)
262
+
263
+ parent_attrs = {}
264
+ parent_attrs[:xmlns] = options[:namespace] if options[:namespace]
265
+
266
+ siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
267
+ siblings.each do |sibling|
268
+ result_set.delete(sibling)
269
+ node_tag = (options[:record] || sibling[sibling.class.inheritance_column] || 'node').underscore
270
+ node_tag = node_tag.dasherize unless options[:dasherize]
271
+ attrs = block_given? ? block.call(sibling, level) : sibling.attributes(:only => options[:only], :except => options[:except])
272
+ options[:methods].inject(attrs) { |enum, m| enum[m.to_s] = sibling.send(m) if sibling.respond_to?(m); enum }
273
+ if options[:nested] && sibling.children?
274
+ opts = options.merge(:parent_id => sibling.id, :level => level + 1, :inner_recursion => true, :skip_instruct => true)
275
+ options[:builder].tag!(node_tag, attrs) { result_to_attributes_xml(result_set, opts, &block) }
276
+ else
277
+ options[:builder].tag!(node_tag, attrs)
278
+ end
279
+ end
280
+ unless inner_recursion
281
+ result_set.each do |orphan|
282
+ node_tag = (options[:record] || orphan[orphan.class.inheritance_column] || 'node').underscore
283
+ node_tag = node_tag.dasherize unless options[:dasherize]
284
+ attrs = block_given? ? block.call(orphan, level) : orphan.attributes(:only => options[:only], :except => options[:except])
285
+ options[:methods].inject(attrs) { |enum, m| enum[m.to_s] = orphan.send(m) if orphan.respond_to?(m); enum }
286
+ options[:builder].tag!(node_tag, attrs)
287
+ end
288
+ end
289
+ options[:builder].target!
290
+ end
291
+
292
+ # Returns the single root for the class (or just the first root, if there are several).
293
+ # Deprecation note: the original acts_as_nested_set allowed roots to have parent_id = 0,
294
+ # so we currently do the same. This silliness will not be tolerated in future versions, however.
295
+ def root(scope = {})
296
+ find_in_nested_set(:first, { :conditions => "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)" }, scope)
297
+ end
298
+
299
+ # Returns the roots and/or virtual roots of all trees. See the explanation of virtual roots in the README.
300
+ def roots(scope = {})
301
+ find_in_nested_set(:all, { :conditions => "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope)
302
+ end
303
+
304
+ # Checks the left/right indexes of all records,
305
+ # returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
306
+ def check_all
307
+ total = 0
308
+ transaction do
309
+ # if there are virtual roots, only call check_full_tree on the first, because it will check other virtual roots in that tree.
310
+ total = roots.inject(0) {|sum, r| sum + (r[r.left_col_name] == 1 ? r.check_full_tree : 0 )}
311
+ raise ActiveRecord::ActiveRecordError, "Scope problems or nodes without a valid root" unless acts_as_nested_set_options[:class].count == total
312
+ end
313
+ return total
314
+ end
315
+
316
+ # Re-calculate the left/right values of all nodes. Can be used to convert ordinary trees into nested sets.
317
+ def renumber_all
318
+ scopes = []
319
+ # only call it once for each scope_condition (if the scope conditions are messed up, this will obviously cause problems)
320
+ roots.each do |r|
321
+ r.renumber_full_tree unless scopes.include?(r.scope_condition)
322
+ scopes << r.scope_condition
323
+ end
324
+ end
325
+
326
+ # Returns an SQL fragment that matches _items_ *and* all of their descendants, for use in a WHERE clause.
327
+ # You can pass it a single object, a single ID, or an array of objects and/or IDs.
328
+ # # if a.lft = 2, a.rgt = 7, b.lft = 12 and b.rgt = 13
329
+ # Set.sql_for([a,b]) # returns "((lft BETWEEN 2 AND 7) OR (lft BETWEEN 12 AND 13))"
330
+ # Returns "1 != 1" if passed no items. If you need to exclude items, just use "NOT (#{sql_for(items)})".
331
+ # Note that if you have multiple trees, it is up to you to apply your scope condition.
332
+ def sql_for(items)
333
+ items = [items] unless items.is_a?(Array)
334
+ # get objects for IDs
335
+ items.collect! {|s| s.is_a?(acts_as_nested_set_options[:class]) ? s : acts_as_nested_set_options[:class].find(s)}.uniq
336
+ items.reject! {|e| e.new_record?} # exclude unsaved items, since they don't have left/right values yet
337
+
338
+ return "1 != 1" if items.empty? # PostgreSQL didn't like '0', and SQLite3 didn't like 'FALSE'
339
+ items.map! {|e| "(#{prefixed_left_col_name} BETWEEN #{e[left_col_name]} AND #{e[right_col_name]})" }
340
+ "(#{items.join(' OR ')})"
341
+ end
342
+
343
+ # Wrap a method with this block to disable the default scope_condition
344
+ def without_scope_condition(&block)
345
+ if block_given?
346
+ disable_scope_condition
347
+ yield
348
+ enable_scope_condition
349
+ end
350
+ end
351
+
352
+ def use_scope_condition?#:nodoc:
353
+ base_set_class.acts_as_nested_set_scope_enabled == true
354
+ end
355
+
356
+ def disable_scope_condition#:nodoc:
357
+ base_set_class.acts_as_nested_set_scope_enabled = false
358
+ end
359
+
360
+ def enable_scope_condition#:nodoc:
361
+ base_set_class.acts_as_nested_set_scope_enabled = true
362
+ end
363
+
364
+ def left_col_name#:nodoc:
365
+ acts_as_nested_set_options[:left_column]
366
+ end
367
+ def prefixed_left_col_name#:nodoc:
368
+ "#{table_name}.#{left_col_name}"
369
+ end
370
+ def right_col_name#:nodoc:
371
+ acts_as_nested_set_options[:right_column]
372
+ end
373
+ def prefixed_right_col_name#:nodoc:
374
+ "#{table_name}.#{right_col_name}"
375
+ end
376
+ def parent_col_name#:nodoc:
377
+ acts_as_nested_set_options[:parent_column]
378
+ end
379
+ def prefixed_parent_col_name#:nodoc:
380
+ "#{table_name}.#{parent_col_name}"
381
+ end
382
+ def base_set_class#:nodoc:
383
+ acts_as_nested_set_options[:class] # for single-table inheritance
384
+ end
385
+
386
+ end
387
+
388
+ end
389
+
390
+ # This module provides instance methods for an enhanced acts_as_nested_set mixin. Please see the README for background information, examples, and tips on usage.
391
+ module InstanceMethods
392
+ # convenience methods to make the code more readable
393
+ def left_col_name#:nodoc:
394
+ self.class.left_col_name
395
+ end
396
+ def prefixed_left_col_name#:nodoc:
397
+ self.class.prefixed_left_col_name
398
+ end
399
+ def right_col_name#:nodoc:
400
+ self.class.right_col_name
401
+ end
402
+ def prefixed_right_col_name#:nodoc:
403
+ self.class.prefixed_right_col_name
404
+ end
405
+ def parent_col_name#:nodoc:
406
+ self.class.parent_col_name
407
+ end
408
+ def prefixed_parent_col_name#:nodoc:
409
+ self.class.prefixed_parent_col_name
410
+ end
411
+ alias parent_column parent_col_name#:nodoc: Deprecated
412
+ def base_set_class#:nodoc:
413
+ acts_as_nested_set_options[:class] # for single-table inheritance
414
+ end
415
+
416
+ # This takes care of valid queries when called on a root node
417
+ def sibling_condition
418
+ self[parent_col_name] ? "#{prefixed_parent_col_name} = #{self[parent_col_name]}" : "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)"
419
+ end
420
+
421
+ # On creation, automatically add the new node to the right of all existing nodes in this tree.
422
+ def set_left_right # already protected by a transaction within #create
423
+ maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0
424
+ self[left_col_name] = maxright+1
425
+ self[right_col_name] = maxright+2
426
+ end
427
+
428
+ # On destruction, delete all children and shift the lft/rgt values back to the left so the counts still work.
429
+ def destroy_descendants # already protected by a transaction within #destroy
430
+ return if self[right_col_name].nil? || self[left_col_name].nil? || self.skip_before_destroy
431
+ reloaded = self.reload rescue nil # in case a concurrent move has altered the indexes - rescue if non-existent
432
+ return unless reloaded
433
+ dif = self[right_col_name] - self[left_col_name] + 1
434
+ if acts_as_nested_set_options[:dependent] == :delete_all
435
+ base_set_class.delete_all( "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})" )
436
+ else
437
+ set = base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})", :order => "#{prefixed_right_col_name} DESC")
438
+ set.each { |child| child.skip_before_destroy = true; remove_descendant(child) }
439
+ end
440
+ base_set_class.update_all("#{left_col_name} = CASE \
441
+ WHEN #{left_col_name} > #{self[right_col_name]} THEN (#{left_col_name} - #{dif}) \
442
+ ELSE #{left_col_name} END, \
443
+ #{right_col_name} = CASE \
444
+ WHEN #{right_col_name} > #{self[right_col_name]} THEN (#{right_col_name} - #{dif} ) \
445
+ ELSE #{right_col_name} END",
446
+ scope_condition)
447
+ end
448
+
449
+ # By default, records are compared and sorted using the left column.
450
+ def <=>(x)
451
+ self[left_col_name] <=> x[left_col_name]
452
+ end
453
+
454
+ # Deprecated. Returns true if this is a root node.
455
+ def root?
456
+ parent_id = self[parent_col_name]
457
+ (parent_id == 0 || parent_id.nil?) && self[right_col_name] && self[left_col_name] && (self[right_col_name] > self[left_col_name])
458
+ end
459
+
460
+ # Deprecated. Returns true if this is a child node
461
+ def child?
462
+ parent_id = self[parent_col_name]
463
+ !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name])
464
+ end
465
+
466
+ # Deprecated. Returns true if we have no idea what this is
467
+ def unknown?
468
+ !root? && !child?
469
+ end
470
+
471
+ # Returns this record's root ancestor.
472
+ def root(scope = {})
473
+ # the BETWEEN clause is needed to ensure we get the right virtual root, if using those
474
+ self.class.find_in_nested_set(:first, { :conditions => "#{scope_condition} \
475
+ AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0) AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope)
476
+ end
477
+
478
+ # Returns the root or virtual roots of this record's tree (a tree cannot have more than one real root). See the explanation of virtual roots in the README.
479
+ def roots(scope = {})
480
+ self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope)
481
+ end
482
+
483
+ # Returns this record's parent.
484
+ def parent
485
+ self.class.find_in_nested_set(self[parent_col_name]) if self[parent_col_name]
486
+ end
487
+
488
+ # Returns an array of all parents, starting with the root.
489
+ def ancestors(scope = {})
490
+ self_and_ancestors(scope) - [self]
491
+ end
492
+
493
+ # Returns an array of all parents plus self, starting with the root.
494
+ def self_and_ancestors(scope = {})
495
+ self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})", :order => "#{prefixed_left_col_name}" }, scope)
496
+ end
497
+
498
+ # Returns all the children of this node's parent, except self.
499
+ def siblings(scope = {})
500
+ self_and_siblings(scope) - [self]
501
+ end
502
+
503
+ # Returns all siblings to the left of self, in descending order, so the first sibling is the one closest to the left of self
504
+ def previous_siblings(scope = {})
505
+ self.class.find_in_nested_set(:all,
506
+ { :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_right_col_name} < ?", self.id, self[left_col_name]], :order => "#{prefixed_left_col_name} DESC" }, scope)
507
+ end
508
+
509
+ # Returns all siblings to the right of self, in ascending order, so the first sibling is the one closest to the right of self
510
+ def next_siblings(scope = {})
511
+ self.class.find_in_nested_set(:all,
512
+ { :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_left_col_name} > ?", self.id, self[right_col_name]], :order => "#{prefixed_left_col_name} ASC"}, scope)
513
+ end
514
+
515
+ # Returns first siblings amongst it's siblings.
516
+ def first_sibling(scope = {})
517
+ self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} ASC")).first
518
+ end
519
+
520
+ def first_sibling?(scope = {})
521
+ self == first_sibling(scope)
522
+ end
523
+ alias :first? :first_sibling?
524
+
525
+ # Returns last siblings amongst it's siblings.
526
+ def last_sibling(scope = {})
527
+ self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} DESC")).first
528
+ end
529
+
530
+ def last_sibling?(scope = {})
531
+ self == last_sibling(scope)
532
+ end
533
+ alias :last? :last_sibling?
534
+
535
+ # Returns previous sibling of node or nil if there is none.
536
+ def previous_sibling(num = 1, scope = {})
537
+ scope[:limit] = num
538
+ siblings = previous_siblings(scope)
539
+ num == 1 ? siblings.first : siblings
540
+ end
541
+ alias :higher_item :previous_sibling
542
+
543
+ # Returns next sibling of node or nil if there is none.
544
+ def next_sibling(num = 1, scope = {})
545
+ scope[:limit] = num
546
+ siblings = next_siblings(scope)
547
+ num == 1 ? siblings.first : siblings
548
+ end
549
+ alias :lower_item :next_sibling
550
+
551
+ # Returns all the children of this node's parent, including self.
552
+ def self_and_siblings(scope = {})
553
+ if self[parent_col_name].nil? || self[parent_col_name].zero?
554
+ [self]
555
+ else
556
+ self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition}", :order => "#{prefixed_left_col_name}" }, scope)
557
+ end
558
+ end
559
+
560
+ # Returns the level of this object in the tree, root level being 0.
561
+ def level(scope = {})
562
+ return 0 if self[parent_col_name].nil?
563
+ self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope) - 1
564
+ end
565
+
566
+ # Returns the number of nested children of this object.
567
+ def all_children_count(scope = nil)
568
+ return all_children(scope).length if scope.is_a?(Hash)
569
+ return (self[right_col_name] - self[left_col_name] - 1)/2
570
+ end
571
+
572
+ # Returns itself and all nested children.
573
+ # Pass :exclude => item, or id, or [items or id] to exclude one or more items *and* all of their descendants.
574
+ def full_set(scope = {})
575
+ if exclude = scope.delete(:exclude)
576
+ exclude_str = " AND NOT (#{base_set_class.sql_for(exclude)}) "
577
+ elsif new_record? || self[right_col_name] - self[left_col_name] == 1
578
+ return [self]
579
+ end
580
+ self.class.find_in_nested_set(:all, {
581
+ :order => "#{prefixed_left_col_name}",
582
+ :conditions => "#{scope_condition} #{exclude_str} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})"
583
+ }, scope)
584
+ end
585
+
586
+ # Returns the child for the requested id within the scope of its children, otherwise nil
587
+ def child_by_id(id, scope = {})
588
+ children_by_id(id, scope).first
589
+ end
590
+
591
+ # Returns a child collection for the requested ids within the scope of its children, otherwise empty array
592
+ def children_by_id(*args)
593
+ scope = args.last.is_a?(Hash) ? args.pop : {}
594
+ ids = args.flatten.compact.uniq
595
+ self.class.find_in_nested_set(:all, {
596
+ :conditions => ["#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids]
597
+ }, scope)
598
+ end
599
+
600
+ # Returns the child for the requested id within the scope of its immediate children, otherwise nil
601
+ def direct_child_by_id(id, scope = {})
602
+ direct_children_by_id(id, scope).first
603
+ end
604
+
605
+ # Returns a child collection for the requested ids within the scope of its immediate children, otherwise empty array
606
+ def direct_children_by_id(*args)
607
+ scope = args.last.is_a?(Hash) ? args.pop : {}
608
+ ids = args.flatten.compact.uniq
609
+ self.class.find_in_nested_set(:all, {
610
+ :conditions => ["#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id} AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids]
611
+ }, scope)
612
+ end
613
+
614
+ # Tests wether self is within scope of parent
615
+ def child_of?(parent, scope = {})
616
+ if !scope.empty? && parent.respond_to?(:child_by_id)
617
+ parent.child_by_id(self.id, scope).is_a?(self.class)
618
+ else
619
+ parent.respond_to?(left_col_name) && self[left_col_name] > parent[left_col_name] && self[right_col_name] < parent[right_col_name]
620
+ end
621
+ end
622
+
623
+ # Tests wether self is within immediate scope of parent
624
+ def direct_child_of?(parent, scope = {})
625
+ if !scope.empty? && parent.respond_to?(:direct_child_by_id)
626
+ parent.direct_child_by_id(self.id, scope).is_a?(self.class)
627
+ else
628
+ parent.respond_to?(parent_col_name) && self[parent_col_name] == parent.id
629
+ end
630
+ end
631
+
632
+ # Returns all children and nested children.
633
+ # Pass :exclude => item, or id, or [items or id] to exclude one or more items *and* all of their descendants.
634
+ def all_children(scope = {})
635
+ full_set(scope) - [self]
636
+ end
637
+
638
+ def children_count(scope= {})
639
+ self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}" }, scope)
640
+ end
641
+
642
+ # Returns this record's immediate children.
643
+ def children(scope = {})
644
+ self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}", :order => "#{prefixed_left_col_name}" }, scope)
645
+ end
646
+
647
+ def children?(scope = {})
648
+ children_count(scope) > 0
649
+ end
650
+
651
+ # Deprecated
652
+ alias direct_children children
653
+
654
+ # Returns this record's terminal children (nodes without children).
655
+ def leaves(scope = {})
656
+ self.class.find_in_nested_set(:all,
657
+ { :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}", :order => "#{prefixed_left_col_name}" }, scope)
658
+ end
659
+
660
+ # Returns the count of this record's terminal children (nodes without children).
661
+ def leaves_count(scope = {})
662
+ self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}" }, scope)
663
+ end
664
+
665
+ # All nodes between two nodes, those nodes included
666
+ # in effect all ancestors until the other is reached
667
+ def ancestors_and_self_through(other, scope = {})
668
+ first, last = [self, other].sort
669
+ self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{last[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name}) AND #{prefixed_left_col_name} >= #{first[left_col_name]}",
670
+ :order => "#{prefixed_left_col_name}" }, scope)
671
+ end
672
+
673
+ # Ancestors until the other is reached - excluding self
674
+ def ancestors_through(other, scope = {})
675
+ ancestors_and_self_through(other, scope) - [self]
676
+ end
677
+
678
+ # All children until the other is reached - excluding self
679
+ def all_children_through(other, scope = {})
680
+ full_set_through(other, scope) - [self]
681
+ end
682
+
683
+ # All children until the other is reached - including self
684
+ def full_set_through(other, scope = {})
685
+ first, last = [self, other].sort
686
+ self.class.find_in_nested_set(:all,
687
+ { :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{first[right_col_name]}) AND #{prefixed_left_col_name} <= #{last[left_col_name]}", :order => "#{prefixed_left_col_name}" }, scope)
688
+ end
689
+
690
+ # All siblings until the other is reached - including self
691
+ def self_and_siblings_through(other, scope = {})
692
+ if self[parent_col_name].nil? || self[parent_col_name].zero?
693
+ [self]
694
+ else
695
+ first, last = [self, other].sort
696
+ self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{last[right_col_name]})", :order => "#{prefixed_left_col_name}" }, scope)
697
+ end
698
+ end
699
+
700
+ # All siblings until the other is reached - excluding self
701
+ def siblings_through(other, scope = {})
702
+ self_and_siblings_through(other, scope) - [self]
703
+ end
704
+
705
+ # Checks the left/right indexes of one node and all descendants.
706
+ # Throws ActiveRecord::ActiveRecordError if it finds a problem.
707
+ def check_subtree
708
+ transaction do
709
+ self.reload
710
+ check # this method is implemented via #check, so that we don't generate lots of unnecessary nested transactions
711
+ end
712
+ end
713
+
714
+ # Checks the left/right indexes of the entire tree that this node belongs to,
715
+ # returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
716
+ # This method is needed because check_subtree alone cannot find gaps between virtual roots, orphaned nodes or endless loops.
717
+ def check_full_tree
718
+ total_nodes = 0
719
+ transaction do
720
+ # virtual roots make this method more complex than it otherwise would be
721
+ n = 1
722
+ roots.each do |r|
723
+ raise ActiveRecord::ActiveRecordError, "Gaps between roots in the tree containing record ##{r.id}" if r[left_col_name] != n
724
+ r.check_subtree
725
+ n = r[right_col_name] + 1
726
+ end
727
+ total_nodes = roots.inject(0) {|sum, r| sum + r.all_children_count + 1 }
728
+ unless base_set_class.count(:conditions => "#{scope_condition}") == total_nodes
729
+ raise ActiveRecord::ActiveRecordError, "Orphaned nodes or endless loops in the tree containing record ##{self.id}"
730
+ end
731
+ end
732
+ return total_nodes
733
+ end
734
+
735
+ # Re-calculate the left/right values of all nodes in this record's tree. Can be used to convert an ordinary tree into a nested set.
736
+ def renumber_full_tree
737
+ indexes = []
738
+ n = 1
739
+ transaction do
740
+ for r in roots # because we may have virtual roots
741
+ n = 1 + r.calc_numbers(n, indexes)
742
+ end
743
+ for i in indexes
744
+ base_set_class.update_all("#{left_col_name} = #{i[:lft]}, #{right_col_name} = #{i[:rgt]}", "#{self.class.primary_key} = #{i[:id]}")
745
+ end
746
+ end
747
+ ## reload?
748
+ end
749
+
750
+ # Deprecated. Adds a child to this object in the tree. If this object hasn't been initialized,
751
+ # it gets set up as a root node.
752
+ #
753
+ # This method exists only for compatibility and will be removed in future versions.
754
+ def add_child(child)
755
+ transaction do
756
+ self.reload; child.reload # for compatibility with old version
757
+ # the old version allows records with nil values for lft and rgt
758
+ unless self[left_col_name] && self[right_col_name]
759
+ if child[left_col_name] || child[right_col_name]
760
+ raise ActiveRecord::ActiveRecordError, "If parent lft or rgt are nil, you can't add a child with non-nil lft or rgt"
761
+ end
762
+ base_set_class.update_all("#{left_col_name} = CASE \
763
+ WHEN id = #{self.id} \
764
+ THEN 1 \
765
+ WHEN id = #{child.id} \
766
+ THEN 3 \
767
+ ELSE #{left_col_name} END, \
768
+ #{right_col_name} = CASE \
769
+ WHEN id = #{self.id} \
770
+ THEN 2 \
771
+ WHEN id = #{child.id} \
772
+ THEN 4 \
773
+ ELSE #{right_col_name} END",
774
+ scope_condition)
775
+ self.reload; child.reload
776
+ end
777
+ unless child[left_col_name] && child[right_col_name]
778
+ maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0
779
+ base_set_class.update_all("#{left_col_name} = CASE \
780
+ WHEN id = #{child.id} \
781
+ THEN #{maxright + 1} \
782
+ ELSE #{left_col_name} END, \
783
+ #{right_col_name} = CASE \
784
+ WHEN id = #{child.id} \
785
+ THEN #{maxright + 2} \
786
+ ELSE #{right_col_name} END",
787
+ scope_condition)
788
+ child.reload
789
+ end
790
+
791
+ child.move_to_child_of(self)
792
+ # self.reload ## even though move_to calls target.reload, at least one object in the tests was not reloading (near the end of test_common_usage)
793
+ end
794
+ # self.reload
795
+ # child.reload
796
+ #
797
+ # if child.root?
798
+ # raise ActiveRecord::ActiveRecordError, "Adding sub-tree isn\'t currently supported"
799
+ # else
800
+ # if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) )
801
+ # # Looks like we're now the root node! Woo
802
+ # self[left_col_name] = 1
803
+ # self[right_col_name] = 4
804
+ #
805
+ # # What do to do about validation?
806
+ # return nil unless self.save
807
+ #
808
+ # child[parent_col_name] = self.id
809
+ # child[left_col_name] = 2
810
+ # child[right_col_name]= 3
811
+ # return child.save
812
+ # else
813
+ # # OK, we need to add and shift everything else to the right
814
+ # child[parent_col_name] = self.id
815
+ # right_bound = self[right_col_name]
816
+ # child[left_col_name] = right_bound
817
+ # child[right_col_name] = right_bound + 1
818
+ # self[right_col_name] += 2
819
+ # self.class.transaction {
820
+ # self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
821
+ # self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
822
+ # self.save
823
+ # child.save
824
+ # }
825
+ # end
826
+ # end
827
+ end
828
+
829
+ # Insert a node at a specific position among the children of target.
830
+ def insert_at(target, index = :last, scope = {})
831
+ level_nodes = target.children(scope)
832
+ current_index = level_nodes.index(self)
833
+ last_index = level_nodes.length - 1
834
+ as_first = (index == :first)
835
+ as_last = (index == :last || (index.is_a?(Fixnum) && index > last_index))
836
+ index = 0 if as_first
837
+ index = last_index if as_last
838
+ if last_index < 0
839
+ move_to_child_of(target)
840
+ elsif index >= 0 && index <= last_index && level_nodes[index]
841
+ if as_last && index != current_index
842
+ move_to_right_of(level_nodes[index])
843
+ elsif (as_first || index == 0) && index != current_index
844
+ move_to_left_of(level_nodes[index])
845
+ elsif !current_index.nil? && index > current_index
846
+ move_to_right_of(level_nodes[index])
847
+ elsif !current_index.nil? && index < current_index
848
+ move_to_left_of(level_nodes[index])
849
+ elsif current_index.nil?
850
+ move_to_left_of(level_nodes[index])
851
+ end
852
+ end
853
+ end
854
+
855
+ # Move this node to the left of _target_ (you can pass an object or just an id).
856
+ # Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
857
+ def move_to_left_of(target)
858
+ self.move_to target, :left
859
+ end
860
+
861
+ # Move this node to the right of _target_ (you can pass an object or just an id).
862
+ # Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
863
+ def move_to_right_of(target)
864
+ self.move_to target, :right
865
+ end
866
+
867
+ # Make this node a child of _target_ (you can pass an object or just an id).
868
+ # Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
869
+ def move_to_child_of(target)
870
+ self.move_to target, :child
871
+ end
872
+
873
+ # Moves a node to a certain position amongst its siblings.
874
+ def move_to_position(index, scope = {})
875
+ insert_at(self.parent, index, scope)
876
+ end
877
+
878
+ # Moves a node one up amongst its siblings. Does nothing if it's already
879
+ # the first sibling.
880
+ def move_lower
881
+ next_sib = next_sibling
882
+ move_to_right_of(next_sib) if next_sib
883
+ end
884
+
885
+ # Moves a node one down amongst its siblings. Does nothing if it's already
886
+ # the last sibling.
887
+ def move_higher
888
+ prev_sib = previous_sibling
889
+ move_to_left_of(prev_sib) if prev_sib
890
+ end
891
+
892
+ # Moves a node one to be the first amongst its siblings. Does nothing if it's already
893
+ # the first sibling.
894
+ def move_to_top
895
+ first_sib = first_sibling
896
+ move_to_left_of(first_sib) if first_sib && self != first_sib
897
+ end
898
+
899
+ # Moves a node one to be the last amongst its siblings. Does nothing if it's already
900
+ # the last sibling.
901
+ def move_to_bottom
902
+ last_sib = last_sibling
903
+ move_to_right_of(last_sib) if last_sib && self != last_sib
904
+ end
905
+
906
+ # Swaps the position of two sibling nodes preserving a sibling's descendants.
907
+ # The current implementation only works amongst siblings.
908
+ def swap(target, transact = true)
909
+ move_to(target, :swap, transact)
910
+ end
911
+
912
+ # Reorder children according to an array of ids
913
+ def reorder_children(*ids)
914
+ transaction do
915
+ ordered_ids = ids.flatten.uniq
916
+ current_children = children({ :conditions => { :id => ordered_ids } })
917
+ current_children_ids = current_children.map(&:id)
918
+ ordered_ids = ordered_ids & current_children_ids
919
+ return [] unless ordered_ids.length > 1 && ordered_ids != current_children_ids
920
+ perform_reorder_of_children(ordered_ids, current_children)
921
+ end
922
+ end
923
+
924
+ protected
925
+ def move_to(target, position, transact = true) #:nodoc:
926
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if new_record?
927
+ raise ActiveRecord::ActiveRecordError, "You cannot move a node if left or right is nil" unless self[left_col_name] && self[right_col_name]
928
+
929
+ with_optional_transaction(transact) do
930
+ self.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}") # the lft/rgt values could be stale (target is reloaded below)
931
+ if target.is_a?(base_set_class)
932
+ target.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}") # could be stale
933
+ else
934
+ target = self.class.find_in_nested_set(target) # load object if we were given an ID
935
+ end
936
+
937
+ if (target[left_col_name] >= self[left_col_name]) && (target[right_col_name] <= self[right_col_name])
938
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
939
+ end
940
+
941
+ # prevent moves between different trees
942
+ if target.scope_condition != scope_condition
943
+ raise ActiveRecord::ActiveRecordError, "Scope conditions do not match. Is the target in the same tree?"
944
+ end
945
+
946
+ if position == :swap
947
+ unless self.siblings.include?(target)
948
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node should be a sibling."
949
+ end
950
+
951
+ direction = (self[left_col_name] < target[left_col_name]) ? :down : :up
952
+
953
+ i0 = (direction == :up) ? target[left_col_name] : self[left_col_name]
954
+ i1 = (direction == :up) ? target[right_col_name] : self[right_col_name]
955
+ i2 = (direction == :up) ? self[left_col_name] : target[left_col_name]
956
+ i3 = (direction == :up) ? self[right_col_name] : target[right_col_name]
957
+
958
+ base_set_class.update_all(%[
959
+ #{left_col_name} = CASE WHEN #{left_col_name} BETWEEN #{i0} AND #{i1} THEN #{i3} + #{left_col_name} - #{i1}
960
+ WHEN #{left_col_name} BETWEEN #{i2} AND #{i3} THEN #{i0} + #{left_col_name} - #{i2}
961
+ ELSE #{i0} + #{i3} + #{left_col_name} - #{i1} - #{i2} END,
962
+ #{right_col_name} = CASE WHEN #{right_col_name} BETWEEN #{i0} AND #{i1} THEN #{i3} + #{right_col_name} - #{i1}
963
+ WHEN #{right_col_name} BETWEEN #{i2} AND #{i3} THEN #{i0} + #{right_col_name} - #{i2}
964
+ ELSE #{i0} + #{i3} + #{right_col_name} - #{i1} - #{i2} END ], "#{left_col_name} BETWEEN #{i0} AND #{i3} AND #{i0} < #{i1} AND #{i1} < #{i2} AND #{i2} < #{i3} AND #{scope_condition}")
965
+ else
966
+ # the move: we just need to define two adjoining segments of the left/right index and swap their positions
967
+ bound = case position
968
+ when :child then target[right_col_name]
969
+ when :left then target[left_col_name]
970
+ when :right then target[right_col_name] + 1
971
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left or :right ('#{position}' received)."
972
+ end
973
+
974
+ if bound > self[right_col_name]
975
+ bound = bound - 1
976
+ other_bound = self[right_col_name] + 1
977
+ else
978
+ other_bound = self[left_col_name] - 1
979
+ end
980
+
981
+ return if bound == self[right_col_name] || bound == self[left_col_name] # there would be no change, and other_bound is now wrong anyway
982
+
983
+ # we have defined the boundaries of two non-overlapping intervals,
984
+ # so sorting puts both the intervals and their boundaries in order
985
+ a, b, c, d = [self[left_col_name], self[right_col_name], bound, other_bound].sort
986
+
987
+ # change nil to NULL for new parent
988
+ if position == :child
989
+ new_parent = target.id
990
+ else
991
+ new_parent = target[parent_col_name].nil? ? 'NULL' : target[parent_col_name]
992
+ end
993
+
994
+ base_set_class.update_all("\
995
+ #{left_col_name} = CASE \
996
+ WHEN #{left_col_name} BETWEEN #{a} AND #{b} THEN #{left_col_name} + #{d - b} \
997
+ WHEN #{left_col_name} BETWEEN #{c} AND #{d} THEN #{left_col_name} + #{a - c} \
998
+ ELSE #{left_col_name} END, \
999
+ #{right_col_name} = CASE \
1000
+ WHEN #{right_col_name} BETWEEN #{a} AND #{b} THEN #{right_col_name} + #{d - b} \
1001
+ WHEN #{right_col_name} BETWEEN #{c} AND #{d} THEN #{right_col_name} + #{a - c} \
1002
+ ELSE #{right_col_name} END, \
1003
+ #{parent_col_name} = CASE \
1004
+ WHEN #{self.class.primary_key} = #{self.id} THEN #{new_parent} \
1005
+ ELSE #{parent_col_name} END",
1006
+ scope_condition)
1007
+ end
1008
+ self.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}")
1009
+ target.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}")
1010
+ end
1011
+ end
1012
+
1013
+ def check #:nodoc:
1014
+ # performance improvements (3X or more for tables with lots of columns) by using :select to load just id, lft and rgt
1015
+ ## i don't use the scope condition here, because it shouldn't be needed
1016
+ my_children = self.class.find_in_nested_set(:all, :conditions => "#{prefixed_parent_col_name} = #{self.id}",
1017
+ :order => "#{prefixed_left_col_name}", :select => "#{self.class.primary_key}, #{prefixed_left_col_name}, #{prefixed_right_col_name}")
1018
+
1019
+ if my_children.empty?
1020
+ unless self[left_col_name] && self[right_col_name]
1021
+ raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} or #{left_col_name} is blank"
1022
+ end
1023
+ unless self[right_col_name] - self[left_col_name] == 1
1024
+ raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} should be 1 greater than #{left_col_name}"
1025
+ end
1026
+ else
1027
+ n = self[left_col_name]
1028
+ for c in (my_children) # the children come back ordered by lft
1029
+ unless c[left_col_name] && c[right_col_name]
1030
+ raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{c.id}.#{right_col_name} or #{left_col_name} is blank"
1031
+ end
1032
+ unless c[left_col_name] == n + 1
1033
+ raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{c.id}.#{left_col_name} should be 1 greater than #{n}"
1034
+ end
1035
+ c.check
1036
+ n = c[right_col_name]
1037
+ end
1038
+ unless self[right_col_name] == n + 1
1039
+ raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} should be 1 greater than #{n}"
1040
+ end
1041
+ end
1042
+ end
1043
+
1044
+ # used by the renumbering methods
1045
+ def calc_numbers(n, indexes) #:nodoc:
1046
+ my_lft = n
1047
+ # performance improvements (3X or more for tables with lots of columns) by using :select to load just id, lft and rgt
1048
+ ## i don't use the scope condition here, because it shouldn't be needed
1049
+ my_children = self.class.find_in_nested_set(:all, :conditions => "#{prefixed_parent_col_name} = #{self.id}",
1050
+ :order => "#{prefixed_left_col_name}", :select => "#{self.class.primary_key}, #{prefixed_left_col_name}, #{prefixed_right_col_name}")
1051
+ if my_children.empty?
1052
+ my_rgt = (n += 1)
1053
+ else
1054
+ for c in (my_children)
1055
+ n = c.calc_numbers(n + 1, indexes)
1056
+ end
1057
+ my_rgt = (n += 1)
1058
+ end
1059
+ indexes << {:id => self.id, :lft => my_lft, :rgt => my_rgt} unless self[left_col_name] == my_lft && self[right_col_name] == my_rgt
1060
+ return n
1061
+ end
1062
+
1063
+ # Actually perform the ordering using calculated steps
1064
+ def perform_reorder_of_children(ordered_ids, current)
1065
+ steps = calculate_reorder_steps(ordered_ids, current)
1066
+ steps.inject([]) do |result, (source, idx)|
1067
+ target = current[idx]
1068
+ if source.id != target.id
1069
+ source.swap(target, false)
1070
+ from = current.index(source)
1071
+ current[from], current[idx] = current[idx], current[from]
1072
+ result << source
1073
+ end
1074
+ result
1075
+ end
1076
+ end
1077
+
1078
+ # Calculate the least amount of swap steps to achieve the requested order
1079
+ def calculate_reorder_steps(ordered_ids, current)
1080
+ steps = []
1081
+ current.each_with_index do |source, idx|
1082
+ new_idx = ordered_ids.index(source.id)
1083
+ steps << [source, new_idx] if idx != new_idx
1084
+ end
1085
+ steps
1086
+ end
1087
+
1088
+ # The following code is my crude method of making things concurrency-safe.
1089
+ # Basically, we need to ensure that whenever a record is saved, the lft/rgt
1090
+ # values are _not_ written to the database, because if any changes to the tree
1091
+ # structure occurrred since the object was loaded, the lft/rgt values could
1092
+ # be out of date and corrupt the indexes.
1093
+ # There is an open ticket for this in the Rails Core: http://dev.rubyonrails.org/ticket/6896
1094
+
1095
+ private
1096
+ # override the sql preparation method to exclude the lft/rgt columns
1097
+ # under the same conditions that the primary key column is excluded
1098
+ def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) #:nodoc:
1099
+ left_and_right_column = [acts_as_nested_set_options[:left_column], acts_as_nested_set_options[:right_column]]
1100
+ quoted = {}
1101
+ connection = self.class.connection
1102
+ attribute_names.each do |name|
1103
+ if column = column_for_attribute(name)
1104
+ quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && (column.primary || left_and_right_column.include?(column.name))
1105
+ end
1106
+ end
1107
+ include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
1108
+ end
1109
+
1110
+ # i couldn't figure out how to call attributes_with_quotes without cutting and pasting this private method in. :(
1111
+ # Quote strings appropriately for SQL statements.
1112
+ def quote_value(value, column = nil) #:nodoc:
1113
+ self.class.connection.quote(value, column)
1114
+ end
1115
+
1116
+ # optionally use a transaction
1117
+ def with_optional_transaction(bool, &block)
1118
+ bool ? transaction { yield } : yield
1119
+ end
1120
+
1121
+ # as a seperate method to facilitate custom implementations based on :dependent option
1122
+ def remove_descendant(descendant)
1123
+ descendant.destroy
1124
+ end
1125
+
1126
+ end
1127
+ end
1128
+ end
1129
+ end
1130
+