acts_as_ordered_tree 1.3.1 → 2.0.0.beta3
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.
- checksums.yaml +4 -4
- data/lib/acts_as_ordered_tree.rb +22 -100
- data/lib/acts_as_ordered_tree/adapters.rb +17 -0
- data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
- data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
- data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
- data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
- data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
- data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
- data/lib/acts_as_ordered_tree/hooks.rb +38 -0
- data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
- data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
- data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
- data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
- data/lib/acts_as_ordered_tree/node.rb +78 -0
- data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
- data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
- data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
- data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
- data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
- data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
- data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
- data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
- data/lib/acts_as_ordered_tree/position.rb +143 -0
- data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
- data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
- data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
- data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
- data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
- data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
- data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
- data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
- data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
- data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
- data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
- data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
- data/lib/acts_as_ordered_tree/tree.rb +148 -0
- data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
- data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
- data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
- data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
- data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
- data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
- data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
- data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
- data/lib/acts_as_ordered_tree/validators.rb +1 -1
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/spec/acts_as_ordered_tree_spec.rb +80 -909
- data/spec/adapters/postgresql_spec.rb +14 -0
- data/spec/adapters/recursive_spec.rb +12 -0
- data/spec/adapters/shared.rb +272 -0
- data/spec/callbacks_spec.rb +177 -0
- data/spec/counter_cache_spec.rb +31 -0
- data/spec/create_spec.rb +110 -0
- data/spec/destroy_spec.rb +57 -0
- data/spec/inheritance_spec.rb +176 -0
- data/spec/move_spec.rb +94 -0
- data/spec/node/movements/concurrent_movements_spec.rb +354 -0
- data/spec/node/movements/move_higher_spec.rb +46 -0
- data/spec/node/movements/move_lower_spec.rb +46 -0
- data/spec/node/movements/move_to_child_of_spec.rb +147 -0
- data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
- data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
- data/spec/node/movements/move_to_left_of_spec.rb +120 -0
- data/spec/node/movements/move_to_right_of_spec.rb +120 -0
- data/spec/node/movements/move_to_root_spec.rb +67 -0
- data/spec/node/predicates_spec.rb +211 -0
- data/spec/node/reloading_spec.rb +42 -0
- data/spec/node/siblings_spec.rb +193 -0
- data/spec/node/traversals_spec.rb +71 -0
- data/spec/persevering_transaction_spec.rb +98 -0
- data/spec/relation/arrangeable_spec.rb +88 -0
- data/spec/relation/iterable_spec.rb +104 -0
- data/spec/relation/preloaded_spec.rb +57 -0
- data/spec/reorder_spec.rb +83 -0
- data/spec/spec_helper.rb +30 -38
- data/spec/support/db/boot.rb +22 -0
- data/spec/{db → support/db}/config.travis.yml +2 -0
- data/spec/{db → support/db}/config.yml +1 -0
- data/spec/{db → support/db}/schema.rb +9 -0
- data/spec/support/factories.rb +2 -2
- data/spec/support/matchers.rb +67 -58
- data/spec/support/models.rb +6 -14
- data/spec/support/tree_factory.rb +315 -0
- data/spec/tree/children_association_spec.rb +72 -0
- data/spec/tree/columns_spec.rb +65 -0
- data/spec/tree/scopes_spec.rb +39 -0
- metadata +161 -43
- data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
- data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
- data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
- data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
- data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
- data/spec/concurrency_support_spec.rb +0 -156
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/compatibility/features'
|
4
|
+
|
5
|
+
module ActsAsOrderedTree
|
6
|
+
# Since we support multiple Rails versions, we need to turn on some features
|
7
|
+
# for old Rails versions.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
module Compatibility
|
11
|
+
features do
|
12
|
+
scope :active_record do
|
13
|
+
versions '< 4.0.0' do
|
14
|
+
feature :association_scope
|
15
|
+
feature :null_relation
|
16
|
+
end
|
17
|
+
|
18
|
+
feature :default_scoped, '< 4.1.0'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Scoping
|
3
|
+
module Default
|
4
|
+
module ClassMethods
|
5
|
+
# default_scoped is a new method from Rails 4.1.
|
6
|
+
# Used in RecursiveRelation
|
7
|
+
def default_scoped
|
8
|
+
scope = relation.merge(send(:build_default_scope))
|
9
|
+
scope.default_scoped = true
|
10
|
+
scope
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActsAsOrderedTree::Compatibility.version '< 3.2.0' do
|
18
|
+
ActiveRecord::Base.extend(ActiveRecord::Scoping::Default::ClassMethods)
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module NullRelation # :nodoc:
|
5
|
+
def exec_queries
|
6
|
+
@records = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_a
|
10
|
+
[]
|
11
|
+
end
|
12
|
+
|
13
|
+
def pluck(*column_names)
|
14
|
+
[]
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_all(_conditions = nil)
|
18
|
+
0
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_all(_updates, _conditions = nil, _options = {})
|
22
|
+
0
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(_id_or_array)
|
26
|
+
0
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
0
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def any?
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def many?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_sql
|
46
|
+
@to_sql ||= ""
|
47
|
+
end
|
48
|
+
|
49
|
+
def count(*)
|
50
|
+
0
|
51
|
+
end
|
52
|
+
|
53
|
+
def sum(*)
|
54
|
+
0
|
55
|
+
end
|
56
|
+
|
57
|
+
def calculate(_operation, _column_name, _options = {})
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def exists?(_id = false)
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Relation
|
67
|
+
def none
|
68
|
+
extending(NullRelation)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'tsort'
|
4
|
+
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
require 'active_support/core_ext/array/wrap'
|
7
|
+
|
8
|
+
module ActsAsOrderedTree
|
9
|
+
module Compatibility
|
10
|
+
UnknownFeature = Class.new(StandardError)
|
11
|
+
|
12
|
+
class Feature
|
13
|
+
class Version
|
14
|
+
def initialize(operator, version = nil)
|
15
|
+
operator, version = operator.split unless version
|
16
|
+
operator, version = '=', operator unless version
|
17
|
+
operator = '==' if operator == '='
|
18
|
+
|
19
|
+
@operator, @version = operator, version.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def matches?
|
23
|
+
ActiveRecord::VERSION::STRING.send(@operator, @version)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
[@operator, @version].join(' ')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :name, :versions, :prerequisites
|
32
|
+
|
33
|
+
def initialize(name, versions, prerequisites)
|
34
|
+
@name = name.to_s
|
35
|
+
@versions = Array.wrap(versions).map { |v| Version.new(v) }
|
36
|
+
@prerequisites = Array.wrap(prerequisites)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Requires dependency
|
40
|
+
def require
|
41
|
+
Kernel.require(path) if @versions.all?(&:matches?)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def path
|
46
|
+
"acts_as_ordered_tree/compatibility/#{name}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class DependencyTree
|
51
|
+
include TSort
|
52
|
+
|
53
|
+
def initialize
|
54
|
+
@features = Hash.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def require
|
58
|
+
@features.each_value(&:require)
|
59
|
+
end
|
60
|
+
|
61
|
+
def <<(feature)
|
62
|
+
feature.prerequisites.each do |pre|
|
63
|
+
unless @features.key?(pre.to_s)
|
64
|
+
@features[pre.to_s] = Feature.new(pre, feature.versions.map(&:to_s), [])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
@features[feature.name] = feature
|
69
|
+
end
|
70
|
+
|
71
|
+
def [](name)
|
72
|
+
@features[name.to_s]
|
73
|
+
end
|
74
|
+
|
75
|
+
def each_dependency(name, &block)
|
76
|
+
raise UnknownFeature, "Unknown compatibility feature #{name}" unless @features.key?(name.to_s)
|
77
|
+
|
78
|
+
each_strongly_connected_component_from(name.to_s, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def tsort_each_node(&block)
|
83
|
+
@features.each_key(&block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def tsort_each_child(node, &block)
|
87
|
+
@features[node].prerequisites.each(&block) if @features[node]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class DependencyTreeBuilder
|
92
|
+
attr_reader :tree
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@tree = DependencyTree.new
|
96
|
+
@default_versions = nil
|
97
|
+
@prerequisites = []
|
98
|
+
@scope = ''
|
99
|
+
end
|
100
|
+
|
101
|
+
def versions(*versions, &block)
|
102
|
+
@default_versions = versions
|
103
|
+
|
104
|
+
instance_eval(&block)
|
105
|
+
ensure
|
106
|
+
@default_versions = nil
|
107
|
+
end
|
108
|
+
alias_method :version, :versions
|
109
|
+
|
110
|
+
def scope(name, &block)
|
111
|
+
if name.is_a?(Hash)
|
112
|
+
@scope = name.keys.first.to_s
|
113
|
+
@prerequisites = Array.wrap(name.values.first)
|
114
|
+
else
|
115
|
+
@scope = name.to_s
|
116
|
+
end
|
117
|
+
|
118
|
+
instance_eval(&block)
|
119
|
+
ensure
|
120
|
+
@scope = ''
|
121
|
+
@prerequisites = []
|
122
|
+
end
|
123
|
+
|
124
|
+
def feature(name, options = {})
|
125
|
+
@tree << if name.is_a?(Hash)
|
126
|
+
version = name.delete(:versions) || @default_versions
|
127
|
+
name, prereq = *name.first
|
128
|
+
|
129
|
+
prereq = @prerequisites + Array.wrap(prereq).map { |x| [@scope, x].join('/') }
|
130
|
+
|
131
|
+
Feature.new([@scope, name].join('/'), version, prereq)
|
132
|
+
else
|
133
|
+
version = options.is_a?(Hash) ? options.delete(:versions) : options
|
134
|
+
Feature.new([@scope, name].join('/'), version || @default_versions, @prerequisites)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
module DSL
|
140
|
+
def features(&block)
|
141
|
+
builder = DependencyTreeBuilder.new
|
142
|
+
builder.instance_eval(&block)
|
143
|
+
builder.tree.require
|
144
|
+
end
|
145
|
+
|
146
|
+
def version(*versions)
|
147
|
+
versions = versions.map { |v| Feature::Version.new(*v) }
|
148
|
+
yield if versions.all?(&:matches?)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
extend DSL
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ActsAsOrderedTree
|
4
|
+
# @api private
|
5
|
+
module Deprecate
|
6
|
+
NEXT_VERSION = '2.1'
|
7
|
+
|
8
|
+
def deprecated_method(method, replacement = nil, &block)
|
9
|
+
define_method(method) do |*args, &method_block|
|
10
|
+
message = "#{self.class.name}##{__method__} is "\
|
11
|
+
"deprecated and will be removed in acts_as_ordered_tree-#{NEXT_VERSION}"
|
12
|
+
message << ", use ##{replacement} instead" if replacement
|
13
|
+
|
14
|
+
ActiveSupport::Deprecation.warn message, caller(2)
|
15
|
+
|
16
|
+
if block
|
17
|
+
instance_exec(*args, &block)
|
18
|
+
elsif replacement
|
19
|
+
__send__(replacement, *args, &method_block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
require 'acts_as_ordered_tree/hooks/update'
|
6
|
+
|
7
|
+
module ActsAsOrderedTree
|
8
|
+
# Included into AR::Base this module allows to intercept
|
9
|
+
# internal AR calls, such as +create_record+ and execute
|
10
|
+
# patched code.
|
11
|
+
#
|
12
|
+
# Hooks intention is to execute well optimized INSERTs and
|
13
|
+
# UPDATEs at certain cases.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# class Category < ActiveRecord::Base
|
17
|
+
# include ActsAsOrderedTree::Hooks
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# category.hook_update do |update|
|
21
|
+
# update.scope = category.parent.children
|
22
|
+
# update.values = {:counter => Category.arel_table[:counter] + 1}
|
23
|
+
#
|
24
|
+
# # all callbacks, including :before_save and :after_save will
|
25
|
+
# # be invoked, but patched UPDATE will be called instead of
|
26
|
+
# # original AR `ActiveRecord::Persistence#update_record`
|
27
|
+
# category.save
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
module Hooks
|
32
|
+
extend ActiveSupport::Concern
|
33
|
+
|
34
|
+
included do
|
35
|
+
include Update
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module ActsAsOrderedTree
|
6
|
+
module Hooks
|
7
|
+
# This AR-hook is used in Move transactions to update parent_id, position
|
8
|
+
# and other changed attributes using single SQL-query.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class Category < ActiveRecord::Base
|
12
|
+
# include ActsAsOrderedTree::Hooks
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# category = Category.first
|
16
|
+
# category.hook_update do |update|
|
17
|
+
# update.scope = Category.where(:parent_id => category.parent_id)
|
18
|
+
# update.values = { :name => Arel.sql('CASE WHEN parent_id IS NULL THEN name ELSE name || name END') }
|
19
|
+
#
|
20
|
+
# # `update.update!` will be called instead of usual `AR::Persistence#update`
|
21
|
+
# record.save
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
module Update
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
|
28
|
+
included do
|
29
|
+
attr_accessor :__update_hook
|
30
|
+
|
31
|
+
# Since rails 4.0 :update_record is used for actual updates
|
32
|
+
# Since rails 4.0.x and 4.1.x (i really don't know which is x) :_update_record is used
|
33
|
+
method_name = [:update_record, :_update_record].detect { |m| private_method_defined?(m) } || :update
|
34
|
+
|
35
|
+
alias_method :update_without_hook, method_name
|
36
|
+
alias_method method_name, :update_with_hook
|
37
|
+
end
|
38
|
+
|
39
|
+
def hook_update
|
40
|
+
self.__update_hook = UpdateManager.new(self)
|
41
|
+
yield __update_hook
|
42
|
+
ensure
|
43
|
+
self.__update_hook = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def update_with_hook(*args)
|
48
|
+
if __update_hook
|
49
|
+
__update_hook.update!
|
50
|
+
else
|
51
|
+
update_without_hook(*args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class UpdateManager
|
56
|
+
attr_reader :record
|
57
|
+
attr_accessor :scope, :values
|
58
|
+
|
59
|
+
def initialize(record)
|
60
|
+
@record = record
|
61
|
+
@values = {}
|
62
|
+
end
|
63
|
+
|
64
|
+
def update!
|
65
|
+
scope.update_all(to_sql)
|
66
|
+
record.reload
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def to_sql
|
71
|
+
values.keys.map do |attr|
|
72
|
+
name = attr.is_a?(Arel::Attributes::Attribute) ? attr.name : attr.to_s
|
73
|
+
|
74
|
+
quoted = record.class.connection.quote_column_name(name)
|
75
|
+
"#{quoted} = (#{value_of(attr)})"
|
76
|
+
end.join(', ')
|
77
|
+
end
|
78
|
+
|
79
|
+
def value_of(attr)
|
80
|
+
value = values[attr]
|
81
|
+
value.respond_to?(:to_sql) ? value.to_sql : record.class.connection.quote(value)
|
82
|
+
end
|
83
|
+
end # class CustomUpdate
|
84
|
+
end # module Update
|
85
|
+
end # module Hooks
|
86
|
+
end # module ActsAsOrderedTree
|
@@ -1,472 +1,111 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
|
3
|
-
require 'acts_as_ordered_tree/
|
4
|
-
require 'acts_as_ordered_tree/
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/node'
|
4
|
+
require 'acts_as_ordered_tree/transaction/factory'
|
5
|
+
require 'acts_as_ordered_tree/deprecate'
|
5
6
|
|
6
7
|
module ActsAsOrderedTree
|
7
8
|
module InstanceMethods
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
#
|
68
|
-
def
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
# Returns the array of all children of the parent, except self
|
73
|
-
def siblings
|
74
|
-
self_and_siblings.where(arel[:id].not_eq(id))
|
75
|
-
end
|
76
|
-
|
77
|
-
def level
|
78
|
-
if depth_column
|
79
|
-
# cached result becomes invalid when parent is changed
|
80
|
-
if new_record? ||
|
81
|
-
changed_attributes.include?(parent_column.to_s) ||
|
82
|
-
self[depth_column].blank?
|
83
|
-
self[depth_column] = compute_level
|
84
|
-
else
|
85
|
-
self[depth_column]
|
86
|
-
end
|
87
|
-
else
|
88
|
-
compute_level
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Returns a set of all of its children and nested children.
|
93
|
-
# A little bit tricky. use RDBMS with recursive queries support (PostgreSQL)
|
94
|
-
def descendants
|
95
|
-
records = fetch_self_and_descendants - [self]
|
96
|
-
|
97
|
-
ActsAsOrderedTree::Relation::Preloaded.new(self.class).
|
98
|
-
where(:id => records.map(&:id).compact).
|
99
|
-
extending(Arrangeable).
|
100
|
-
records(records)
|
9
|
+
extend Deprecate
|
10
|
+
|
11
|
+
delegate :root?,
|
12
|
+
:leaf?,
|
13
|
+
:has_children?,
|
14
|
+
:has_parent?,
|
15
|
+
:first?,
|
16
|
+
:last?,
|
17
|
+
:is_descendant_of?,
|
18
|
+
:is_or_is_descendant_of?,
|
19
|
+
:is_ancestor_of?,
|
20
|
+
:is_or_is_ancestor_of?,
|
21
|
+
:to => :ordered_tree_node
|
22
|
+
|
23
|
+
delegate :move_to_root,
|
24
|
+
:move_higher,
|
25
|
+
:move_left,
|
26
|
+
:move_lower,
|
27
|
+
:move_right,
|
28
|
+
:move_to_above_of,
|
29
|
+
:move_to_left_of,
|
30
|
+
:move_to_bottom_of,
|
31
|
+
:move_to_right_of,
|
32
|
+
:move_to_child_of,
|
33
|
+
:move_to_child_with_index,
|
34
|
+
:move_to_child_with_position,
|
35
|
+
:to => :ordered_tree_node
|
36
|
+
|
37
|
+
delegate :ancestors,
|
38
|
+
:self_and_ancestors,
|
39
|
+
:descendants,
|
40
|
+
:self_and_descendants,
|
41
|
+
:root,
|
42
|
+
:siblings,
|
43
|
+
:self_and_siblings,
|
44
|
+
:left_siblings,
|
45
|
+
:higher_items,
|
46
|
+
:left_sibling,
|
47
|
+
:higher_item,
|
48
|
+
:left_sibling=,
|
49
|
+
:left_sibling_id=,
|
50
|
+
:higher_item=,
|
51
|
+
:higher_item_id=,
|
52
|
+
:right_siblings,
|
53
|
+
:lower_items,
|
54
|
+
:right_sibling,
|
55
|
+
:lower_item,
|
56
|
+
:right_sibling=,
|
57
|
+
:right_sibling_id=,
|
58
|
+
:lower_item=,
|
59
|
+
:lower_item_id=,
|
60
|
+
:to => :ordered_tree_node
|
61
|
+
|
62
|
+
delegate :level, :to => :ordered_tree_node
|
63
|
+
|
64
|
+
# Returns ordered tree node - an object which maintains tree integrity.
|
65
|
+
# WARNING: THIS METHOD IS NOT THREAD SAFE!
|
66
|
+
# Though I'm not sure if it can cause any problems.
|
67
|
+
#
|
68
|
+
# @return [ActsAsOrderedTree::Node]
|
69
|
+
def ordered_tree_node
|
70
|
+
@ordered_tree_node ||= ActsAsOrderedTree::Node.new(self)
|
101
71
|
end
|
102
72
|
|
103
|
-
# Returns a set of itself and all of its nested children
|
104
|
-
def self_and_descendants
|
105
|
-
records = fetch_self_and_descendants
|
106
|
-
|
107
|
-
ActsAsOrderedTree::Relation::Preloaded.new(self.class).
|
108
|
-
where(:id => records.map(&:id)).
|
109
|
-
extending(Arrangeable).
|
110
|
-
records(records)
|
111
|
-
end
|
112
|
-
|
113
|
-
def is_descendant_of?(other)
|
114
|
-
ancestors.include? other
|
115
|
-
end
|
116
|
-
|
117
|
-
def is_or_is_descendant_of?(other)
|
118
|
-
self == other || is_descendant_of?(other)
|
119
|
-
end
|
120
|
-
|
121
|
-
def is_ancestor_of?(other)
|
122
|
-
other.is_descendant_of? self
|
123
|
-
end
|
124
|
-
|
125
|
-
def is_or_is_ancestor_of?(other)
|
126
|
-
other.is_or_is_descendant_of? self
|
127
|
-
end
|
128
|
-
|
129
|
-
# Return +true+ if this object is the first in the list.
|
130
|
-
def first?
|
131
|
-
self[position_column] <= 1
|
132
|
-
end
|
133
|
-
|
134
|
-
# Return +true+ if this object is the last in the list.
|
135
|
-
def last?
|
136
|
-
!right_sibling
|
137
|
-
end
|
138
|
-
|
139
|
-
# Returns a left (upper) sibling of the node
|
140
|
-
def left_sibling
|
141
|
-
siblings.
|
142
|
-
where( arel[position_column].lt(self[position_column]) ).
|
143
|
-
reorder( arel[position_column].desc ).
|
144
|
-
first
|
145
|
-
end
|
146
|
-
alias higher_item left_sibling
|
147
|
-
|
148
|
-
# Returns a right (lower) sibling of the node
|
149
|
-
def right_sibling
|
150
|
-
siblings.
|
151
|
-
where( arel[position_column].gt(self[position_column]) ).
|
152
|
-
reorder( arel[position_column].asc ).
|
153
|
-
first
|
154
|
-
end
|
155
|
-
alias lower_item right_sibling
|
156
|
-
|
157
73
|
# Insert the item at the given position (defaults to the top position of 1).
|
158
|
-
# +acts_as_list+
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
# Shorthand method for finding the left sibling and moving to the left of it.
|
164
|
-
def move_left
|
165
|
-
tenacious_transaction do
|
166
|
-
move_to_left_of left_sibling.try(:lock!)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
alias move_higher move_left
|
170
|
-
|
171
|
-
# Shorthand method for finding the right sibling and moving to the right of it.
|
172
|
-
def move_right
|
173
|
-
tenacious_transaction do
|
174
|
-
move_to_right_of right_sibling.try(:lock!)
|
175
|
-
end
|
176
|
-
end
|
177
|
-
alias move_lower move_right
|
178
|
-
|
179
|
-
# Move the node to the left of another node
|
180
|
-
def move_to_left_of(node)
|
181
|
-
move_to node, :left
|
182
|
-
end
|
183
|
-
alias move_to_above_of move_to_left_of
|
184
|
-
|
185
|
-
# Move the node to the left of another node
|
186
|
-
def move_to_right_of(node)
|
187
|
-
move_to node, :right
|
74
|
+
# +acts_as_list+ compatibility
|
75
|
+
#
|
76
|
+
# @deprecated
|
77
|
+
deprecated_method :insert_at, :move_to_child_with_position do |position = 1|
|
78
|
+
move_to_child_with_position(parent, position)
|
188
79
|
end
|
189
|
-
alias move_to_bottom_of move_to_right_of
|
190
80
|
|
191
|
-
#
|
192
|
-
|
193
|
-
|
81
|
+
# Returns +true+ if it is possible to move node to left/right/child of +target+.
|
82
|
+
#
|
83
|
+
# @param [ActiveRecord::Base] target
|
84
|
+
# @deprecated
|
85
|
+
deprecated_method :move_possible? do |target|
|
86
|
+
ordered_tree_node.same_scope?(target) &&
|
87
|
+
!ordered_tree_node.is_or_is_ancestor_of?(target)
|
194
88
|
end
|
195
89
|
|
196
|
-
#
|
197
|
-
|
198
|
-
|
90
|
+
# Returns true if node contains any children.
|
91
|
+
#
|
92
|
+
# @deprecated
|
93
|
+
deprecated_method :branch?, :has_children?
|
199
94
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
reject { |root_node| root_node == self }
|
205
|
-
|
206
|
-
if new_siblings.empty?
|
207
|
-
node ? move_to_child_of(node) : move_to_root
|
208
|
-
elsif new_siblings.count <= index
|
209
|
-
move_to_right_of(new_siblings.last)
|
210
|
-
elsif
|
211
|
-
index >= 0 ? move_to_left_of(new_siblings[index]) : move_to_right_of(new_siblings[index])
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
# Move the node to root nodes
|
217
|
-
def move_to_root
|
218
|
-
move_to nil, :root
|
219
|
-
end
|
220
|
-
|
221
|
-
# Returns +true+ it is possible to move node to left/right/child of +target+
|
222
|
-
def move_possible?(target)
|
223
|
-
same_scope?(target) && !is_or_is_ancestor_of?(target)
|
224
|
-
end
|
225
|
-
|
226
|
-
# Check if other model is in the same scope
|
227
|
-
def same_scope?(other)
|
228
|
-
scope_column_names.empty? || scope_column_names.all? do |attr|
|
229
|
-
self[attr] == other[attr]
|
230
|
-
end
|
231
|
-
end
|
95
|
+
# Returns true is node is not a root node.
|
96
|
+
#
|
97
|
+
# @deprecated
|
98
|
+
deprecated_method :child?, :has_parent?
|
232
99
|
|
233
100
|
private
|
234
|
-
|
235
|
-
def
|
236
|
-
|
237
|
-
end
|
238
|
-
|
239
|
-
def compute_ordered_tree_columns(target, pos) #:nodoc:
|
240
|
-
position_was = send("#{position_column}_was")
|
241
|
-
|
242
|
-
case pos
|
243
|
-
when :root then
|
244
|
-
parent_id = nil
|
245
|
-
position = if root? && self[position_column]
|
246
|
-
# already root node
|
247
|
-
self[position_column]
|
248
|
-
else
|
249
|
-
ordered_tree_scope.roots.maximum(position_column).try(:succ) || 1
|
250
|
-
end
|
251
|
-
depth = 0
|
252
|
-
when :left then
|
253
|
-
parent_id = target[parent_column]
|
254
|
-
position = target[position_column]
|
255
|
-
position -= 1 if position_was && target[parent_column] == send("#{parent_column}_was") && target[position_column] > position_was # right
|
256
|
-
depth = target.level
|
257
|
-
when :right then
|
258
|
-
parent_id = target[parent_column]
|
259
|
-
position = target[position_column]
|
260
|
-
position += 1 if target[parent_column] != send("#{parent_column}_was") || position_was.blank? || target[position_column] < position_was # left
|
261
|
-
depth = target.level
|
262
|
-
when :child then
|
263
|
-
parent_id = target.id
|
264
|
-
position = if self[parent_column] == parent_id && self[position_column]
|
265
|
-
# already child of target node
|
266
|
-
self[position_column]
|
267
|
-
else
|
268
|
-
# lock should be obtained on target
|
269
|
-
target.children.maximum(position_column).try(:succ) || 1
|
270
|
-
end
|
271
|
-
depth = target.level + 1
|
272
|
-
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{pos}' received)."
|
273
|
-
end
|
274
|
-
return parent_id, position, depth
|
275
|
-
end
|
276
|
-
|
277
|
-
# This method do real node movements
|
278
|
-
def move_to(target, pos) #:nodoc:
|
279
|
-
tenacious_transaction do
|
280
|
-
if pos != :root && target
|
281
|
-
# load object if node is not an object
|
282
|
-
target = self.class.lock(true).find(target)
|
283
|
-
elsif pos == :root
|
284
|
-
# Obtain lock on all root nodes
|
285
|
-
ordered_tree_scope.
|
286
|
-
roots.
|
287
|
-
lock(true).
|
288
|
-
reload
|
289
|
-
end
|
290
|
-
|
291
|
-
unless pos == :root || target && move_possible?(target)
|
292
|
-
raise ActiveRecord::ActiveRecordError, "Impossible move"
|
293
|
-
end
|
294
|
-
|
295
|
-
position_was = send("#{position_column}_was")
|
296
|
-
parent_id_was = send("#{parent_column}_was")
|
297
|
-
parent_id, position, depth = compute_ordered_tree_columns(target, pos)
|
298
|
-
|
299
|
-
# nothing changed - quit
|
300
|
-
return if parent_id == parent_id_was && position == position_was
|
301
|
-
|
302
|
-
self.class.lock(true).find(self)
|
303
|
-
self[position_column], self[parent_column] = position, parent_id
|
304
|
-
|
305
|
-
move_kind = case
|
306
|
-
when id_was && parent_id != parent_id_was then :move
|
307
|
-
when id_was && position != position_was then :reorder
|
308
|
-
else nil
|
309
|
-
end
|
310
|
-
|
311
|
-
update = proc do
|
312
|
-
if move_kind == :move
|
313
|
-
move!(id, parent_id_was, parent_id, position_was, position, depth)
|
314
|
-
else
|
315
|
-
reorder!(parent_id, position_was, position)
|
316
|
-
end
|
317
|
-
|
318
|
-
reload
|
319
|
-
end
|
320
|
-
|
321
|
-
if move_kind
|
322
|
-
run_callbacks move_kind, &update
|
323
|
-
else
|
324
|
-
update.call
|
325
|
-
end
|
326
|
-
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
def decrement_lower_positions(parent_id, position) #:nodoc:
|
331
|
-
conditions = arel[parent_column].eq(parent_id).and(arel[position_column].gt(position))
|
332
|
-
|
333
|
-
ordered_tree_scope.where(conditions).update_all("#{position_column} = #{position_column} - 1")
|
334
|
-
end
|
335
|
-
|
336
|
-
# Internal
|
337
|
-
def move!(id, parent_id_was, parent_id, position_was, position, depth) #:nodoc:
|
338
|
-
pk = self.class.primary_key
|
339
|
-
|
340
|
-
assignments = [
|
341
|
-
"#{parent_column} = CASE " +
|
342
|
-
"WHEN #{pk} = :id " +
|
343
|
-
"THEN :parent_id " +
|
344
|
-
"ELSE #{parent_column} " +
|
345
|
-
"END",
|
346
|
-
"#{position_column} = CASE " +
|
347
|
-
# set new position
|
348
|
-
"WHEN #{pk} = :id " +
|
349
|
-
"THEN :position " +
|
350
|
-
# decrement lower positions within old parent
|
351
|
-
"WHEN #{parent_column} #{parent_id_was.nil? ? " IS NULL" : " = :parent_id_was"} AND #{position_column} > :position_was " +
|
352
|
-
"THEN #{position_column} - 1 " +
|
353
|
-
# increment lower positions within new parent
|
354
|
-
"WHEN #{parent_column} #{parent_id.nil? ? "IS NULL" : " = :parent_id"} AND #{position_column} >= :position " +
|
355
|
-
"THEN #{position_column} + 1 " +
|
356
|
-
"ELSE #{position_column} " +
|
357
|
-
"END",
|
358
|
-
("#{depth_column} = CASE " +
|
359
|
-
"WHEN #{pk} = :id " +
|
360
|
-
"THEN :depth " +
|
361
|
-
"ELSE #{depth_column} " +
|
362
|
-
"END" if depth_column)
|
363
|
-
]
|
364
|
-
|
365
|
-
conditions = arel[pk].eq(id).or(
|
366
|
-
arel[parent_column].eq(parent_id_was)
|
367
|
-
).or(
|
368
|
-
arel[parent_column].eq(parent_id)
|
369
|
-
)
|
370
|
-
|
371
|
-
binds = {:id => id,
|
372
|
-
:parent_id_was => parent_id_was,
|
373
|
-
:parent_id => parent_id,
|
374
|
-
:position_was => position_was,
|
375
|
-
:position => position,
|
376
|
-
:depth => depth}
|
377
|
-
|
378
|
-
ordered_tree_scope.where(conditions).update_all([assignments.compact.join(', '), binds])
|
379
|
-
end
|
380
|
-
|
381
|
-
# Internal
|
382
|
-
def reorder!(parent_id, position_was, position)
|
383
|
-
assignments = [
|
384
|
-
if position_was
|
385
|
-
<<-SQL
|
386
|
-
#{position_column} = CASE
|
387
|
-
WHEN #{position_column} = :position_was
|
388
|
-
THEN :position
|
389
|
-
WHEN #{position_column} <= :position AND #{position_column} > :position_was AND :position > :position_was
|
390
|
-
THEN #{position_column} - 1
|
391
|
-
WHEN #{position_column} >= :position AND #{position_column} < :position_was AND :position < :position_was
|
392
|
-
THEN #{position_column} + 1
|
393
|
-
ELSE #{position_column}
|
394
|
-
END
|
395
|
-
SQL
|
396
|
-
else
|
397
|
-
<<-SQL
|
398
|
-
#{position_column} = CASE
|
399
|
-
WHEN #{position_column} > :position
|
400
|
-
THEN #{position_column} + 1
|
401
|
-
WHEN #{position_column} IS NULL
|
402
|
-
THEN :position
|
403
|
-
ELSE #{position_column}
|
404
|
-
END
|
405
|
-
SQL
|
406
|
-
end
|
407
|
-
]
|
408
|
-
|
409
|
-
conditions = arel[parent_column].eq(parent_id)
|
410
|
-
binds = {:id => id, :position_was => position_was, :position => position}
|
411
|
-
|
412
|
-
ordered_tree_scope.where(conditions).update_all([assignments.compact.join(', '), binds])
|
413
|
-
end
|
414
|
-
|
415
|
-
# recursively load descendants
|
416
|
-
def fetch_self_and_descendants #:nodoc:
|
417
|
-
@self_and_descendants ||= [self] + children.map { |child| [child, child.descendants] }.flatten
|
418
|
-
end
|
419
|
-
|
420
|
-
def set_depth! #:nodoc:
|
421
|
-
self[depth_column] = compute_level
|
422
|
-
end
|
423
|
-
|
424
|
-
def set_scope! #:nodoc:
|
425
|
-
scope_column_names.each do |column|
|
426
|
-
self[column] = parent[column]
|
427
|
-
end
|
428
|
-
end
|
429
|
-
|
430
|
-
def flush_descendants #:nodoc:
|
431
|
-
@self_and_descendants = nil
|
432
|
-
end
|
433
|
-
|
434
|
-
def update_descendants_depth #:nodoc:
|
435
|
-
depth_was = send("#{depth_column}_was")
|
436
|
-
|
437
|
-
yield
|
438
|
-
|
439
|
-
diff = self[depth_column] - depth_was
|
440
|
-
if diff != 0
|
441
|
-
sign = diff > 0 ? "+" : "-"
|
442
|
-
# update categories set depth = depth - 1 where id in (...)
|
443
|
-
descendants.update_all(["#{depth_column} = #{depth_column} #{sign} ?", diff.abs]) if descendants.count > 0
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
# Used in built-in around_move routine
|
448
|
-
def update_counter_cache #:nodoc:
|
449
|
-
parent_id_was = send "#{parent_column}_was"
|
450
|
-
|
451
|
-
yield
|
452
|
-
|
453
|
-
parent_id_new = self[parent_column]
|
454
|
-
unless parent_id_new == parent_id_was
|
455
|
-
self.class.increment_counter(children_counter_cache_column, parent_id_new) if parent_id_new
|
456
|
-
self.class.decrement_counter(children_counter_cache_column, parent_id_was) if parent_id_was
|
457
|
-
end
|
458
|
-
end
|
459
|
-
|
460
|
-
def arel #:nodoc:
|
461
|
-
self.class.arel_table
|
101
|
+
# Around callback that starts ActsAsOrderedTree::Transaction
|
102
|
+
def save_ordered_tree_node(&block)
|
103
|
+
Transaction::Factory.create(ordered_tree_node).start(&block)
|
462
104
|
end
|
463
105
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
else
|
468
|
-
self.class.base_class.where Hash[scope_column_names.map { |column| [column, self[column]] }]
|
469
|
-
end
|
106
|
+
# Around callback that starts ActsAsOrderedTree::Transaction
|
107
|
+
def destroy_ordered_tree_node(&block)
|
108
|
+
Transaction::Factory.create(ordered_tree_node, true).start(&block)
|
470
109
|
end
|
471
110
|
end # module InstanceMethods
|
472
111
|
end # module ActsAsOrderedTree
|