annotate .svn/pristine/3e/3e2856705bc7c004aa221bf2a7acea9b1ba0b99e.svn-base @ 1327:287f201c2802 redmine-2.2-integration

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