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 +4 -0
- data/.rspec +2 -0
- data/Appraisals +11 -0
- data/Gemfile +5 -1
- data/README.md +44 -10
- data/Rakefile +4 -0
- data/acts_as_ordered_tree.gemspec +12 -11
- data/lib/acts_as_ordered_tree.rb +99 -34
- data/lib/acts_as_ordered_tree/class_methods.rb +29 -0
- data/lib/acts_as_ordered_tree/fake_scope.rb +28 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +375 -0
- data/lib/acts_as_ordered_tree/validators.rb +15 -0
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/spec/acts_as_ordered_tree_spec.rb +710 -205
- data/spec/db/schema.rb +23 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/factories.rb +14 -0
- data/spec/support/matchers.rb +132 -0
- data/spec/support/models.rb +23 -0
- metadata +118 -51
- data/init.rb +0 -2
- data/lib/acts_as_ordered_tree/iterator.rb +0 -36
- data/lib/acts_as_ordered_tree/list.rb +0 -100
- data/lib/acts_as_ordered_tree/tree.rb +0 -156
- data/spec/database.yml +0 -3
- data/spec/iterator_spec.rb +0 -73
- data/spec/test_helper.rb +0 -53
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Appraisals
ADDED
data/Gemfile
CHANGED
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
|
-
|
6
|
-
|
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
|
-
|
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
|
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
|
-
|
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 =
|
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
|
-
|
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
@@ -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 =
|
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 =
|
18
|
+
s.require_paths = %w(lib)
|
19
19
|
|
20
|
-
s.add_dependency "
|
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 "
|
26
|
-
s.add_development_dependency "
|
27
|
-
s.add_development_dependency "
|
28
|
-
s.add_development_dependency "
|
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
|
data/lib/acts_as_ordered_tree.rb
CHANGED
@@ -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/
|
9
|
-
require "acts_as_ordered_tree/
|
10
|
-
require "acts_as_ordered_tree/
|
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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
24
|
-
#
|
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
|
-
|
29
|
-
|
89
|
+
# setup validations
|
90
|
+
validates_with Validators::CyclicReferenceValidator, :on => :update, :if => :parent
|
30
91
|
end # def acts_as_ordered_tree
|
31
92
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
99
|
+
def position_column
|
100
|
+
acts_as_ordered_tree_options[:position_column]
|
101
|
+
end
|
40
102
|
|
41
|
-
|
42
|
-
|
103
|
+
def depth_column
|
104
|
+
acts_as_ordered_tree_options[:depth_column] || nil
|
105
|
+
end
|
43
106
|
|
44
|
-
|
45
|
-
|
107
|
+
def children_counter_cache_column
|
108
|
+
acts_as_ordered_tree_options[:counter_cache] || nil
|
109
|
+
end
|
46
110
|
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|