acts_as_ordered_tree 0.0.7 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -2,3 +2,7 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ .idea/*
6
+ coverage/*
7
+ .rbx/*
8
+ gemfiles/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,11 @@
1
+ appraise "rails3.0" do
2
+ gem "activerecord", "~> 3.0.0"
3
+ end
4
+
5
+ appraise "rails3.1" do
6
+ gem "activerecord", "~> 3.1.0"
7
+ end
8
+
9
+ appraise "rails3.2" do
10
+ gem "activerecord", "~> 3.2.0"
11
+ end
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in acts_as_ordered_tree.gemspec
4
- gemspec
4
+ gemspec
5
+
6
+ gem "sqlite3", :platforms => :ruby
7
+ gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
8
+ gem "simplecov", :platform => :ruby_19
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Acts As Ordered Tree
2
2
  WARNING! THIS GEM IS NOT COMPATIBLE WITH <a href="http://ordered-tree.rubyforge.org">ordered_tree gem</a>.
3
3
 
4
- Specify this `acts_as` extension if you want to model an ordered tree structure by providing a parent association, a children
5
- association and a sort column. For proper use you should have a foreign key column, which by default is called `parent_id`, and
6
- a sort column, which by default is called `position`.
4
+ Specify this `acts_as` extension if you want to model an ordered tree structure ([adjacency list hierarchical structure](http://www.sqlsummit.com/AdjacencyList.htm)) by providing a parent association, a children association and a sort column. For proper use you should have a foreign key column, which by default is called `parent_id`, and a sort column, which is by default called `position`.
5
+
6
+ This extension is mostly compatible with [`awesome_nested_set`](https://github.com/collectiveidea/awesome_nested_set/) gem
7
7
 
8
8
  ## Requirements
9
- Gem depends on `active_record >= 3`.
9
+ Gem depends on `active_record >= 3`. We test it with `rails-3.0`, `rails-3.1`, `rails-3.2` and with `ruby-1.9.3`, `ruby-1.9.2`, `ruby-1.8.7`, `jruby-1.6.7` and `rubinius-2.0`.
10
10
 
11
11
  ## Installation
12
12
  Install it via rubygems:
@@ -15,12 +15,32 @@ Install it via rubygems:
15
15
  gem install acts_as_ordered_tree
16
16
  ```
17
17
 
18
- Gem depends on `acts_as_tree` and `acts_as_list` gems.
18
+ ## Usage
19
+
20
+ To make use of `acts_as_ordered_tree`, your model needs to have 2 fields: parent_id and position. You can also have an optional fields: depth and children_count:
21
+ ```ruby
22
+ class CreateCategories < ActiveRecord::Migration
23
+ def self.up
24
+ create_table :categories do |t|
25
+ t.integer :company_id
26
+ t.string :name
27
+ t.integer :parent_id # this is mandatory
28
+ t.integer :position # this is mandatory
29
+ t.integer :depth # this is optional
30
+ t.integer :children_count # this is optional
31
+ end
32
+ end
33
+
34
+ def self.down
35
+ drop_table :categories
36
+ end
37
+ end
38
+ ```
19
39
 
20
40
  Setup your model:
21
41
 
22
42
  ```ruby
23
- class Node < ActiveRecord::Base
43
+ class Category < ActiveRecord::Base
24
44
  acts_as_ordered_tree
25
45
 
26
46
  # gem introduces new ActiveRecord callbacks:
@@ -31,7 +51,8 @@ class Node < ActiveRecord::Base
31
51
  end
32
52
  ```
33
53
 
34
- ## Example
54
+ Now you can use `acts_as_ordered_tree` features:
55
+
35
56
  ```ruby
36
57
  # root
37
58
  # \_ child1
@@ -39,12 +60,12 @@ end
39
60
  # \_ subchild2
40
61
 
41
62
 
42
- root = Node.create(:name => "root")
63
+ root = Category.create(:name => "root")
43
64
  child1 = root.children.create(:name => "child1")
44
65
  subchild1 = child1.children.create("name" => "subchild1")
45
66
  subchild2 = child1.children.create("name" => "subchild2")
46
67
 
47
- Node.roots # => [root]
68
+ Category.roots # => [root]
48
69
 
49
70
  root.root? # => true
50
71
  root.parent # => nil
@@ -70,4 +91,17 @@ subchild1.move_to_bottom_of(child1)
70
91
  subchild1.move_to_child_of(root)
71
92
  subchild1.move_lower
72
93
  subchild1.move_higher
73
- ```
94
+ ```
95
+
96
+ ## Contributing
97
+
98
+ 1. Fork it
99
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
100
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
101
+ 4. Push to the branch (`git push origin my-new-feature`)
102
+ 5. Create new Pull Request
103
+
104
+ ## TODO
105
+ 1. Fix README typos and grammatical errors (english speaking contributors are welcomed)
106
+ 2. Add moar examples and docs.
107
+ 3. Implement converter from other structures (nested_set, closure_tree)
data/Rakefile CHANGED
@@ -1 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "appraisal"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
@@ -5,8 +5,8 @@ require "acts_as_ordered_tree/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "acts_as_ordered_tree"
7
7
  s.version = ActsAsOrderedTree::VERSION
8
- s.authors = ["Alexei Mikhailov"]
9
- s.email = ["amikhailov83@gmail.com"]
8
+ s.authors = ["Alexei Mikhailov", "Vladimir Kuznetsov"]
9
+ s.email = %w(amikhailov83@gmail.com kv86@mail.ru)
10
10
  s.homepage = "https://github.com/take-five/acts_as_ordered_tree"
11
11
  s.summary = %q{ActiveRecord extension for sorted adjacency lists support}
12
12
 
@@ -15,15 +15,16 @@ Gem::Specification.new do |s|
15
15
  s.files = `git ls-files`.split("\n")
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
- s.require_paths = ["lib"]
18
+ s.require_paths = %w(lib)
19
19
 
20
- s.add_dependency "activesupport", "~> 3"
21
- s.add_dependency "activerecord", "~> 3"
22
- s.add_dependency "acts_as_tree", "~> 0.1"
23
- s.add_dependency "acts_as_list", "~> 0.1"
20
+ s.add_dependency "activerecord", ">= 3.0.0"
24
21
 
25
- s.add_development_dependency "rspec"
26
- s.add_development_dependency "simplecov"
27
- s.add_development_dependency "sqlite3"
28
- s.add_development_dependency "bundler"
22
+ s.add_development_dependency "bundler", ">= 1.0"
23
+ s.add_development_dependency "rails", ">= 3.0.0"
24
+ s.add_development_dependency "rspec", ">= 2.11"
25
+ s.add_development_dependency "rspec-rails", ">= 2.11"
26
+ s.add_development_dependency "shoulda-matchers", ">= 1.2.0"
27
+ s.add_development_dependency "factory_girl", "< 3"
28
+ s.add_development_dependency "factory_girl_rails", "< 3"
29
+ s.add_development_dependency "appraisal", ">= 0.4.0"
29
30
  end
@@ -1,52 +1,117 @@
1
- require "enumerator"
2
-
3
1
  require "active_record"
4
- require "acts_as_list"
5
- require "acts_as_tree"
6
-
7
2
  require "acts_as_ordered_tree/version"
8
- require "acts_as_ordered_tree/iterator"
9
- require "acts_as_ordered_tree/tree"
10
- require "acts_as_ordered_tree/list"
3
+ require "acts_as_ordered_tree/class_methods"
4
+ require "acts_as_ordered_tree/fake_scope"
5
+ require "acts_as_ordered_tree/instance_methods"
6
+ require "acts_as_ordered_tree/validators"
11
7
 
12
8
  module ActsAsOrderedTree
9
+ # == Usage
10
+ # class Category < ActiveRecord::Base
11
+ # acts_as_ordered_tree :parent_column => :parent_id,
12
+ # :position_column => :position,
13
+ # :depth_column => :depth,
14
+ # :counter_cache => :children_count
15
+ # end
13
16
  def acts_as_ordered_tree(options = {})
14
- configuration = configure_ordered_tree(options)
17
+ options = {
18
+ :parent_column => :parent_id,
19
+ :position_column => :position,
20
+ :depth_column => :depth
21
+ }.merge(options)
22
+
23
+ class_attribute :acts_as_ordered_tree_options, :instance_writer => false
24
+ self.acts_as_ordered_tree_options = options
25
+
26
+ acts_as_ordered_tree_options[:depth_column] = nil unless
27
+ columns_hash.include?(acts_as_ordered_tree_options[:depth_column].to_s)
28
+
29
+ extend Columns
30
+ include Columns
31
+
32
+ has_many_children_options = {
33
+ :class_name => name,
34
+ :foreign_key => options[:parent_column],
35
+ :order => options[:position_column],
36
+ :inverse_of => (:parent unless options[:polymorphic])
37
+ }
38
+
39
+ [:before_add, :after_add, :before_remove, :after_remove].each do |callback|
40
+ has_many_children_options[callback] = options[callback] if options.key?(callback)
41
+ end
42
+
43
+ if scope_column_names.any?
44
+ has_many_children_options[:conditions] = proc do
45
+ [scope_column_names.map { |c| "#{c} = ?" }.join(' AND '),
46
+ scope_column_names.map { |c| self[c] }]
47
+ end
48
+ end
49
+
50
+ # create associations
51
+ has_many :children, has_many_children_options
52
+ belongs_to :parent,
53
+ :class_name => name,
54
+ :foreign_key => options[:parent_column],
55
+ :counter_cache => options[:counter_cache],
56
+ :inverse_of => (:children unless options[:polymorphic])
57
+
58
+ define_model_callbacks :move, :reorder
59
+
60
+ include ClassMethods
61
+ include InstanceMethods
62
+
63
+ # protect position&depth from mass-assignment
64
+ attr_protected depth_column, position_column
65
+
66
+ if depth_column
67
+ before_create :set_depth!
68
+ before_save :set_depth!, :if => "#{parent_column}_changed?".to_sym
69
+ around_move :update_descendants_depth
70
+ end
71
+
72
+ if children_counter_cache_column
73
+ around_move :update_counter_cache
74
+ end
15
75
 
16
- acts_as_tree :foreign_key => parent_column,
17
- :order => position_column,
18
- :counter_cache => configuration[:counter_cache]
76
+ unless scope_column_names.empty?
77
+ before_save :set_scope!, :unless => :root?
78
+ validates_with Validators::ScopeValidator, :on => :update, :unless => :root?
79
+ end
19
80
 
20
- acts_as_list :column => position_column,
21
- :scope => parent_column
81
+ after_save :move_to_root, :unless => [position_column, parent_column]
82
+ after_save 'move_to_child_of(parent)', :if => parent_column, :unless => position_column
83
+ after_save "move_to_child_with_index(parent, #{position_column})",
84
+ :if => "#{position_column} && (#{position_column}_changed? || #{parent_column}_changed?)"
22
85
 
23
- # acts_as_tree creates ugly associations
24
- # patch them
25
- children = reflect_on_association :children
26
- children.options[:order] = quoted_position_column
86
+ before_destroy :destroy_descendants
87
+ after_destroy "decrement_lower_positions(#{parent_column}_was, #{position_column}_was)", :if => position_column
27
88
 
28
- include ActsAsOrderedTree::Tree
29
- include ActsAsOrderedTree::List
89
+ # setup validations
90
+ validates_with Validators::CyclicReferenceValidator, :on => :update, :if => :parent
30
91
  end # def acts_as_ordered_tree
31
92
 
32
- private
33
- # Add ordered_tree configuration readers
34
- def configure_ordered_tree(options = {}) #:nodoc:
35
- configuration = { :foreign_key => :parent_id ,
36
- :order => :position }
37
- configuration.update(options) if options.is_a?(Hash)
93
+ # Mixed into both classes and instances to provide easy access to the column names
94
+ module Columns
95
+ def parent_column
96
+ acts_as_ordered_tree_options[:parent_column]
97
+ end
38
98
 
39
- class_attribute :parent_column, :position_column
99
+ def position_column
100
+ acts_as_ordered_tree_options[:position_column]
101
+ end
40
102
 
41
- self.parent_column = configuration[:foreign_key].to_sym
42
- self.position_column = configuration[:order].to_sym
103
+ def depth_column
104
+ acts_as_ordered_tree_options[:depth_column] || nil
105
+ end
43
106
 
44
- configuration
45
- end # def configure_ordered_tree
107
+ def children_counter_cache_column
108
+ acts_as_ordered_tree_options[:counter_cache] || nil
109
+ end
46
110
 
47
- def quoted_position_column #:nodoc:
48
- [quoted_table_name, connection.quote_column_name(position_column)].join('.')
49
- end # def quoted_position_column
111
+ def scope_column_names
112
+ Array(acts_as_ordered_tree_options[:scope]).compact
113
+ end
114
+ end
50
115
  end # module ActsAsOrderedTree
51
116
 
52
117
  ActiveRecord::Base.extend(ActsAsOrderedTree)
@@ -0,0 +1,29 @@
1
+ module ActsAsOrderedTree
2
+ module ClassMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ scope :preorder, order(arel_table[position_column].asc)
7
+ scope :roots, where(arel_table[parent_column].eq(nil)).preorder
8
+
9
+ # add +leaves+ scope only if counter_cache column present
10
+ scope :leaves, where(arel_table[children_counter_cache_column].eq(0)) if
11
+ children_counter_cache?
12
+
13
+ # when default value for counter_cache is absent we should set it manually
14
+ before_create "self.#{children_counter_cache_column} = 0" if children_counter_cache?
15
+ end
16
+
17
+ module ClassMethods
18
+ # Returns the first root
19
+ def root
20
+ roots.first
21
+ end
22
+
23
+ private
24
+ def children_counter_cache? #:nodoc:
25
+ children_counter_cache_column && columns_hash.key?(children_counter_cache_column.to_s)
26
+ end
27
+ end # module ClassMethods
28
+ end # module ClassMethods
29
+ end # module ActsAsOrderedTree
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ module ActsAsOrderedTree
3
+ class FakeScope < ActiveRecord::Relation
4
+ # create fake relation, with loaded records
5
+ #
6
+ # == Usage
7
+ # FakeScope.new(Category.where(:id => 1), [record])
8
+ # FakeScope.new(Category, [record]) { where(:id => 1) }
9
+ # FakeScope.new(Category, [record], :where => {:id => 1}, :order => "id desc")
10
+ def initialize(relation, records, conditions = {})
11
+ relation = relation.scoped if relation.is_a?(Class)
12
+
13
+ conditions.each do |method, arg|
14
+ relation = relation.send(method, arg)
15
+ end
16
+
17
+ super(relation.klass, relation.table)
18
+
19
+ # copy instance variables from real relation
20
+ relation.instance_variables.each do |ivar|
21
+ instance_variable_set(ivar, relation.instance_variable_get(ivar))
22
+ end
23
+
24
+ @loaded = true
25
+ @records = records
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,375 @@
1
+ # coding: utf-8
2
+ module ActsAsOrderedTree
3
+ module InstanceMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ # Returns true if this is a root node.
7
+ def root?
8
+ self[parent_column].nil?
9
+ end
10
+
11
+ # Returns true if this is the end of a branch.
12
+ def leaf?
13
+ persisted? && if children_counter_cache_column
14
+ self[children_counter_cache_column] == 0
15
+ else
16
+ children.count == 0
17
+ end
18
+ end
19
+
20
+ def branch?
21
+ !leaf?
22
+ end
23
+
24
+ # Returns true is this is a child node
25
+ def child?
26
+ !root?
27
+ end
28
+
29
+ # Returns root (not really fast operation)
30
+ def root
31
+ root? ? self : parent.root
32
+ end
33
+
34
+ # Returns the array of all parents and self starting from root
35
+ def self_and_ancestors
36
+ # 1. recursively load ancestors
37
+ nodes = []
38
+ node = self
39
+
40
+ while node
41
+ nodes << node
42
+ node = node.parent
43
+ end
44
+
45
+ # 2. first ancestor is a root
46
+ nodes.reverse!
47
+
48
+ # 3. create fake scope
49
+ ActsAsOrderedTree::FakeScope.new(self.class, nodes, :where => {:id => nodes.map(&:id)})
50
+ end
51
+
52
+ # Returns the array of all parents starting from root
53
+ def ancestors
54
+ records = self_and_ancestors - [self]
55
+
56
+ scope = self_and_ancestors.where(arel[:id].not_eq(id))
57
+ ActsAsOrderedTree::FakeScope.new(scope, records)
58
+ end
59
+
60
+ # Returns the array of all children of the parent, including self
61
+ def self_and_siblings
62
+ ordered_tree_scope.where(parent_column => self[parent_column])
63
+ end
64
+
65
+ # Returns the array of all children of the parent, except self
66
+ def siblings
67
+ self_and_siblings.where(arel[:id].not_eq(id))
68
+ end
69
+
70
+ def level
71
+ if depth_column
72
+ # cached result becomes invalid when parent is changed
73
+ if new_record? ||
74
+ changed_attributes.include?(parent_column.to_s) ||
75
+ self[depth_column].blank?
76
+ self[depth_column] = compute_level
77
+ else
78
+ self[depth_column]
79
+ end
80
+ else
81
+ compute_level
82
+ end
83
+ end
84
+
85
+ # Returns a set of all of its children and nested children.
86
+ # A little bit tricky. use RDBMS with recursive queries support (PostgreSQL)
87
+ def descendants
88
+ records = fetch_self_and_descendants - [self]
89
+
90
+ ActsAsOrderedTree::FakeScope.new self.class, records, :where => {:id => records.map(&:id)}
91
+ end
92
+
93
+ # Returns a set of itself and all of its nested children
94
+ def self_and_descendants
95
+ records = fetch_self_and_descendants
96
+
97
+ ActsAsOrderedTree::FakeScope.new self.class, records, :where => {:id => records.map(&:id)}
98
+ end
99
+
100
+ def is_descendant_of?(other)
101
+ ancestors.include? other
102
+ end
103
+
104
+ def is_or_is_descendant_of?(other)
105
+ self == other || is_descendant_of?(other)
106
+ end
107
+
108
+ def is_ancestor_of?(other)
109
+ other.is_descendant_of? self
110
+ end
111
+
112
+ def is_or_is_ancestor_of?(other)
113
+ other.is_or_is_descendant_of? self
114
+ end
115
+
116
+ # Return +true+ if this object is the first in the list.
117
+ def first?
118
+ self[position_column] <= 1
119
+ end
120
+
121
+ # Return +true+ if this object is the last in the list.
122
+ def last?
123
+ !right_sibling
124
+ end
125
+
126
+ # Returns a left (upper) sibling of the node
127
+ def left_sibling
128
+ siblings.
129
+ where( arel[position_column].lt(self[position_column]) ).
130
+ reorder( arel[position_column].desc ).
131
+ first
132
+ end
133
+ alias higher_item left_sibling
134
+
135
+ # Returns a right (lower) sibling of the node
136
+ def right_sibling
137
+ siblings.
138
+ where( arel[position_column].gt(self[position_column]) ).
139
+ reorder( arel[position_column].asc ).
140
+ first
141
+ end
142
+ alias lower_item right_sibling
143
+
144
+ # Insert the item at the given position (defaults to the top position of 1).
145
+ # +acts_as_list+ compatability
146
+ def insert_at(position = 1)
147
+ move_to_child_with_index(parent, position - 1)
148
+ end
149
+
150
+ # Shorthand method for finding the left sibling and moving to the left of it.
151
+ def move_left
152
+ move_to_left_of left_sibling
153
+ end
154
+ alias move_higher move_left
155
+
156
+ # Shorthand method for finding the right sibling and moving to the right of it.
157
+ def move_right
158
+ move_to_right_of right_sibling
159
+ end
160
+ alias move_lower move_right
161
+
162
+ # Move the node to the left of another node
163
+ def move_to_left_of(node)
164
+ move_to node, :left
165
+ end
166
+ alias move_to_above_of move_to_left_of
167
+
168
+ # Move the node to the left of another node
169
+ def move_to_right_of(node)
170
+ move_to node, :right
171
+ end
172
+ alias move_to_bottom_of move_to_right_of
173
+
174
+ # Move the node to the child of another node
175
+ def move_to_child_of(node)
176
+ move_to node, :child
177
+ end
178
+
179
+ # Move the node to the child of another node with specify index
180
+ def move_to_child_with_index(node, index)
181
+ raise ActiveRecord::ActiveRecordError, "index cant be nil" unless index
182
+ new_siblings = node.try(:children) || self.class.roots.delete_if { |root_node| root_node == self }
183
+
184
+ if new_siblings.empty?
185
+ node ? move_to_child_of(node) : move_to_root
186
+ elsif new_siblings.count <= index
187
+ move_to_right_of(new_siblings.last)
188
+ elsif
189
+ index >= 0 ? move_to_left_of(new_siblings[index]) : move_to_right_of(new_siblings[index])
190
+ end
191
+ end
192
+
193
+ # Move the node to root nodes
194
+ def move_to_root
195
+ move_to nil, :root
196
+ end
197
+
198
+ # Returns +true+ it is possible to move node to left/right/child of +target+
199
+ def move_possible?(target)
200
+ same_scope?(target) && !is_or_is_ancestor_of?(target)
201
+ end
202
+
203
+ # Check if other model is in the same scope
204
+ def same_scope?(other)
205
+ scope_column_names.empty? || scope_column_names.all? do |attr|
206
+ self[attr] == other[attr]
207
+ end
208
+ end
209
+
210
+ private
211
+ # reloads relevant ordered_tree columns
212
+ def reload_node #:nodoc:
213
+ reload(
214
+ :select => [parent_column,
215
+ position_column,
216
+ depth_column,
217
+ children_counter_cache_column].compact,
218
+ :lock => true
219
+ )
220
+ end
221
+
222
+ def compute_level #:nodoc:
223
+ ancestors.count
224
+ end
225
+
226
+ def compute_ordered_tree_columns(target, pos) #:nodoc:
227
+ case pos
228
+ when :root then
229
+ parent_id = nil
230
+ position = if root? && self[position_column]
231
+ # already root node
232
+ self[position_column]
233
+ else
234
+ ordered_tree_scope.roots.maximum(position_column).try(:succ) || 1
235
+ end
236
+ depth = 0
237
+ when :left then
238
+ parent_id = target[parent_column]
239
+ position = target[position_column]
240
+ position -= 1 if target[parent_column] == send("#{parent_column}_was") && target[position_column] > position_was # right
241
+ depth = target.level
242
+ when :right then
243
+ parent_id = target[parent_column]
244
+ position = target[position_column]
245
+ position += 1 if target[parent_column] != send("#{parent_column}_was") || target[position_column] < position_was # left
246
+ depth = target.level
247
+ when :child then
248
+ parent_id = target.id
249
+ position = if self[parent_column] == parent_id && self[position_column]
250
+ # already children of target node
251
+ self[position_column]
252
+ else
253
+ target.children.maximum(position_column).try(:succ) || 1
254
+ end
255
+ depth = target.level + 1
256
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{pos}' received)."
257
+ end
258
+ return parent_id, position, depth
259
+ end
260
+
261
+ # This method do real node movements
262
+ def move_to(target, pos) #:nodoc:
263
+ if target.is_a? self.class.base_class
264
+ target.reload
265
+ elsif pos != :root && target
266
+ # load object if node is not an object
267
+ target = self.class.find(target)
268
+ end
269
+
270
+ unless pos == :root || target && move_possible?(target)
271
+ raise ActiveRecord::ActiveRecordError, "Impossible move"
272
+ end
273
+
274
+ position_was = send "#{position_column}_was".intern
275
+ parent_id_was = send "#{parent_column}_was".intern
276
+ parent_id, position, depth = compute_ordered_tree_columns(target, pos)
277
+
278
+ # nothing changed - quit
279
+ return if parent_id == parent_id_was && position == position_was
280
+
281
+ update = proc do
282
+ decrement_lower_positions parent_id_was, position_was if position_was
283
+ increment_lower_positions parent_id, position
284
+
285
+ columns = {parent_column => parent_id, position_column => position}
286
+ columns[depth_column] = depth if depth_column
287
+
288
+ ordered_tree_scope.update_all(columns, :id => id)
289
+ reload_node
290
+ end
291
+
292
+ move_kind = case
293
+ when id_was && parent_id != parent_id_was then :move
294
+ when id_was && position != position_was then :reorder
295
+ else nil
296
+ end
297
+
298
+ if move_kind
299
+ run_callbacks move_kind, &update
300
+ else
301
+ update.call
302
+ end
303
+ end
304
+
305
+ def decrement_lower_positions(parent_id, position) #:nodoc:
306
+ conditions = arel[parent_column].eq(parent_id).and(arel[position_column].gt(position))
307
+
308
+ ordered_tree_scope.update_all "#{position_column} = #{position_column} - 1", conditions
309
+ end
310
+
311
+ def increment_lower_positions(parent_id, position) #:nodoc:
312
+ conditions = arel[parent_column].eq(parent_id).and(arel[position_column].gteq(position))
313
+
314
+ ordered_tree_scope.update_all "#{position_column} = #{position_column} + 1", conditions
315
+ end
316
+
317
+ # recursively load descendants
318
+ def fetch_self_and_descendants #:nodoc:
319
+ @self_and_descendants ||= [self] + children.map { |child| [child, child.descendants] }.flatten
320
+ end
321
+
322
+ def set_depth! #:nodoc:
323
+ self[depth_column] = compute_level
324
+ end
325
+
326
+ def set_scope! #:nodoc:
327
+ scope_column_names.each do |column|
328
+ self[column] = parent[column]
329
+ end
330
+ end
331
+
332
+ def destroy_descendants #:nodoc:
333
+ descendants.delete_all
334
+ # flush memoization
335
+ @self_and_descendants = nil
336
+ end
337
+
338
+ def update_descendants_depth #:nodoc:
339
+ depth_was = self[depth_column]
340
+
341
+ yield
342
+
343
+ diff = self[depth_column] - depth_was
344
+ if diff != 0
345
+ sign = diff > 0 ? "+" : "-"
346
+ # update categories set depth = depth - 1 where id in (...)
347
+ descendants.update_all(["#{depth_column} = #{depth_column} #{sign} ?", diff.abs])
348
+ end
349
+ end
350
+
351
+ def update_counter_cache #:nodoc:
352
+ parent_id_was = self[parent_column]
353
+
354
+ yield
355
+
356
+ parent_id_new = self[parent_column]
357
+ unless parent_id_new == parent_id_was
358
+ self.class.increment_counter(children_counter_cache_column, parent_id_new) if parent_id_new
359
+ self.class.decrement_counter(children_counter_cache_column, parent_id_was) if parent_id_was
360
+ end
361
+ end
362
+
363
+ def arel #:nodoc:
364
+ self.class.arel_table
365
+ end
366
+
367
+ def ordered_tree_scope
368
+ if scope_column_names.empty?
369
+ self.class.base_class.scoped
370
+ else
371
+ self.class.base_class.where Hash[scope_column_names.map { |column| [column, self[column]] }]
372
+ end
373
+ end
374
+ end # module InstanceMethods
375
+ end # module ActsAsOrderedTree