annotate vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb @ 507:0c939c159af4 redmine-1.2

Update to Redmine 1.2.1 on 1.2-stable branch (Redmine SVN rev 6270)
author Chris Cannam
date Thu, 14 Jul 2011 10:32:19 +0100
parents cbce1fd3b1b7
children cbb26bc654de
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@37 157 (root.left > left && root.right > right).tap 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@441 428 options = {:order => "#{self.class.table_name}.#{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@441 447 return if right.nil? || left.nil? || leaf? || !self.class.exists?(id)
Chris@0 448
Chris@0 449 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
Chris@0 450 :destroy_all : :delete_all
Chris@0 451
Chris@441 452 # TODO: should destroy children (not descendants) when deleted_method is :destroy_all
Chris@0 453 self.class.base_class.transaction do
Chris@210 454 reload_nested_set
Chris@0 455 nested_set_scope.send(delete_method,
Chris@0 456 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
Chris@0 457 left, right]
Chris@0 458 )
Chris@210 459 reload_nested_set
Chris@210 460 diff = right - left + 1
Chris@0 461 nested_set_scope.update_all(
Chris@0 462 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
Chris@0 463 ["#{quoted_left_column_name} >= ?", right]
Chris@0 464 )
Chris@0 465 nested_set_scope.update_all(
Chris@0 466 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
Chris@0 467 ["#{quoted_right_column_name} >= ?", right]
Chris@0 468 )
Chris@0 469 end
Chris@441 470
Chris@441 471 # Reload is needed because children may have updated their parent (self) during deletion.
Chris@441 472 reload
Chris@0 473 end
Chris@0 474
Chris@0 475 # reload left, right, and parent
Chris@0 476 def reload_nested_set
Chris@0 477 reload(:select => "#{quoted_left_column_name}, " +
Chris@0 478 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
Chris@0 479 end
Chris@0 480
Chris@0 481 def move_to(target, position)
Chris@0 482 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
Chris@0 483 return if callback(:before_move) == false
Chris@0 484 transaction do
Chris@0 485 if target.is_a? self.class.base_class
Chris@0 486 target.reload_nested_set
Chris@0 487 elsif position != :root
Chris@0 488 # load object if node is not an object
Chris@0 489 target = nested_set_scope.find(target)
Chris@0 490 end
Chris@0 491 self.reload_nested_set
Chris@0 492
Chris@0 493 unless position == :root || move_possible?(target)
Chris@0 494 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
Chris@0 495 end
Chris@0 496
Chris@0 497 bound = case position
Chris@0 498 when :child; target[right_column_name]
Chris@0 499 when :left; target[left_column_name]
Chris@0 500 when :right; target[right_column_name] + 1
Chris@0 501 when :root; 1
Chris@0 502 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
Chris@0 503 end
Chris@0 504
Chris@0 505 if bound > self[right_column_name]
Chris@0 506 bound = bound - 1
Chris@0 507 other_bound = self[right_column_name] + 1
Chris@0 508 else
Chris@0 509 other_bound = self[left_column_name] - 1
Chris@0 510 end
Chris@0 511
Chris@0 512 # there would be no change
Chris@0 513 return if bound == self[right_column_name] || bound == self[left_column_name]
Chris@0 514
Chris@0 515 # we have defined the boundaries of two non-overlapping intervals,
Chris@0 516 # so sorting puts both the intervals and their boundaries in order
Chris@0 517 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
Chris@0 518
Chris@0 519 new_parent = case position
Chris@0 520 when :child; target.id
Chris@0 521 when :root; nil
Chris@0 522 else target[parent_column_name]
Chris@0 523 end
Chris@0 524
Chris@0 525 self.class.base_class.update_all([
Chris@0 526 "#{quoted_left_column_name} = CASE " +
Chris@0 527 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
Chris@0 528 "THEN #{quoted_left_column_name} + :d - :b " +
Chris@0 529 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
Chris@0 530 "THEN #{quoted_left_column_name} + :a - :c " +
Chris@0 531 "ELSE #{quoted_left_column_name} END, " +
Chris@0 532 "#{quoted_right_column_name} = CASE " +
Chris@0 533 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
Chris@0 534 "THEN #{quoted_right_column_name} + :d - :b " +
Chris@0 535 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
Chris@0 536 "THEN #{quoted_right_column_name} + :a - :c " +
Chris@0 537 "ELSE #{quoted_right_column_name} END, " +
Chris@0 538 "#{quoted_parent_column_name} = CASE " +
Chris@0 539 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
Chris@0 540 "ELSE #{quoted_parent_column_name} END",
Chris@0 541 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
Chris@0 542 ], nested_set_scope.proxy_options[:conditions])
Chris@0 543 end
Chris@0 544 target.reload_nested_set if target
Chris@0 545 self.reload_nested_set
Chris@0 546 callback(:after_move)
Chris@0 547 end
Chris@0 548
Chris@0 549 end
Chris@0 550
Chris@0 551 end
Chris@0 552 end
Chris@0 553 end