acts_as_ordered_tree 1.3.1 → 2.0.0.beta3

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