| 1 |
module SymetrieCom |
|---|
| 2 |
module Acts #:nodoc: |
|---|
| 3 |
module NestedSet #:nodoc: |
|---|
| 4 |
def self.append_features(base) |
|---|
| 5 |
super |
|---|
| 6 |
base.extend(ClassMethods) |
|---|
| 7 |
end |
|---|
| 8 |
|
|---|
| 9 |
# better_nested_set ehances the core nested_set tree functionality provided in ruby_on_rails. |
|---|
| 10 |
# |
|---|
| 11 |
# This acts provides Nested Set functionality. Nested Set is a smart way to implement |
|---|
| 12 |
# an _ordered_ tree, with the added feature that you can select the children and all of their |
|---|
| 13 |
# descendents with a single query. The drawback is that insertion or move need some complex |
|---|
| 14 |
# sql queries. But everything is done here by this module! |
|---|
| 15 |
# |
|---|
| 16 |
# Nested sets are appropriate each time you want either an orderd tree (menus, |
|---|
| 17 |
# commercial categories) or an efficient way of querying big trees (threaded posts). |
|---|
| 18 |
# |
|---|
| 19 |
# == API |
|---|
| 20 |
# Methods names are aligned on Tree's ones as much as possible, to make replacment from one |
|---|
| 21 |
# by another easier, except for the creation: |
|---|
| 22 |
# |
|---|
| 23 |
# in acts_as_tree: |
|---|
| 24 |
# item.children.create(:name => "child1") |
|---|
| 25 |
# |
|---|
| 26 |
# in acts_as_nested_set: |
|---|
| 27 |
# # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1 |
|---|
| 28 |
# child = MyClass.new(:name => "child1") |
|---|
| 29 |
# child.save |
|---|
| 30 |
# # now move the item to its right place |
|---|
| 31 |
# child.move_to_child_of my_item |
|---|
| 32 |
# |
|---|
| 33 |
# You can use: |
|---|
| 34 |
# * move_to_child_of |
|---|
| 35 |
# * move_to_right_of |
|---|
| 36 |
# * move_to_left_of |
|---|
| 37 |
# and pass them an id or an object. |
|---|
| 38 |
# |
|---|
| 39 |
# Other methods added by this mixin are: |
|---|
| 40 |
# * +root+ - root item of the tree (the one that has a nil parent; should have left_column = 1 too) |
|---|
| 41 |
# * +roots+ - root items, in case of multiple roots (the ones that have a nil parent) |
|---|
| 42 |
# * +level+ - number indicating the level, a root being level 0 |
|---|
| 43 |
# * +ancestors+ - array of all parents, with root as first item |
|---|
| 44 |
# * +self_and_ancestors+ - array of all parents and self |
|---|
| 45 |
# * +siblings+ - array of all siblings, that are the items sharing the same parent and level |
|---|
| 46 |
# * +self_and_siblings+ - array of itself and all siblings |
|---|
| 47 |
# * +children_count+ - count of all immediate children |
|---|
| 48 |
# * +children+ - array of all immediate childrens |
|---|
| 49 |
# * +all_children+ - array of all children and nested children |
|---|
| 50 |
# * +full_set+ - array of itself and all children and nested children |
|---|
| 51 |
# |
|---|
| 52 |
# These should not be useful, except if you want to write direct SQL: |
|---|
| 53 |
# * +left_col_name+ - name of the left column passed on the declaration line |
|---|
| 54 |
# * +right_col_name+ - name of the right column passed on the declaration line |
|---|
| 55 |
# * +parent_col_name+ - name of the parent column passed on the declaration line |
|---|
| 56 |
# |
|---|
| 57 |
# recommandations: |
|---|
| 58 |
# Don't name your left and right columns 'left' and 'right': these names are reserved on most of dbs. |
|---|
| 59 |
# Usage is to name them 'lft' and 'rgt' for instance. |
|---|
| 60 |
# |
|---|
| 61 |
module ClassMethods |
|---|
| 62 |
# Configuration options are: |
|---|
| 63 |
# |
|---|
| 64 |
# * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) |
|---|
| 65 |
# * +left_column+ - column name for left boundry data, default "lft" |
|---|
| 66 |
# * +right_column+ - column name for right boundry data, default "rgt" |
|---|
| 67 |
# * +text_column+ - column name for the title field (optional). Used as default in the |
|---|
| 68 |
# {your-class}_options_for_select helper method. If empty, will use the first string field |
|---|
| 69 |
# of your model class. |
|---|
| 70 |
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" |
|---|
| 71 |
# (if that hasn't been already) and use that as the foreign key restriction. It's also possible |
|---|
| 72 |
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. |
|---|
| 73 |
# Example: <tt>acts_as_nested_set :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt> |
|---|
| 74 |
def acts_as_nested_set(options = {}) |
|---|
| 75 |
if options[:scope].is_a?(Symbol) |
|---|
| 76 |
if options[:scope].to_s !~ /_id$/ |
|---|
| 77 |
options[:scope] = "#{options[:scope]}_id".intern |
|---|
| 78 |
end |
|---|
| 79 |
options[:scope] = %(#{options[:scope].to_s}.nil? ? "#{options[:scope].to_s} IS NULL" : "#{options[:scope].to_s} = \#{#{options[:scope].to_s}}") |
|---|
| 80 |
end |
|---|
| 81 |
|
|---|
| 82 |
# options[:scope] = %("#{options[:scope]}") |
|---|
| 83 |
|
|---|
| 84 |
write_inheritable_attribute(:acts_as_nested_set_options, |
|---|
| 85 |
{ :parent_column => (options[:parent_column] || 'parent_id'), |
|---|
| 86 |
:left_column => (options[:left_column] || 'lft'), |
|---|
| 87 |
:right_column => (options[:right_column] || 'rgt'), |
|---|
| 88 |
:scope => (options[:scope] || '1 = 1'), |
|---|
| 89 |
:text_column => (options[:text_column] || columns.collect{|c| (c.type == :string) ? c.name : nil }.compact.first) |
|---|
| 90 |
} ) |
|---|
| 91 |
|
|---|
| 92 |
class_inheritable_reader :acts_as_nested_set_options |
|---|
| 93 |
|
|---|
| 94 |
# no bulk assignment |
|---|
| 95 |
attr_protected acts_as_nested_set_options[:left_column].intern, |
|---|
| 96 |
acts_as_nested_set_options[:right_column].intern, |
|---|
| 97 |
acts_as_nested_set_options[:parent_column].intern |
|---|
| 98 |
# no assignment to structure fields |
|---|
| 99 |
module_eval <<-"end_eval", __FILE__, __LINE__ |
|---|
| 100 |
def #{acts_as_nested_set_options[:left_column]}=(x) |
|---|
| 101 |
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:left_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." |
|---|
| 102 |
end |
|---|
| 103 |
def #{acts_as_nested_set_options[:right_column]}=(x) |
|---|
| 104 |
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:right_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." |
|---|
| 105 |
end |
|---|
| 106 |
def #{acts_as_nested_set_options[:parent_column]}=(x) |
|---|
| 107 |
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:parent_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." |
|---|
| 108 |
end |
|---|
| 109 |
end_eval |
|---|
| 110 |
|
|---|
| 111 |
include SymetrieCom::Acts::NestedSet::InstanceMethods |
|---|
| 112 |
extend SymetrieCom::Acts::NestedSet::ClassMethods |
|---|
| 113 |
|
|---|
| 114 |
# adds the helper for the class |
|---|
| 115 |
# ActionView::Base.send(:define_method, "#{Inflector.underscore(self.class)}_options_for_select") { special=nil |
|---|
| 116 |
# "#{acts_as_nested_set_options[:text_column]} || "#{self.class} id #{id}" |
|---|
| 117 |
# } |
|---|
| 118 |
|
|---|
| 119 |
end |
|---|
| 120 |
|
|---|
| 121 |
# Returns the single root |
|---|
| 122 |
def root |
|---|
| 123 |
self.find(:first, :conditions => "(#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:parent_column]} IS NULL)") |
|---|
| 124 |
end |
|---|
| 125 |
|
|---|
| 126 |
# Returns roots when multiple roots (or virtual root, which is the same) |
|---|
| 127 |
def roots |
|---|
| 128 |
self.find(:all, :conditions => "(#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:parent_column]} IS NULL)", :order => "#{acts_as_nested_set_options[:left_column]}") |
|---|
| 129 |
end |
|---|
| 130 |
end |
|---|
| 131 |
|
|---|
| 132 |
module InstanceMethods |
|---|
| 133 |
def left_col_name() acts_as_nested_set_options[:left_column] end |
|---|
| 134 |
def right_col_name() acts_as_nested_set_options[:right_column] end |
|---|
| 135 |
def parent_col_name() acts_as_nested_set_options[:parent_column] end |
|---|
| 136 |
|
|---|
| 137 |
# on creation, set automatically lft and rgt to the end of the tree |
|---|
| 138 |
def before_create |
|---|
| 139 |
maxright = self.class.maximum(acts_as_nested_set_options[:right_column], :conditions => acts_as_nested_set_options[:scope]) || 0 |
|---|
| 140 |
# adds the new node to the right of all existing nodes |
|---|
| 141 |
|
|---|
| 142 |
self[acts_as_nested_set_options[:left_column]] = maxright+1 |
|---|
| 143 |
self[acts_as_nested_set_options[:right_column]] = maxright+2 |
|---|
| 144 |
end |
|---|
| 145 |
|
|---|
| 146 |
# Returns true if this is a root node. |
|---|
| 147 |
def root? |
|---|
| 148 |
parent_id = self[acts_as_nested_set_options[:parent_column]] |
|---|
| 149 |
(parent_id == 0 || parent_id.nil?) && (self[acts_as_nested_set_options[:left_column]] == 1) && (self[acts_as_nested_set_options[:right_column]] > self[acts_as_nested_set_options[:left_column]]) |
|---|
| 150 |
end |
|---|
| 151 |
|
|---|
| 152 |
# Returns true is this is a child node |
|---|
| 153 |
def child? |
|---|
| 154 |
parent_id = self[acts_as_nested_set_options[:parent_column]] |
|---|
| 155 |
!(parent_id == 0 || parent_id.nil?) && (self[acts_as_nested_set_options[:left_column]] > 1) && (self[acts_as_nested_set_options[:right_column]] > self[acts_as_nested_set_options[:left_column]]) |
|---|
| 156 |
end |
|---|
| 157 |
|
|---|
| 158 |
# Returns true if we have no idea what this is |
|---|
| 159 |
# |
|---|
| 160 |
# Deprecated, will be removed in next versions |
|---|
| 161 |
def unknown? |
|---|
| 162 |
!root? && !child? |
|---|
| 163 |
end |
|---|
| 164 |
|
|---|
| 165 |
# order by left column |
|---|
| 166 |
def <=>(x) |
|---|
| 167 |
self[acts_as_nested_set_options[:left_column]] <=> x[acts_as_nested_set_options[:left_column]] |
|---|
| 168 |
end |
|---|
| 169 |
|
|---|
| 170 |
# Adds a child to this object in the tree. If this object hasn't been initialized, |
|---|
| 171 |
# it gets set up as a root node. Otherwise, this method will update all of the |
|---|
| 172 |
# other elements in the tree and shift them to the right, keeping everything |
|---|
| 173 |
# balanced. |
|---|
| 174 |
# |
|---|
| 175 |
# Deprecated, will be removed in next versions |
|---|
| 176 |
def add_child( child ) |
|---|
| 177 |
self.reload |
|---|
| 178 |
child.reload |
|---|
| 179 |
|
|---|
| 180 |
if child.root? |
|---|
| 181 |
raise ActiveRecord::ActiveRecordError, "Adding sub-tree isn\'t currently supported" |
|---|
| 182 |
else |
|---|
| 183 |
if ( (self[acts_as_nested_set_options[:left_column]] == nil) || (self[acts_as_nested_set_options[:right_column]] == nil) ) |
|---|
| 184 |
# Looks like we're now the root node! Woo |
|---|
| 185 |
self[acts_as_nested_set_options[:left_column]] = 1 |
|---|
| 186 |
self[acts_as_nested_set_options[:right_column]] = 4 |
|---|
| 187 |
|
|---|
| 188 |
# What do to do about validation? |
|---|
| 189 |
return nil unless self.save |
|---|
| 190 |
|
|---|
| 191 |
child[acts_as_nested_set_options[:parent_column]] = self.id |
|---|
| 192 |
child[acts_as_nested_set_options[:left_column]] = 2 |
|---|
| 193 |
child[acts_as_nested_set_options[:right_column]]= 3 |
|---|
| 194 |
return child.save |
|---|
| 195 |
else |
|---|
| 196 |
# OK, we need to add and shift everything else to the right |
|---|
| 197 |
child[acts_as_nested_set_options[:parent_column]] = self.id |
|---|
| 198 |
right_bound = self[acts_as_nested_set_options[:right_column]] |
|---|
| 199 |
child[acts_as_nested_set_options[:left_column]] = right_bound |
|---|
| 200 |
child[acts_as_nested_set_options[:right_column]] = right_bound + 1 |
|---|
| 201 |
self[acts_as_nested_set_options[:right_column]] += 2 |
|---|
| 202 |
self.class.transaction { |
|---|
| 203 |
self.class.update_all( "#{acts_as_nested_set_options[:left_column]} = (#{acts_as_nested_set_options[:left_column]} + 2)", "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:left_column]} >= #{right_bound}" ) |
|---|
| 204 |
self.class.update_all( "#{acts_as_nested_set_options[:right_column]} = (#{acts_as_nested_set_options[:right_column]} + 2)", "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:right_column]} >= #{right_bound}" ) |
|---|
| 205 |
self.save |
|---|
| 206 |
child.save |
|---|
| 207 |
} |
|---|
| 208 |
end |
|---|
| 209 |
end |
|---|
| 210 |
end |
|---|
| 211 |
|
|---|
| 212 |
# Returns root |
|---|
| 213 |
def root |
|---|
| 214 |
self.class.find(:first, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:parent_column]} IS NULL)") |
|---|
| 215 |
end |
|---|
| 216 |
|
|---|
| 217 |
# Returns roots when multiple roots (or virtual root, which is the same) |
|---|
| 218 |
def roots |
|---|
| 219 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:parent_column]} IS NULL)", :order => "#{acts_as_nested_set_options[:left_column]}") |
|---|
| 220 |
end |
|---|
| 221 |
|
|---|
| 222 |
# Returns the parent |
|---|
| 223 |
def parent |
|---|
| 224 |
self.class.find(self[acts_as_nested_set_options[:parent_column]]) if self[acts_as_nested_set_options[:parent_column]] |
|---|
| 225 |
end |
|---|
| 226 |
|
|---|
| 227 |
# Returns an array of all parents |
|---|
| 228 |
# Maybe 'full_outline' would be a better name, but we prefer to mimic the Tree class |
|---|
| 229 |
def ancestors |
|---|
| 230 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} < #{self[acts_as_nested_set_options[:left_column]]} and #{acts_as_nested_set_options[:right_column]} > #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column] ) |
|---|
| 231 |
end |
|---|
| 232 |
|
|---|
| 233 |
# Returns the array of all parents and self |
|---|
| 234 |
def self_and_ancestors |
|---|
| 235 |
ancestors + [self] |
|---|
| 236 |
end |
|---|
| 237 |
|
|---|
| 238 |
# Returns the array of all children of the parent, except self |
|---|
| 239 |
def siblings |
|---|
| 240 |
self_and_siblings - [self] |
|---|
| 241 |
end |
|---|
| 242 |
|
|---|
| 243 |
# Returns the array of all children of the parent, included self |
|---|
| 244 |
def self_and_siblings |
|---|
| 245 |
if self[acts_as_nested_set_options[:parent_column]].nil? || self[acts_as_nested_set_options[:parent_column]].zero? |
|---|
| 246 |
[self] |
|---|
| 247 |
else |
|---|
| 248 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} and #{acts_as_nested_set_options[:parent_column]} = #{self[acts_as_nested_set_options[:parent_column]]}", :order => acts_as_nested_set_options[:left_column]) |
|---|
| 249 |
end |
|---|
| 250 |
end |
|---|
| 251 |
|
|---|
| 252 |
# Returns the level of this object in the tree |
|---|
| 253 |
# root level is 0 |
|---|
| 254 |
def level |
|---|
| 255 |
return 0 if self[acts_as_nested_set_options[:parent_column]].nil? |
|---|
| 256 |
self.class.count("#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} < #{self[acts_as_nested_set_options[:left_column]]} and #{acts_as_nested_set_options[:right_column]} > #{self[acts_as_nested_set_options[:right_column]]})") |
|---|
| 257 |
end |
|---|
| 258 |
|
|---|
| 259 |
# Returns the number of nested children of this object. |
|---|
| 260 |
def children_count |
|---|
| 261 |
return (self[acts_as_nested_set_options[:right_column]] - self[acts_as_nested_set_options[:left_column]] - 1)/2 |
|---|
| 262 |
end |
|---|
| 263 |
|
|---|
| 264 |
# Returns a set of itself and all of its nested children |
|---|
| 265 |
# Pass :exclude => item, or id, or [items or id] to exclude some parts of the tree |
|---|
| 266 |
def full_set(special=nil) |
|---|
| 267 |
return [self] if new_record? or self[acts_as_nested_set_options[:right_column]]-self[acts_as_nested_set_options[:left_column]] == 1 |
|---|
| 268 |
# self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} BETWEEN #{self[acts_as_nested_set_options[:left_column]]} and #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column]) |
|---|
| 269 |
[self] + all_children(special) |
|---|
| 270 |
end |
|---|
| 271 |
|
|---|
| 272 |
# Returns a set of all of its children and nested children |
|---|
| 273 |
# Pass :exclude => item, or id, or [items or id] to exclude some parts of the tree |
|---|
| 274 |
def all_children(special=nil) |
|---|
| 275 |
if special && special[:exclude] |
|---|
| 276 |
transaction do |
|---|
| 277 |
# exclude some items and all their children |
|---|
| 278 |
special[:exclude] = [special[:exclude]] if !special[:exclude].is_a?(Array) |
|---|
| 279 |
# get objects for ids |
|---|
| 280 |
special[:exclude].collect! {|s| s.is_a?(self.class) ? s : self.class.find(s)} |
|---|
| 281 |
# get all subtrees and flatten the list |
|---|
| 282 |
exclude_list = special[:exclude].map{|e| e.full_set.map{|ee| ee.id}}.flatten.uniq.join(',') |
|---|
| 283 |
if exclude_list.blank? |
|---|
| 284 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]}) and (#{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column]) |
|---|
| 285 |
else |
|---|
| 286 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND id NOT IN (#{exclude_list}) AND (#{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]}) and (#{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column]) |
|---|
| 287 |
end |
|---|
| 288 |
end |
|---|
| 289 |
else |
|---|
| 290 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND (#{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]}) and (#{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]})", :order => acts_as_nested_set_options[:left_column]) |
|---|
| 291 |
end |
|---|
| 292 |
end |
|---|
| 293 |
|
|---|
| 294 |
# Returns a set of only this entry's immediate children |
|---|
| 295 |
def children |
|---|
| 296 |
self.class.find(:all, :conditions => "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:parent_column]} = #{self.id}", :order => acts_as_nested_set_options[:left_column]) |
|---|
| 297 |
end |
|---|
| 298 |
|
|---|
| 299 |
# Prunes a branch off of the tree, shifting all of the elements on the right |
|---|
| 300 |
# back to the left so the counts still work. |
|---|
| 301 |
def before_destroy |
|---|
| 302 |
return if self[acts_as_nested_set_options[:right_column]].nil? || self[acts_as_nested_set_options[:left_column]].nil? |
|---|
| 303 |
dif = self[acts_as_nested_set_options[:right_column]] - self[acts_as_nested_set_options[:left_column]] + 1 |
|---|
| 304 |
|
|---|
| 305 |
self.class.transaction { |
|---|
| 306 |
self.class.delete_all( "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:left_column]} > #{self[acts_as_nested_set_options[:left_column]]} and #{acts_as_nested_set_options[:right_column]} < #{self[acts_as_nested_set_options[:right_column]]}" ) |
|---|
| 307 |
self.class.update_all( "#{acts_as_nested_set_options[:left_column]} = (#{acts_as_nested_set_options[:left_column]} - #{dif})", "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:left_column]} >= #{self[acts_as_nested_set_options[:right_column]]}" ) |
|---|
| 308 |
self.class.update_all( "#{acts_as_nested_set_options[:right_column]} = (#{acts_as_nested_set_options[:right_column]} - #{dif} )", "#{acts_as_nested_set_options[:scope]} AND #{acts_as_nested_set_options[:right_column]} >= #{self[acts_as_nested_set_options[:right_column]]}" ) |
|---|
| 309 |
} |
|---|
| 310 |
end |
|---|
| 311 |
|
|---|
| 312 |
# Move the node to the left of another node (you can pass id only) |
|---|
| 313 |
def move_to_left_of(node) |
|---|
| 314 |
self.move_to node, :left |
|---|
| 315 |
end |
|---|
| 316 |
|
|---|
| 317 |
# Move the node to the left of another node (you can pass id only) |
|---|
| 318 |
def move_to_right_of(node) |
|---|
| 319 |
self.move_to node, :right |
|---|
| 320 |
end |
|---|
| 321 |
|
|---|
| 322 |
# Move the node to the child of another node (you can pass id only) |
|---|
| 323 |
def move_to_child_of(node) |
|---|
| 324 |
self.move_to node, :child |
|---|
| 325 |
end |
|---|
| 326 |
|
|---|
| 327 |
protected |
|---|
| 328 |
def move_to(target, position) |
|---|
| 329 |
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.id.nil? |
|---|
| 330 |
|
|---|
| 331 |
# use shorter names for readability: current left and right |
|---|
| 332 |
cur_left, cur_right = self[acts_as_nested_set_options[:left_column]], self[acts_as_nested_set_options[:right_column]] |
|---|
| 333 |
|
|---|
| 334 |
# extent is the width of the tree self and children |
|---|
| 335 |
extent = cur_right - cur_left + 1 |
|---|
| 336 |
|
|---|
| 337 |
# load object if node is not an object |
|---|
| 338 |
if !(self.class === target) |
|---|
| 339 |
target = self.class.find(target) |
|---|
| 340 |
end |
|---|
| 341 |
target_left, target_right = target[acts_as_nested_set_options[:left_column]], target[acts_as_nested_set_options[:right_column]] |
|---|
| 342 |
|
|---|
| 343 |
# detect impossible move |
|---|
| 344 |
if ((cur_left <= target_left) && (target_left <= cur_right)) or ((cur_left <= target_right) && (target_right <= cur_right)) |
|---|
| 345 |
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree." |
|---|
| 346 |
end |
|---|
| 347 |
|
|---|
| 348 |
# compute new left/right for self |
|---|
| 349 |
if position == :child |
|---|
| 350 |
if target_left < cur_left |
|---|
| 351 |
new_left = target_left + 1 |
|---|
| 352 |
new_right = target_left + extent |
|---|
| 353 |
else |
|---|
| 354 |
new_left = target_left - extent + 1 |
|---|
| 355 |
new_right = target_left |
|---|
| 356 |
end |
|---|
| 357 |
elsif position == :left |
|---|
| 358 |
if target_left < cur_left |
|---|
| 359 |
new_left = target_left |
|---|
| 360 |
new_right = target_left + extent - 1 |
|---|
| 361 |
else |
|---|
| 362 |
new_left = target_left - extent |
|---|
| 363 |
new_right = target_left - 1 |
|---|
| 364 |
end |
|---|
| 365 |
elsif position == :right |
|---|
| 366 |
if target_right < cur_right |
|---|
| 367 |
new_left = target_right + 1 |
|---|
| 368 |
new_right = target_right + extent |
|---|
| 369 |
else |
|---|
| 370 |
new_left = target_right - extent + 1 |
|---|
| 371 |
new_right = target_right |
|---|
| 372 |
end |
|---|
| 373 |
else |
|---|
| 374 |
raise ActiveRecord::ActiveRecordError, "Position should be either left or right ('#{position}' received)." |
|---|
| 375 |
end |
|---|
| 376 |
|
|---|
| 377 |
# boundaries of update action |
|---|
| 378 |
b_left, b_right = [cur_left, new_left].min, [cur_right, new_right].max |
|---|
| 379 |
|
|---|
| 380 |
# Shift value to move self to new position |
|---|
| 381 |
shift = new_left - cur_left |
|---|
| 382 |
|
|---|
| 383 |
# Shift value to move nodes inside boundaries but not under self_and_children |
|---|
| 384 |
updown = (shift > 0) ? -extent : extent |
|---|
| 385 |
|
|---|
| 386 |
# change nil to NULL for new parent |
|---|
| 387 |
if position == :child |
|---|
| 388 |
new_parent = target.id |
|---|
| 389 |
else |
|---|
| 390 |
new_parent = target[acts_as_nested_set_options[:parent_column]].nil? ? 'NULL' : target[acts_as_nested_set_options[:parent_column]] |
|---|
| 391 |
end |
|---|
| 392 |
|
|---|
| 393 |
# update and that rules |
|---|
| 394 |
self.class.update_all( "#{acts_as_nested_set_options[:left_column]} = CASE \ |
|---|
| 395 |
WHEN #{acts_as_nested_set_options[:left_column]} BETWEEN #{cur_left} AND #{cur_right} \ |
|---|
| 396 |
THEN #{acts_as_nested_set_options[:left_column]} + #{shift} \ |
|---|
| 397 |
WHEN #{acts_as_nested_set_options[:left_column]} BETWEEN #{b_left} AND #{b_right} \ |
|---|
| 398 |
THEN #{acts_as_nested_set_options[:left_column]} + #{updown} \ |
|---|
| 399 |
ELSE #{acts_as_nested_set_options[:left_column]} END, \ |
|---|
| 400 |
#{acts_as_nested_set_options[:right_column]} = CASE \ |
|---|
| 401 |
WHEN #{acts_as_nested_set_options[:right_column]} BETWEEN #{cur_left} AND #{cur_right} \ |
|---|
| 402 |
THEN #{acts_as_nested_set_options[:right_column]} + #{shift} \ |
|---|
| 403 |
WHEN #{acts_as_nested_set_options[:right_column]} BETWEEN #{b_left} AND #{b_right} \ |
|---|
| 404 |
THEN #{acts_as_nested_set_options[:right_column]} + #{updown} \ |
|---|
| 405 |
ELSE #{acts_as_nested_set_options[:right_column]} END, \ |
|---|
| 406 |
#{acts_as_nested_set_options[:parent_column]} = CASE \ |
|---|
| 407 |
WHEN #{self.class.primary_key} = #{self.id} \ |
|---|
| 408 |
THEN #{new_parent} \ |
|---|
| 409 |
ELSE #{acts_as_nested_set_options[:parent_column]} END", |
|---|
| 410 |
acts_as_nested_set_options[:scope] ) |
|---|
| 411 |
self.reload |
|---|
| 412 |
end |
|---|
| 413 |
|
|---|
| 414 |
end |
|---|
| 415 |
|
|---|
| 416 |
end |
|---|
| 417 |
end |
|---|
| 418 |
end |
|---|