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 +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
|