annotate vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents 513646585e45
children 94944d00e43c
rev   line source
Chris@0 1 module CollectiveIdea #:nodoc:
Chris@0 2 module Acts #:nodoc:
Chris@0 3 module NestedSet #:nodoc:
Chris@0 4 def self.included(base)
Chris@0 5 base.extend(SingletonMethods)
Chris@0 6 end
Chris@0 7
Chris@0 8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
Chris@0 9 # an _ordered_ tree, with the added feature that you can select the children and all of their
Chris@0 10 # descendants with a single query. The drawback is that insertion or move need some complex
Chris@0 11 # sql queries. But everything is done here by this module!
Chris@0 12 #
Chris@0 13 # Nested sets are appropriate each time you want either an orderd tree (menus,
Chris@0 14 # commercial categories) or an efficient way of querying big trees (threaded posts).
Chris@0 15 #
Chris@0 16 # == API
Chris@0 17 #
Chris@0 18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
Chris@0 19 # by another easier, except for the creation:
Chris@0 20 #
Chris@0 21 # in acts_as_tree:
Chris@0 22 # item.children.create(:name => "child1")
Chris@0 23 #
Chris@0 24 # in acts_as_nested_set:
Chris@0 25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
Chris@0 26 # child = MyClass.new(:name => "child1")
Chris@0 27 # child.save
Chris@0 28 # # now move the item to its right place
Chris@0 29 # child.move_to_child_of my_item
Chris@0 30 #
Chris@0 31 # You can pass an id or an object to:
Chris@0 32 # * <tt>#move_to_child_of</tt>
Chris@0 33 # * <tt>#move_to_right_of</tt>
Chris@0 34 # * <tt>#move_to_left_of</tt>
Chris@0 35 #
Chris@0 36 module SingletonMethods
Chris@0 37 # Configuration options are:
Chris@0 38 #
Chris@0 39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
Chris@0 40 # * +:left_column+ - column name for left boundry data, default "lft"
Chris@0 41 # * +:right_column+ - column name for right boundry data, default "rgt"
Chris@0 42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
Chris@0 43 # (if it hasn't been already) and use that as the foreign key restriction. You
Chris@0 44 # can also pass an array to scope by multiple attributes.
Chris@0 45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
Chris@0 46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
Chris@0 47 # child objects are destroyed alongside this object by calling their destroy
Chris@0 48 # method. If set to :delete_all (default), all the child objects are deleted
Chris@0 49 # without calling their destroy method.
Chris@0 50 #
Chris@0 51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
Chris@0 52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
Chris@0 53 # to acts_as_nested_set models
Chris@0 54 def acts_as_nested_set(options = {})
Chris@0 55 options = {
Chris@0 56 :parent_column => 'parent_id',
Chris@0 57 :left_column => 'lft',
Chris@0 58 :right_column => 'rgt',
Chris@0 59 :order => 'id',
Chris@0 60 :dependent => :delete_all, # or :destroy
Chris@0 61 }.merge(options)
Chris@0 62
Chris@0 63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
Chris@0 64 options[:scope] = "#{options[:scope]}_id".intern
Chris@0 65 end
Chris@0 66
Chris@0 67 write_inheritable_attribute :acts_as_nested_set_options, options
Chris@0 68 class_inheritable_reader :acts_as_nested_set_options
Chris@0 69
Chris@0 70 include Comparable
Chris@0 71 include Columns
Chris@0 72 include InstanceMethods
Chris@0 73 extend Columns
Chris@0 74 extend ClassMethods
Chris@0 75
Chris@0 76 # no bulk assignment
Chris@0 77 attr_protected left_column_name.intern,
Chris@0 78 right_column_name.intern,
Chris@0 79 parent_column_name.intern
Chris@0 80
Chris@0 81 before_create :set_default_left_and_right
Chris@0 82 before_destroy :prune_from_tree
Chris@0 83
Chris@0 84 # no assignment to structure fields
Chris@0 85 [left_column_name, right_column_name, parent_column_name].each do |column|
Chris@0 86 module_eval <<-"end_eval", __FILE__, __LINE__
Chris@0 87 def #{column}=(x)
Chris@0 88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
Chris@0 89 end
Chris@0 90 end_eval
Chris@0 91 end
Chris@0 92
Chris@0 93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
Chris@0 94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
Chris@0 95 if self.respond_to?(:define_callbacks)
Chris@0 96 define_callbacks("before_move", "after_move")
Chris@0 97 end
Chris@0 98
Chris@0 99
Chris@0 100 end
Chris@0 101
Chris@0 102 end
Chris@0 103
Chris@0 104 module ClassMethods
Chris@0 105
Chris@0 106 # Returns the first root
Chris@0 107 def root
Chris@0 108 roots.find(:first)
Chris@0 109 end
Chris@0 110
Chris@0 111 def valid?
Chris@0 112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
Chris@0 113 end
Chris@0 114
Chris@0 115 def left_and_rights_valid?
Chris@0 116 count(
Chris@0 117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
Chris@0 118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
Chris@0 119 :conditions =>
Chris@0 120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
Chris@0 121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
Chris@0 122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
Chris@0 123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
Chris@0 124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
Chris@0 125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
Chris@0 126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
Chris@0 127 ) == 0
Chris@0 128 end
Chris@0 129
Chris@0 130 def no_duplicates_for_columns?
Chris@0 131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
Chris@0 132 connection.quote_column_name(c)
Chris@0 133 end.push(nil).join(", ")
Chris@0 134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
Chris@0 135 # No duplicates
Chris@0 136 find(:first,
Chris@0 137 :select => "#{scope_string}#{column}, COUNT(#{column})",
Chris@0 138 :group => "#{scope_string}#{column}
Chris@0 139 HAVING COUNT(#{column}) > 1").nil?
Chris@0 140 end
Chris@0 141 end
Chris@0 142
Chris@0 143 # Wrapper for each_root_valid? that can deal with scope.
Chris@0 144 def all_roots_valid?
Chris@0 145 if acts_as_nested_set_options[:scope]
Chris@0 146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
Chris@0 147 each_root_valid?(grouped_roots)
Chris@0 148 end
Chris@0 149 else
Chris@0 150 each_root_valid?(roots)
Chris@0 151 end
Chris@0 152 end
Chris@0 153
Chris@0 154 def each_root_valid?(roots_to_validate)
Chris@0 155 left = right = 0
Chris@0 156 roots_to_validate.all? do |root|
Chris@0 157 returning(root.left > left && root.right > right) do
Chris@0 158 left = root.left
Chris@0 159 right = root.right
Chris@0 160 end
Chris@0 161 end
Chris@0 162 end
Chris@0 163
Chris@0 164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
Chris@0 165 def rebuild!
Chris@0 166 # Don't rebuild a valid tree.
Chris@0 167 return true if valid?
Chris@0 168
Chris@0 169 scope = lambda{|node|}
Chris@0 170 if acts_as_nested_set_options[:scope]
Chris@0 171 scope = lambda{|node|
Chris@0 172 scope_column_names.inject(""){|str, column_name|
Chris@0 173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
Chris@0 174 }
Chris@0 175 }
Chris@0 176 end
Chris@0 177 indices = {}
Chris@0 178
Chris@0 179 set_left_and_rights = lambda do |node|
Chris@0 180 # set left
Chris@0 181 node[left_column_name] = indices[scope.call(node)] += 1
Chris@0 182 # find
Chris@0 183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
Chris@0 184 # set right
Chris@0 185 node[right_column_name] = indices[scope.call(node)] += 1
Chris@0 186 node.save!
Chris@0 187 end
Chris@0 188
Chris@0 189 # Find root node(s)
Chris@0 190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
Chris@0 191 # setup index for this scope
Chris@0 192 indices[scope.call(root_node)] ||= 0
Chris@0 193 set_left_and_rights.call(root_node)
Chris@0 194 end
Chris@0 195 end
Chris@0 196 end
Chris@0 197
Chris@0 198 # Mixed into both classes and instances to provide easy access to the column names
Chris@0 199 module Columns
Chris@0 200 def left_column_name
Chris@0 201 acts_as_nested_set_options[:left_column]
Chris@0 202 end
Chris@0 203
Chris@0 204 def right_column_name
Chris@0 205 acts_as_nested_set_options[:right_column]
Chris@0 206 end
Chris@0 207
Chris@0 208 def parent_column_name
Chris@0 209 acts_as_nested_set_options[:parent_column]
Chris@0 210 end
Chris@0 211
Chris@0 212 def scope_column_names
Chris@0 213 Array(acts_as_nested_set_options[:scope])
Chris@0 214 end
Chris@0 215
Chris@0 216 def quoted_left_column_name
Chris@0 217 connection.quote_column_name(left_column_name)
Chris@0 218 end
Chris@0 219
Chris@0 220 def quoted_right_column_name
Chris@0 221 connection.quote_column_name(right_column_name)
Chris@0 222 end
Chris@0 223
Chris@0 224 def quoted_parent_column_name
Chris@0 225 connection.quote_column_name(parent_column_name)
Chris@0 226 end
Chris@0 227
Chris@0 228 def quoted_scope_column_names
Chris@0 229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
Chris@0 230 end
Chris@0 231 end
Chris@0 232
Chris@0 233 # 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.
Chris@0 234 #
Chris@0 235 # category.self_and_descendants.count
Chris@0 236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
Chris@0 237 module InstanceMethods
Chris@0 238 # Value of the parent column
Chris@0 239 def parent_id
Chris@0 240 self[parent_column_name]
Chris@0 241 end
Chris@0 242
Chris@0 243 # Value of the left column
Chris@0 244 def left
Chris@0 245 self[left_column_name]
Chris@0 246 end
Chris@0 247
Chris@0 248 # Value of the right column
Chris@0 249 def right
Chris@0 250 self[right_column_name]
Chris@0 251 end
Chris@0 252
Chris@0 253 # Returns true if this is a root node.
Chris@0 254 def root?
Chris@0 255 parent_id.nil?
Chris@0 256 end
Chris@0 257
Chris@0 258 def leaf?
Chris@0 259 new_record? || (right - left == 1)
Chris@0 260 end
Chris@0 261
Chris@0 262 # Returns true is this is a child node
Chris@0 263 def child?
Chris@0 264 !parent_id.nil?
Chris@0 265 end
Chris@0 266
Chris@0 267 # order by left column
Chris@0 268 def <=>(x)
Chris@0 269 left <=> x.left
Chris@0 270 end
Chris@0 271
Chris@0 272 # Redefine to act like active record
Chris@0 273 def ==(comparison_object)
Chris@0 274 comparison_object.equal?(self) ||
Chris@0 275 (comparison_object.instance_of?(self.class) &&
Chris@0 276 comparison_object.id == id &&
Chris@0 277 !comparison_object.new_record?)
Chris@0 278 end
Chris@0 279
Chris@0 280 # Returns root
Chris@0 281 def root
Chris@0 282 self_and_ancestors.find(:first)
Chris@0 283 end
Chris@0 284
Chris@0 285 # Returns the immediate parent
Chris@0 286 def parent
Chris@0 287 nested_set_scope.find_by_id(parent_id) if parent_id
Chris@0 288 end
Chris@0 289
Chris@0 290 # Returns the array of all parents and self
Chris@0 291 def self_and_ancestors
Chris@0 292 nested_set_scope.scoped :conditions => [
Chris@0 293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
Chris@0 294 ]
Chris@0 295 end
Chris@0 296
Chris@0 297 # Returns an array of all parents
Chris@0 298 def ancestors
Chris@0 299 without_self self_and_ancestors
Chris@0 300 end
Chris@0 301
Chris@0 302 # Returns the array of all children of the parent, including self
Chris@0 303 def self_and_siblings
Chris@0 304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
Chris@0 305 end
Chris@0 306
Chris@0 307 # Returns the array of all children of the parent, except self
Chris@0 308 def siblings
Chris@0 309 without_self self_and_siblings
Chris@0 310 end
Chris@0 311
Chris@0 312 # Returns a set of all of its nested children which do not have children
Chris@0 313 def leaves
Chris@0 314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
Chris@0 315 end
Chris@0 316
Chris@0 317 # Returns the level of this object in the tree
Chris@0 318 # root level is 0
Chris@0 319 def level
Chris@0 320 parent_id.nil? ? 0 : ancestors.count
Chris@0 321 end
Chris@0 322
Chris@0 323 # Returns a set of itself and all of its nested children
Chris@0 324 def self_and_descendants
Chris@0 325 nested_set_scope.scoped :conditions => [
Chris@0 326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
Chris@0 327 ]
Chris@0 328 end
Chris@0 329
Chris@0 330 # Returns a set of all of its children and nested children
Chris@0 331 def descendants
Chris@0 332 without_self self_and_descendants
Chris@0 333 end
Chris@0 334
Chris@0 335 # Returns a set of only this entry's immediate children
Chris@0 336 def children
Chris@0 337 nested_set_scope.scoped :conditions => {parent_column_name => self}
Chris@0 338 end
Chris@0 339
Chris@0 340 def is_descendant_of?(other)
Chris@0 341 other.left < self.left && self.left < other.right && same_scope?(other)
Chris@0 342 end
Chris@0 343
Chris@0 344 def is_or_is_descendant_of?(other)
Chris@0 345 other.left <= self.left && self.left < other.right && same_scope?(other)
Chris@0 346 end
Chris@0 347
Chris@0 348 def is_ancestor_of?(other)
Chris@0 349 self.left < other.left && other.left < self.right && same_scope?(other)
Chris@0 350 end
Chris@0 351
Chris@0 352 def is_or_is_ancestor_of?(other)
Chris@0 353 self.left <= other.left && other.left < self.right && same_scope?(other)
Chris@0 354 end
Chris@0 355
Chris@0 356 # Check if other model is in the same scope
Chris@0 357 def same_scope?(other)
Chris@0 358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
Chris@0 359 self.send(attr) == other.send(attr)
Chris@0 360 end
Chris@0 361 end
Chris@0 362
Chris@0 363 # Find the first sibling to the left
Chris@0 364 def left_sibling
Chris@0 365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
Chris@0 366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
Chris@0 367 end
Chris@0 368
Chris@0 369 # Find the first sibling to the right
Chris@0 370 def right_sibling
Chris@0 371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
Chris@0 372 end
Chris@0 373
Chris@0 374 # Shorthand method for finding the left sibling and moving to the left of it.
Chris@0 375 def move_left
Chris@0 376 move_to_left_of left_sibling
Chris@0 377 end
Chris@0 378
Chris@0 379 # Shorthand method for finding the right sibling and moving to the right of it.
Chris@0 380 def move_right
Chris@0 381 move_to_right_of right_sibling
Chris@0 382 end
Chris@0 383
Chris@0 384 # Move the node to the left of another node (you can pass id only)
Chris@0 385 def move_to_left_of(node)
Chris@0 386 move_to node, :left
Chris@0 387 end
Chris@0 388
Chris@0 389 # Move the node to the left of another node (you can pass id only)
Chris@0 390 def move_to_right_of(node)
Chris@0 391 move_to node, :right
Chris@0 392 end
Chris@0 393
Chris@0 394 # Move the node to the child of another node (you can pass id only)
Chris@0 395 def move_to_child_of(node)
Chris@0 396 move_to node, :child
Chris@0 397 end
Chris@0 398
Chris@0 399 # Move the node to root nodes
Chris@0 400 def move_to_root
Chris@0 401 move_to nil, :root
Chris@0 402 end
Chris@0 403
Chris@0 404 def move_possible?(target)
Chris@0 405 self != target && # Can't target self
Chris@0 406 same_scope?(target) && # can't be in different scopes
Chris@0 407 # !(left..right).include?(target.left..target.right) # this needs tested more
Chris@0 408 # detect impossible move
Chris@0 409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
Chris@0 410 end
Chris@0 411
Chris@0 412 def to_text
Chris@0 413 self_and_descendants.map do |node|
Chris@0 414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
Chris@0 415 end.join("\n")
Chris@0 416 end
Chris@0 417
Chris@0 418 protected
Chris@0 419
Chris@0 420 def without_self(scope)
Chris@0 421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
Chris@0 422 end
Chris@0 423
Chris@0 424 # All nested set queries should use this nested_set_scope, which performs finds on
Chris@0 425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
Chris@0 426 # declaration.
Chris@0 427 def nested_set_scope
Chris@0 428 options = {:order => quoted_left_column_name}
Chris@0 429 scopes = Array(acts_as_nested_set_options[:scope])
Chris@0 430 options[:conditions] = scopes.inject({}) do |conditions,attr|
Chris@0 431 conditions.merge attr => self[attr]
Chris@0 432 end unless scopes.empty?
Chris@0 433 self.class.base_class.scoped options
Chris@0 434 end
Chris@0 435
Chris@0 436 # on creation, set automatically lft and rgt to the end of the tree
Chris@0 437 def set_default_left_and_right
Chris@0 438 maxright = nested_set_scope.maximum(right_column_name) || 0
Chris@0 439 # adds the new node to the right of all existing nodes
Chris@0 440 self[left_column_name] = maxright + 1
Chris@0 441 self[right_column_name] = maxright + 2
Chris@0 442 end
Chris@0 443
Chris@0 444 # Prunes a branch off of the tree, shifting all of the elements on the right
Chris@0 445 # back to the left so the counts still work.
Chris@0 446 def prune_from_tree
Chris@0 447 return if right.nil? || left.nil?
Chris@0 448 diff = right - left + 1
Chris@0 449
Chris@0 450 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
Chris@0 451 :destroy_all : :delete_all
Chris@0 452
Chris@0 453 self.class.base_class.transaction do
Chris@0 454 nested_set_scope.send(delete_method,
Chris@0 455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
Chris@0 456 left, right]
Chris@0 457 )
Chris@0 458 nested_set_scope.update_all(
Chris@0 459 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
Chris@0 460 ["#{quoted_left_column_name} >= ?", right]
Chris@0 461 )
Chris@0 462 nested_set_scope.update_all(
Chris@0 463 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
Chris@0 464 ["#{quoted_right_column_name} >= ?", right]
Chris@0 465 )
Chris@0 466 end
Chris@0 467 end
Chris@0 468
Chris@0 469 # reload left, right, and parent
Chris@0 470 def reload_nested_set
Chris@0 471 reload(:select => "#{quoted_left_column_name}, " +
Chris@0 472 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
Chris@0 473 end
Chris@0 474
Chris@0 475 def move_to(target, position)
Chris@0 476 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
Chris@0 477 return if callback(:before_move) == false
Chris@0 478 transaction do
Chris@0 479 if target.is_a? self.class.base_class
Chris@0 480 target.reload_nested_set
Chris@0 481 elsif position != :root
Chris@0 482 # load object if node is not an object
Chris@0 483 target = nested_set_scope.find(target)
Chris@0 484 end
Chris@0 485 self.reload_nested_set
Chris@0 486
Chris@0 487 unless position == :root || move_possible?(target)
Chris@0 488 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
Chris@0 489 end
Chris@0 490
Chris@0 491 bound = case position
Chris@0 492 when :child; target[right_column_name]
Chris@0 493 when :left; target[left_column_name]
Chris@0 494 when :right; target[right_column_name] + 1
Chris@0 495 when :root; 1
Chris@0 496 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
Chris@0 497 end
Chris@0 498
Chris@0 499 if bound > self[right_column_name]
Chris@0 500 bound = bound - 1
Chris@0 501 other_bound = self[right_column_name] + 1
Chris@0 502 else
Chris@0 503 other_bound = self[left_column_name] - 1
Chris@0 504 end
Chris@0 505
Chris@0 506 # there would be no change
Chris@0 507 return if bound == self[right_column_name] || bound == self[left_column_name]
Chris@0 508
Chris@0 509 # we have defined the boundaries of two non-overlapping intervals,
Chris@0 510 # so sorting puts both the intervals and their boundaries in order
Chris@0 511 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
Chris@0 512
Chris@0 513 new_parent = case position
Chris@0 514 when :child; target.id
Chris@0 515 when :root; nil
Chris@0 516 else target[parent_column_name]
Chris@0 517 end
Chris@0 518
Chris@0 519 self.class.base_class.update_all([
Chris@0 520 "#{quoted_left_column_name} = CASE " +
Chris@0 521 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
Chris@0 522 "THEN #{quoted_left_column_name} + :d - :b " +
Chris@0 523 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
Chris@0 524 "THEN #{quoted_left_column_name} + :a - :c " +
Chris@0 525 "ELSE #{quoted_left_column_name} END, " +
Chris@0 526 "#{quoted_right_column_name} = CASE " +
Chris@0 527 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
Chris@0 528 "THEN #{quoted_right_column_name} + :d - :b " +
Chris@0 529 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
Chris@0 530 "THEN #{quoted_right_column_name} + :a - :c " +
Chris@0 531 "ELSE #{quoted_right_column_name} END, " +
Chris@0 532 "#{quoted_parent_column_name} = CASE " +
Chris@0 533 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
Chris@0 534 "ELSE #{quoted_parent_column_name} END",
Chris@0 535 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
Chris@0 536 ], nested_set_scope.proxy_options[:conditions])
Chris@0 537 end
Chris@0 538 target.reload_nested_set if target
Chris@0 539 self.reload_nested_set
Chris@0 540 callback(:after_move)
Chris@0 541 end
Chris@0 542
Chris@0 543 end
Chris@0 544
Chris@0 545 end
Chris@0 546 end
Chris@0 547 end