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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/lib/acts_as_ordered_tree.rb +22 -100
  3. data/lib/acts_as_ordered_tree/adapters.rb +17 -0
  4. data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
  5. data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
  6. data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
  7. data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
  8. data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
  9. data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
  10. data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
  11. data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
  12. data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
  13. data/lib/acts_as_ordered_tree/hooks.rb +38 -0
  14. data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
  15. data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
  16. data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
  17. data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
  18. data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
  19. data/lib/acts_as_ordered_tree/node.rb +78 -0
  20. data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
  21. data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
  22. data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
  23. data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
  24. data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
  25. data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
  26. data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
  27. data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
  28. data/lib/acts_as_ordered_tree/position.rb +143 -0
  29. data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
  30. data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
  31. data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
  32. data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
  33. data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
  34. data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
  35. data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
  36. data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
  37. data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
  38. data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
  39. data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
  40. data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
  41. data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
  42. data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
  43. data/lib/acts_as_ordered_tree/tree.rb +148 -0
  44. data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
  45. data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
  46. data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
  47. data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
  48. data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
  49. data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
  50. data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
  51. data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
  52. data/lib/acts_as_ordered_tree/validators.rb +1 -1
  53. data/lib/acts_as_ordered_tree/version.rb +1 -1
  54. data/spec/acts_as_ordered_tree_spec.rb +80 -909
  55. data/spec/adapters/postgresql_spec.rb +14 -0
  56. data/spec/adapters/recursive_spec.rb +12 -0
  57. data/spec/adapters/shared.rb +272 -0
  58. data/spec/callbacks_spec.rb +177 -0
  59. data/spec/counter_cache_spec.rb +31 -0
  60. data/spec/create_spec.rb +110 -0
  61. data/spec/destroy_spec.rb +57 -0
  62. data/spec/inheritance_spec.rb +176 -0
  63. data/spec/move_spec.rb +94 -0
  64. data/spec/node/movements/concurrent_movements_spec.rb +354 -0
  65. data/spec/node/movements/move_higher_spec.rb +46 -0
  66. data/spec/node/movements/move_lower_spec.rb +46 -0
  67. data/spec/node/movements/move_to_child_of_spec.rb +147 -0
  68. data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
  69. data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
  70. data/spec/node/movements/move_to_left_of_spec.rb +120 -0
  71. data/spec/node/movements/move_to_right_of_spec.rb +120 -0
  72. data/spec/node/movements/move_to_root_spec.rb +67 -0
  73. data/spec/node/predicates_spec.rb +211 -0
  74. data/spec/node/reloading_spec.rb +42 -0
  75. data/spec/node/siblings_spec.rb +193 -0
  76. data/spec/node/traversals_spec.rb +71 -0
  77. data/spec/persevering_transaction_spec.rb +98 -0
  78. data/spec/relation/arrangeable_spec.rb +88 -0
  79. data/spec/relation/iterable_spec.rb +104 -0
  80. data/spec/relation/preloaded_spec.rb +57 -0
  81. data/spec/reorder_spec.rb +83 -0
  82. data/spec/spec_helper.rb +30 -38
  83. data/spec/support/db/boot.rb +22 -0
  84. data/spec/{db → support/db}/config.travis.yml +2 -0
  85. data/spec/{db → support/db}/config.yml +1 -0
  86. data/spec/{db → support/db}/schema.rb +9 -0
  87. data/spec/support/factories.rb +2 -2
  88. data/spec/support/matchers.rb +67 -58
  89. data/spec/support/models.rb +6 -14
  90. data/spec/support/tree_factory.rb +315 -0
  91. data/spec/tree/children_association_spec.rb +72 -0
  92. data/spec/tree/columns_spec.rb +65 -0
  93. data/spec/tree/scopes_spec.rb +39 -0
  94. metadata +161 -43
  95. data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
  96. data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
  97. data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
  98. data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
  99. data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
  100. 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,9 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Association
4
+ def scope
5
+ scoped
6
+ end
7
+ end
8
+ end
9
+ 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
- require 'acts_as_ordered_tree/tenacious_transaction'
3
- require 'acts_as_ordered_tree/relation/preloaded'
4
- require 'acts_as_ordered_tree/arrangeable'
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
- include ActsAsOrderedTree::TenaciousTransaction
9
-
10
- # Returns true if this is a root node.
11
- def root?
12
- self[parent_column].nil?
13
- end
14
-
15
- # Returns true if this is the end of a branch.
16
- def leaf?
17
- persisted? && if children_counter_cache_column
18
- self[children_counter_cache_column] == 0
19
- else
20
- !children.reorder(nil).exists?
21
- end
22
- end
23
-
24
- def branch?
25
- !leaf?
26
- end
27
-
28
- # Returns true is this is a child node
29
- def child?
30
- !root?
31
- end
32
-
33
- # Returns root (not really fast operation)
34
- def root
35
- root? ? self : parent.root
36
- end
37
-
38
- # Returns the array of all parents and self starting from root
39
- def self_and_ancestors
40
- # 1. recursively load ancestors
41
- nodes = []
42
- node = self
43
-
44
- while node
45
- nodes << node
46
- node = node.parent
47
- end
48
-
49
- # 2. first ancestor is a root
50
- nodes.reverse!
51
-
52
- # 3. create fake scope
53
- ActsAsOrderedTree::Relation::Preloaded.new(self.class).
54
- where(:id => nodes.map(&:id).compact).
55
- extending(Arrangeable).
56
- records(nodes)
57
- end
58
-
59
- # Returns the array of all parents starting from root
60
- def ancestors
61
- records = self_and_ancestors - [self]
62
-
63
- scope = self_and_ancestors.where(arel[:id].not_eq(id))
64
- scope.records(records)
65
- end
66
-
67
- # Returns the array of all children of the parent, including self
68
- def self_and_siblings
69
- ordered_tree_scope.where(parent_column => self[parent_column])
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+ compatability
159
- def insert_at(position = 1)
160
- move_to_child_with_index(parent, position - 1)
161
- end
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
- # Move the node to the child of another node
192
- def move_to_child_of(node)
193
- move_to node, :child
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
- # Move the node to the child of another node with specify index
197
- def move_to_child_with_index(node, index)
198
- raise ActiveRecord::ActiveRecordError, "index can't be nil" unless index
90
+ # Returns true if node contains any children.
91
+ #
92
+ # @deprecated
93
+ deprecated_method :branch?, :has_children?
199
94
 
200
- tenacious_transaction do
201
- new_siblings = (node.try(:children) || ordered_tree_scope.roots).
202
- reload.
203
- lock(true).
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 compute_level #:nodoc:
236
- ancestors.count
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
- def ordered_tree_scope #:nodoc:
465
- if scope_column_names.empty?
466
- self.class.base_class
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