acts_as_ordered_tree 0.0.7 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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