chrislloyd-awesome_nested_set 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ = AwesomeNestedSet
2
+
3
+ Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer.
4
+
5
+ == What makes this so awesome?
6
+
7
+ This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
8
+
9
+ == Installation
10
+
11
+ If you are on Rails 2.1 or later:
12
+
13
+ script/plugin install git://github.com/collectiveidea/awesome_nested_set.git
14
+
15
+ == Usage
16
+
17
+ To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
18
+
19
+ class CreateCategories < ActiveRecord::Migration
20
+ def self.up
21
+ create_table :categories do |t|
22
+ t.string :name
23
+ t.integer :parent_id
24
+ t.integer :lft
25
+ t.integer :rgt
26
+ end
27
+ end
28
+
29
+ def self.down
30
+ drop_table :categories
31
+ end
32
+ end
33
+
34
+ Enable the nested set functionality by declaring acts_as_nested_set on your model
35
+
36
+ class Category < ActiveRecord::Base
37
+ acts_as_nested_set
38
+ end
39
+
40
+ Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::SingletonMethods for more info.
41
+
42
+ == View Helper
43
+
44
+ The view helper is called #nested_set_options.
45
+
46
+ Example usage:
47
+
48
+ <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
49
+
50
+ <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
51
+
52
+ See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
53
+
54
+ == References
55
+
56
+ You can learn more about nested sets at:
57
+
58
+ http://www.dbmsmag.com/9603d06.html
59
+ http://threebit.net/tutorials/nestedset/tutorial1.html
60
+ http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html
61
+ http://opensource.symetrie.com/trac/better_nested_set/
62
+
63
+
64
+ Copyright (c) 2008 Collective Idea, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'rcov/rcovtask'
6
+ require "load_multi_rails_rake_tasks"
7
+
8
+ spec = eval(File.read("#{File.dirname(__FILE__)}/awesome_nested_set.gemspec"))
9
+ PKG_NAME = spec.name
10
+ PKG_VERSION = spec.version
11
+
12
+ Rake::GemPackageTask.new(spec) do |pkg|
13
+ pkg.need_zip = true
14
+ pkg.need_tar = true
15
+ end
16
+
17
+
18
+ desc 'Default: run unit tests.'
19
+ task :default => :test
20
+
21
+ desc 'Test the awesome_nested_set plugin.'
22
+ Rake::TestTask.new(:test) do |t|
23
+ t.libs << 'lib'
24
+ t.pattern = 'test/**/*_test.rb'
25
+ t.verbose = true
26
+ end
27
+
28
+ desc 'Generate documentation for the awesome_nested_set plugin.'
29
+ Rake::RDocTask.new(:rdoc) do |rdoc|
30
+ rdoc.rdoc_dir = 'rdoc'
31
+ rdoc.title = 'AwesomeNestedSet'
32
+ rdoc.options << '--line-numbers' << '--inline-source'
33
+ rdoc.rdoc_files.include('README.rdoc')
34
+ rdoc.rdoc_files.include('lib/**/*.rb')
35
+ end
36
+
37
+ namespace :test do
38
+ desc "just rcov minus html output"
39
+ Rcov::RcovTask.new(:coverage) do |t|
40
+ # t.libs << 'test'
41
+ t.test_files = FileList['test/**/*_test.rb']
42
+ t.output_dir = 'coverage'
43
+ t.verbose = true
44
+ t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage)
45
+ end
46
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,549 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ def self.included(base)
5
+ base.extend(SingletonMethods)
6
+ end
7
+
8
+ # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9
+ # an _ordered_ tree, with the added feature that you can select the children and all of their
10
+ # descendants with a single query. The drawback is that insertion or move need some complex
11
+ # sql queries. But everything is done here by this module!
12
+ #
13
+ # Nested sets are appropriate each time you want either an orderd tree (menus,
14
+ # commercial categories) or an efficient way of querying big trees (threaded posts).
15
+ #
16
+ # == API
17
+ #
18
+ # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19
+ # by another easier, except for the creation:
20
+ #
21
+ # in acts_as_tree:
22
+ # item.children.create(:name => "child1")
23
+ #
24
+ # in acts_as_nested_set:
25
+ # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26
+ # child = MyClass.new(:name => "child1")
27
+ # child.save
28
+ # # now move the item to its right place
29
+ # child.move_to_child_of my_item
30
+ #
31
+ # You can pass an id or an object to:
32
+ # * <tt>#move_to_child_of</tt>
33
+ # * <tt>#move_to_right_of</tt>
34
+ # * <tt>#move_to_left_of</tt>
35
+ #
36
+ module SingletonMethods
37
+ # Configuration options are:
38
+ #
39
+ # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40
+ # * +:left_column+ - column name for left boundry data, default "lft"
41
+ # * +:right_column+ - column name for right boundry data, default "rgt"
42
+ # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43
+ # (if it hasn't been already) and use that as the foreign key restriction. You
44
+ # can also pass an array to scope by multiple attributes.
45
+ # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46
+ # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47
+ # child objects are destroyed alongside this object by calling their destroy
48
+ # method. If set to :delete_all (default), all the child objects are deleted
49
+ # without calling their destroy method.
50
+ #
51
+ # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52
+ # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53
+ # to acts_as_nested_set models
54
+ def acts_as_nested_set(options = {})
55
+ options = {
56
+ :parent_column => 'parent_id',
57
+ :left_column => 'lft',
58
+ :right_column => 'rgt',
59
+ :dependent => :delete_all, # or :destroy
60
+ }.merge(options)
61
+
62
+ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
63
+ options[:scope] = "#{options[:scope]}_id".intern
64
+ end
65
+
66
+ write_inheritable_attribute :acts_as_nested_set_options, options
67
+ class_inheritable_reader :acts_as_nested_set_options
68
+
69
+ include Comparable
70
+ include Columns
71
+ include InstanceMethods
72
+ extend Columns
73
+ extend ClassMethods
74
+
75
+ # no bulk assignment
76
+ attr_protected left_column_name.intern,
77
+ right_column_name.intern,
78
+ parent_column_name.intern
79
+
80
+ before_create :set_default_left_and_right
81
+ before_destroy :prune_from_tree
82
+
83
+ # no assignment to structure fields
84
+ [left_column_name, right_column_name, parent_column_name].each do |column|
85
+ module_eval <<-"end_eval", __FILE__, __LINE__
86
+ def #{column}=(x)
87
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
88
+ end
89
+ end_eval
90
+ end
91
+
92
+ named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
93
+ named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
94
+ if self.respond_to?(:define_callbacks)
95
+ define_callbacks("before_move", "after_move")
96
+ end
97
+
98
+
99
+ end
100
+
101
+ end
102
+
103
+ module ClassMethods
104
+
105
+ # Returns the first root
106
+ def root
107
+ roots.find(:first)
108
+ end
109
+
110
+ def valid?
111
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112
+ end
113
+
114
+ def left_and_rights_valid?
115
+ count(
116
+ :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117
+ "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
118
+ :conditions =>
119
+ "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
120
+ "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
121
+ "#{quoted_table_name}.#{quoted_left_column_name} >= " +
122
+ "#{quoted_table_name}.#{quoted_right_column_name} OR " +
123
+ "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
124
+ "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
125
+ "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
126
+ ) == 0
127
+ end
128
+
129
+ def no_duplicates_for_columns?
130
+ scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
131
+ connection.quote_column_name(c)
132
+ end.push(nil).join(", ")
133
+ [quoted_left_column_name, quoted_right_column_name].all? do |column|
134
+ # No duplicates
135
+ find(:first,
136
+ :select => "#{scope_string}#{column}, COUNT(#{column})",
137
+ :group => "#{scope_string}#{column}
138
+ HAVING COUNT(#{column}) > 1").nil?
139
+ end
140
+ end
141
+
142
+ # Wrapper for each_root_valid? that can deal with scope.
143
+ def all_roots_valid?
144
+ if acts_as_nested_set_options[:scope]
145
+ roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
146
+ each_root_valid?(grouped_roots)
147
+ end
148
+ else
149
+ each_root_valid?(roots)
150
+ end
151
+ end
152
+
153
+ def each_root_valid?(roots_to_validate)
154
+ left = right = 0
155
+ roots_to_validate.all? do |root|
156
+ returning(root.left > left && root.right > right) do
157
+ left = root.left
158
+ right = root.right
159
+ end
160
+ end
161
+ end
162
+
163
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
164
+ def rebuild!
165
+ # Don't rebuild a valid tree.
166
+ return true if valid?
167
+
168
+ scope = lambda{}
169
+ if acts_as_nested_set_options[:scope]
170
+ scope = lambda{|node|
171
+ scope_column_names.inject(""){|str, column_name|
172
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173
+ }
174
+ }
175
+ end
176
+ indices = {}
177
+
178
+ set_left_and_rights = lambda do |node|
179
+ # set left
180
+ node[left_column_name] = indices[scope.call(node)] += 1
181
+ # find
182
+ find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, id").each{|n| set_left_and_rights.call(n) }
183
+ # set right
184
+ node[right_column_name] = indices[scope.call(node)] += 1
185
+ node.save!
186
+ end
187
+
188
+ # Find root node(s)
189
+ root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, id").each do |root_node|
190
+ # setup index for this scope
191
+ indices[scope.call(root_node)] ||= 0
192
+ set_left_and_rights.call(root_node)
193
+ end
194
+ end
195
+ end
196
+
197
+ # Mixed into both classes and instances to provide easy access to the column names
198
+ module Columns
199
+ def left_column_name
200
+ acts_as_nested_set_options[:left_column]
201
+ end
202
+
203
+ def right_column_name
204
+ acts_as_nested_set_options[:right_column]
205
+ end
206
+
207
+ def parent_column_name
208
+ acts_as_nested_set_options[:parent_column]
209
+ end
210
+
211
+ def scope_column_names
212
+ Array(acts_as_nested_set_options[:scope])
213
+ end
214
+
215
+ def quoted_left_column_name
216
+ connection.quote_column_name(left_column_name)
217
+ end
218
+
219
+ def quoted_right_column_name
220
+ connection.quote_column_name(right_column_name)
221
+ end
222
+
223
+ def quoted_parent_column_name
224
+ connection.quote_column_name(parent_column_name)
225
+ end
226
+
227
+ def quoted_scope_column_names
228
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
229
+ end
230
+ end
231
+
232
+ # 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.
233
+ #
234
+ # category.self_and_descendants.count
235
+ # category.ancestors.find(:all, :conditions => "name like '%foo%'")
236
+ module InstanceMethods
237
+ # Value of the parent column
238
+ def parent_id
239
+ self[parent_column_name]
240
+ end
241
+
242
+ # Value of the left column
243
+ def left
244
+ self[left_column_name]
245
+ end
246
+
247
+ # Value of the right column
248
+ def right
249
+ self[right_column_name]
250
+ end
251
+
252
+ # Returns true if this is a root node.
253
+ def root?
254
+ parent_id.nil?
255
+ end
256
+
257
+ def leaf?
258
+ right - left == 1
259
+ end
260
+
261
+ # Returns true is this is a child node
262
+ def child?
263
+ !parent_id.nil?
264
+ end
265
+
266
+ # order by left column
267
+ def <=>(x)
268
+ left <=> x.left
269
+ end
270
+
271
+ # Redefine to act like active record
272
+ def ==(comparison_object)
273
+ comparison_object.equal?(self) ||
274
+ (comparison_object.instance_of?(self.class) &&
275
+ comparison_object.id == id &&
276
+ !comparison_object.new_record?)
277
+ end
278
+
279
+ # Returns root
280
+ def root
281
+ self_and_ancestors.find(:first)
282
+ end
283
+ alias_method :patriarch, :root
284
+
285
+ # Returns the immediate parent
286
+ def parent
287
+ nested_set_scope.find_by_id(parent_id) if parent_id
288
+ end
289
+
290
+ # Returns the array of all parents and self
291
+ def self_and_ancestors
292
+ nested_set_scope.scoped :conditions => [
293
+ "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
294
+ ]
295
+ end
296
+ alias_method :lineage, :self_and_ancestors
297
+
298
+ # Returns an array of all parents
299
+ def ancestors
300
+ without_self self_and_ancestors
301
+ end
302
+
303
+ # Returns the array of all children of the parent, including self
304
+ def self_and_siblings
305
+ nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
306
+ end
307
+
308
+ # Returns the array of all children of the parent, except self
309
+ def siblings
310
+ without_self self_and_siblings
311
+ end
312
+
313
+ # Returns a set of all of its nested children which do not have children
314
+ def leaves
315
+ descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
316
+ end
317
+
318
+ # Returns the level of this object in the tree
319
+ # root level is 0
320
+ def level
321
+ parent_id.nil? ? 0 : ancestors.count
322
+ end
323
+
324
+ # Returns a set of itself and all of its nested children
325
+ def self_and_descendants
326
+ nested_set_scope.scoped :conditions => [
327
+ "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
328
+ ]
329
+ end
330
+
331
+ # Returns a set of all of its children and nested children
332
+ def descendants
333
+ without_self self_and_descendants
334
+ end
335
+
336
+ # Returns a set of only this entry's immediate children
337
+ def children
338
+ nested_set_scope.scoped :conditions => {parent_column_name => self}
339
+ end
340
+
341
+ def is_descendant_of?(other)
342
+ other.left < self.left && self.left < other.right && same_scope?(other)
343
+ end
344
+ alias_method :descendant_of?, :is_descendant_of?
345
+
346
+ def is_or_is_descendant_of?(other)
347
+ other.left <= self.left && self.left < other.right && same_scope?(other)
348
+ end
349
+
350
+ def is_ancestor_of?(other)
351
+ self.left < other.left && other.left < self.right && same_scope?(other)
352
+ end
353
+
354
+ def is_or_is_ancestor_of?(other)
355
+ self.left <= other.left && other.left < self.right && same_scope?(other)
356
+ end
357
+
358
+ # Check if other model is in the same scope
359
+ def same_scope?(other)
360
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
361
+ self.send(attr) == other.send(attr)
362
+ end
363
+ end
364
+
365
+ # Find the first sibling to the left
366
+ def left_sibling
367
+ siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
368
+ :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
369
+ end
370
+
371
+ # Find the first sibling to the right
372
+ def right_sibling
373
+ siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
374
+ end
375
+
376
+ # Shorthand method for finding the left sibling and moving to the left of it.
377
+ def move_left
378
+ move_to_left_of left_sibling
379
+ end
380
+
381
+ # Shorthand method for finding the right sibling and moving to the right of it.
382
+ def move_right
383
+ move_to_right_of right_sibling
384
+ end
385
+
386
+ # Move the node to the left of another node (you can pass id only)
387
+ def move_to_left_of(node)
388
+ move_to node, :left
389
+ end
390
+
391
+ # Move the node to the left of another node (you can pass id only)
392
+ def move_to_right_of(node)
393
+ move_to node, :right
394
+ end
395
+
396
+ # Move the node to the child of another node (you can pass id only)
397
+ def move_to_child_of(node)
398
+ move_to node, :child
399
+ end
400
+
401
+ # Move the node to root nodes
402
+ def move_to_root
403
+ move_to nil, :root
404
+ end
405
+
406
+ def move_possible?(target)
407
+ self != target && # Can't target self
408
+ same_scope?(target) && # can't be in different scopes
409
+ # !(left..right).include?(target.left..target.right) # this needs tested more
410
+ # detect impossible move
411
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
412
+ end
413
+
414
+ def to_text
415
+ self_and_descendants.map do |node|
416
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
417
+ end.join("\n")
418
+ end
419
+
420
+ protected
421
+
422
+ def without_self(scope)
423
+ scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
424
+ end
425
+
426
+ # All nested set queries should use this nested_set_scope, which performs finds on
427
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
428
+ # declaration.
429
+ def nested_set_scope
430
+ options = {:order => quoted_left_column_name}
431
+ scopes = Array(acts_as_nested_set_options[:scope])
432
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
433
+ conditions.merge attr => self[attr]
434
+ end unless scopes.empty?
435
+ self.class.base_class.scoped options
436
+ end
437
+
438
+ # on creation, set automatically lft and rgt to the end of the tree
439
+ def set_default_left_and_right
440
+ maxright = nested_set_scope.maximum(right_column_name) || 0
441
+ # adds the new node to the right of all existing nodes
442
+ self[left_column_name] = maxright + 1
443
+ self[right_column_name] = maxright + 2
444
+ end
445
+
446
+ # Prunes a branch off of the tree, shifting all of the elements on the right
447
+ # back to the left so the counts still work.
448
+ def prune_from_tree
449
+ return if right.nil? || left.nil?
450
+ diff = right - left + 1
451
+
452
+ delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
453
+ :destroy_all : :delete_all
454
+
455
+ self.class.base_class.transaction do
456
+ nested_set_scope.send(delete_method,
457
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
458
+ left, right]
459
+ )
460
+ nested_set_scope.update_all(
461
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
462
+ ["#{quoted_left_column_name} >= ?", right]
463
+ )
464
+ nested_set_scope.update_all(
465
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
466
+ ["#{quoted_right_column_name} >= ?", right]
467
+ )
468
+ end
469
+ end
470
+
471
+ # reload left, right, and parent
472
+ def reload_nested_set
473
+ reload(:select => "#{quoted_left_column_name}, " +
474
+ "#{quoted_right_column_name}, #{quoted_parent_column_name}")
475
+ end
476
+
477
+ def move_to(target, position)
478
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
479
+ return if callback(:before_move) == false
480
+ transaction do
481
+ if target.is_a? self.class.base_class
482
+ target.reload_nested_set
483
+ elsif position != :root
484
+ # load object if node is not an object
485
+ target = nested_set_scope.find(target)
486
+ end
487
+ self.reload_nested_set
488
+
489
+ unless position == :root || move_possible?(target)
490
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
491
+ end
492
+
493
+ bound = case position
494
+ when :child; target[right_column_name]
495
+ when :left; target[left_column_name]
496
+ when :right; target[right_column_name] + 1
497
+ when :root; 1
498
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
499
+ end
500
+
501
+ if bound > self[right_column_name]
502
+ bound = bound - 1
503
+ other_bound = self[right_column_name] + 1
504
+ else
505
+ other_bound = self[left_column_name] - 1
506
+ end
507
+
508
+ # there would be no change
509
+ return if bound == self[right_column_name] || bound == self[left_column_name]
510
+
511
+ # we have defined the boundaries of two non-overlapping intervals,
512
+ # so sorting puts both the intervals and their boundaries in order
513
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
514
+
515
+ new_parent = case position
516
+ when :child; target.id
517
+ when :root; nil
518
+ else target[parent_column_name]
519
+ end
520
+
521
+ self.class.base_class.update_all([
522
+ "#{quoted_left_column_name} = CASE " +
523
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
524
+ "THEN #{quoted_left_column_name} + :d - :b " +
525
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
526
+ "THEN #{quoted_left_column_name} + :a - :c " +
527
+ "ELSE #{quoted_left_column_name} END, " +
528
+ "#{quoted_right_column_name} = CASE " +
529
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
530
+ "THEN #{quoted_right_column_name} + :d - :b " +
531
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
532
+ "THEN #{quoted_right_column_name} + :a - :c " +
533
+ "ELSE #{quoted_right_column_name} END, " +
534
+ "#{quoted_parent_column_name} = CASE " +
535
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
536
+ "ELSE #{quoted_parent_column_name} END",
537
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
538
+ ], nested_set_scope.proxy_options[:conditions])
539
+ end
540
+ target.reload_nested_set if target
541
+ self.reload_nested_set
542
+ callback(:after_move)
543
+ end
544
+
545
+ end
546
+
547
+ end
548
+ end
549
+ end