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,35 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ module Iterators
5
+ # @api private
6
+ class Arranger
7
+ include Enumerable
8
+
9
+ delegate :each, :to => :arrange
10
+
11
+ def initialize(collection)
12
+ @collection = collection
13
+ @cache = Hash.new
14
+ end
15
+
16
+ def arrange
17
+ @collection.each_with_object(Hash.new) do |node, result|
18
+ @cache[node.id] ||= node
19
+
20
+ insertion_point = result
21
+
22
+ ancestors(node).each { |a| insertion_point = (insertion_point[a] ||= {}) }
23
+
24
+ insertion_point[node] = {}
25
+ end
26
+ end
27
+
28
+ private
29
+ def ancestors(node)
30
+ parent = @cache[node.ordered_tree_node.parent_id]
31
+ parent ? ancestors(parent) + [parent] : []
32
+ end
33
+ end # class Arranger
34
+ end # module Iterators
35
+ end # module ActsAsOrderedTree
@@ -0,0 +1,52 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ module Iterators
5
+ # @api private
6
+ class LevelCalculator
7
+ include Enumerable
8
+
9
+ def initialize(collection)
10
+ @collection = collection
11
+ @level = nil # minimal nodes level (first item level)
12
+ end
13
+
14
+ def each(&block)
15
+ return to_enum unless block_given?
16
+
17
+ if @collection.klass.ordered_tree.columns.depth?
18
+ each_with_cached_level(&block)
19
+ else
20
+ each_without_cached_level(&block)
21
+ end
22
+ end
23
+
24
+ private
25
+ def each_with_cached_level
26
+ @collection.each { |node| yield node, node.level }
27
+ end
28
+
29
+ def each_without_cached_level
30
+ path = []
31
+
32
+ @collection.each do |node|
33
+ parent_id = node.ordered_tree_node.parent_id
34
+
35
+ @level ||= node.level
36
+ path << parent_id if path.empty?
37
+
38
+ if parent_id != path.last
39
+ # parent changed
40
+ if path.include?(parent_id) # ascend
41
+ path.pop while path.last != parent_id
42
+ else # descend
43
+ path << parent_id
44
+ end
45
+ end
46
+
47
+ yield node, @level + path.length - 1
48
+ end
49
+ end
50
+ end # class LevelCalculator
51
+ end
52
+ end
@@ -0,0 +1,58 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ module Iterators
5
+ # @api private
6
+ class OrphansPruner
7
+ include Enumerable
8
+
9
+ def initialize(collection)
10
+ @collection = collection
11
+ @cache = Hash.new
12
+ @level = nil # minimal node level
13
+ end
14
+
15
+ def each
16
+ return to_enum unless block_given?
17
+
18
+ prepare if @cache.empty?
19
+
20
+ @collection.each do |node|
21
+ if orphan?(node)
22
+ discard(node.id)
23
+ else
24
+ yield node
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+ def orphan?(node)
31
+ !has_parent?(node) && node.level > @level
32
+ end
33
+
34
+ def has_parent?(node)
35
+ @cache.key?(node.ordered_tree_node.parent_id)
36
+ end
37
+
38
+ def prepare
39
+ @collection.each do |node|
40
+ @cache[node.id] = []
41
+
42
+ if has_parent?(node)
43
+ @cache[node.ordered_tree_node.parent_id] << node.id
44
+ else
45
+ @level = [@level, node.level].compact.min
46
+ end
47
+ end
48
+ end
49
+
50
+ def discard(id)
51
+ if @cache.key?(id)
52
+ @cache[id].each { |k| discard(k) }
53
+ @cache.delete(id)
54
+ end
55
+ end
56
+ end # class OrphansPruner
57
+ end # module Iterators
58
+ end # module ActsAsOrderedTree
@@ -0,0 +1,78 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/node/attributes'
4
+ require 'acts_as_ordered_tree/node/movements'
5
+ require 'acts_as_ordered_tree/node/predicates'
6
+ require 'acts_as_ordered_tree/node/reloading'
7
+ require 'acts_as_ordered_tree/node/siblings'
8
+ require 'acts_as_ordered_tree/node/traversals'
9
+
10
+ module ActsAsOrderedTree
11
+ # ActsAsOrderedTree::Node takes care of tree integrity when record is saved
12
+ # via usual ActiveRecord mechanism
13
+ class Node
14
+ include Attributes
15
+ include Movements
16
+ include Predicates
17
+ include Reloading
18
+ include Siblings
19
+ include Traversals
20
+
21
+ # @attr_reader [ActiveRecord::Base] original AR record, created, updated or destroyed
22
+ attr_reader :record
23
+
24
+ delegate :id, :parent, :children, :==, :to => :record
25
+
26
+ def initialize(record)
27
+ @record = record
28
+ end
29
+
30
+ # Returns scope to which record should be applied
31
+ def scope
32
+ if tree.columns.scope?
33
+ tree.base_class.where Hash[tree.columns.scope.map { |column| [column, record[column]] }]
34
+ else
35
+ tree.base_class.where(nil)
36
+ end
37
+ end
38
+
39
+ # Convert node to AR::Relation
40
+ #
41
+ # @return [ActiveRecord::Relation]
42
+ def to_relation
43
+ scope.where(tree.columns.id => id)
44
+ end
45
+
46
+ # @return [ActsAsOrderedTree::Tree]
47
+ def tree
48
+ record.class.ordered_tree
49
+ end
50
+
51
+ # Returns node level value (0 for root)
52
+ #
53
+ # @return [Fixnum]
54
+ def level
55
+ case
56
+ when root? then 0
57
+ when depth_column_could_be_used? then depth
58
+ when parent_association_loaded? then parent.level + 1
59
+ # @todo move it adapters
60
+ else ancestors.size
61
+ end
62
+ end
63
+
64
+ private
65
+ # @return [Arel::Table]
66
+ def table
67
+ record.class.arel_table
68
+ end
69
+
70
+ def depth_column_could_be_used?
71
+ tree.columns.depth? && record.persisted? && !parent_id_changed? && depth?
72
+ end
73
+
74
+ def parent_association_loaded?
75
+ record.association(:parent).loaded?
76
+ end
77
+ end # class Node
78
+ end # module ActsAsOrderedTree
@@ -0,0 +1,48 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/concern'
4
+
5
+ module ActsAsOrderedTree
6
+ class Node
7
+ # This module when included creates accessor to record's attributes that related to tree structure
8
+ #
9
+ # @example
10
+ # class Node
11
+ # include Attributes
12
+ # end
13
+ #
14
+ # node = Node.new(record)
15
+ # node.parent_id # => record.parent_id
16
+ # node.position # => record.position
17
+ # node.position_was # => record.position_was
18
+ # # etc.
19
+ module Attributes
20
+ extend ActiveSupport::Concern
21
+
22
+ METHODS = ['', '?', ?=, '_was', '_changed?', %w(reset_ !)].freeze
23
+
24
+ included do
25
+ dynamic_attribute_accessor :position
26
+ dynamic_attribute_accessor :parent_id, :parent
27
+ dynamic_attribute_accessor :depth
28
+ dynamic_attribute_accessor :counter_cache
29
+ end
30
+
31
+ module ClassMethods
32
+ # Generates methods based on configurable record attributes
33
+ #
34
+ # @api private
35
+ def dynamic_attribute_accessor(name, column_name_accessor = name)
36
+ METHODS.each do |prefix, suffix|
37
+ prefix, suffix = suffix, prefix unless suffix
38
+ method_name = "#{prefix}#{name}#{suffix}"
39
+
40
+ define_method method_name do |*args|
41
+ record.send "#{prefix}#{tree.columns[column_name_accessor]}#{suffix}", *args
42
+ end
43
+ end
44
+ end
45
+ end # module ClassMethods
46
+ end # module Attributes
47
+ end # class Node
48
+ end # module ActsAsOrderedTree
@@ -0,0 +1,62 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/persevering_transaction'
4
+
5
+ module ActsAsOrderedTree
6
+ class Node
7
+ # @api private
8
+ class Movement
9
+ attr_reader :node, :options
10
+
11
+ delegate :record, :position, :position=, :to => :node
12
+
13
+ def initialize(node, target = nil, options = {}, &block)
14
+ @node, @options, @block = node, options, block
15
+ @_target = target
16
+ end
17
+
18
+ def parent=(id_or_record)
19
+ if id_or_record.is_a?(ActiveRecord::Base)
20
+ record.parent = id_or_record
21
+ else
22
+ node.parent_id = id_or_record
23
+ end
24
+ end
25
+
26
+ def start
27
+ transaction do
28
+ record.reload
29
+
30
+ @block[self] if @block
31
+
32
+ record.save
33
+ end
34
+ end
35
+
36
+ def target
37
+ return @target if defined?(@target)
38
+
39
+ # load target
40
+ @target = case @_target
41
+ when ActiveRecord::Base, nil
42
+ @_target.lock!
43
+ when nil
44
+ nil
45
+ else
46
+ scope = node.scope.lock
47
+
48
+ if options.fetch(:strict, false)
49
+ scope.find(@_target)
50
+ else
51
+ scope.where(:id => @_target).first
52
+ end
53
+ end.try(:ordered_tree_node)
54
+ end
55
+
56
+ private
57
+ def transaction(&block)
58
+ PerseveringTransaction.new(record.class.connection).start(&block)
59
+ end
60
+ end # class Movement
61
+ end # class Node
62
+ end # module ActsAsOrderedTree
@@ -0,0 +1,111 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/core_ext/module/aliasing'
4
+
5
+ require 'acts_as_ordered_tree/node/movement'
6
+
7
+ module ActsAsOrderedTree
8
+ class Node
9
+ # This module provides node with movement functionality
10
+ #
11
+ # Methods:
12
+ # * move_to_root
13
+ # * move_left (move_higher)
14
+ # * move_right (move_lower)
15
+ # * move_to_left_of (move_to_above_of)
16
+ # * move_to_right_of (move_to_bottom_of)
17
+ # * move_to_child_of
18
+ # * move_to_child_with_index
19
+ # * move_to_child_with_position
20
+ module Movements
21
+ # Transform node into root node
22
+ def move_to_root
23
+ movement do |to|
24
+ to.position = record.root? ? position : nil
25
+ to.parent = nil
26
+ end
27
+ end
28
+
29
+ # Swap node with higher sibling
30
+ def move_higher
31
+ movement { self.position -= 1 }
32
+ end
33
+ alias_method :move_left, :move_higher
34
+
35
+ # Swap node with lower sibling
36
+ def move_lower
37
+ movement { self.position += 1 }
38
+ end
39
+ alias_method :move_right, :move_lower
40
+
41
+ # Move node to above(left) of another node
42
+ #
43
+ # @param [ActiveRecord::Base, #to_i] node may be another record of ID
44
+ def move_to_above_of(node)
45
+ movement(node, :strict => true) do |to|
46
+ self.right_sibling = to.target.record
47
+ end
48
+ end
49
+ alias_method :move_to_left_of, :move_to_above_of
50
+
51
+ # Move node to bottom (right) of another node
52
+ #
53
+ # @param [ActiveRecord::Base, #to_i] node may be another record of ID
54
+ def move_to_bottom_of(node)
55
+ movement(node, :strict => true) do |to|
56
+ self.left_sibling = to.target.record
57
+ end
58
+ end
59
+ alias_method :move_to_right_of, :move_to_bottom_of
60
+
61
+ # Move node to child of another node
62
+ #
63
+ # @param [ActiveRecord::Base, #to_i] node may be another record of ID
64
+ def move_to_child_of(node)
65
+ if node
66
+ movement(node) do |to|
67
+ to.parent = node
68
+ to.position = nil if parent_id_changed?
69
+ end
70
+ else
71
+ move_to_root
72
+ end
73
+ end
74
+
75
+ # Move node to child of another node with specified index (which may be negative)
76
+ #
77
+ # @param [ActiveRecord::Base, Fixnum] node
78
+ # @param [#to_i] index
79
+ def move_to_child_with_index(node, index)
80
+ index = index.to_i
81
+
82
+ if index >= 0
83
+ move_to_child_with_position node, index + 1
84
+ elsif node
85
+ movement(node, :strict => true) do |to|
86
+ to.parent = node
87
+ to.position = to.target.children.size + index + 1
88
+ end
89
+ else
90
+ move_to_child_with_position nil, scope.roots.size + index + 1
91
+ end
92
+ end
93
+
94
+ # Move node to child of another node with specified position
95
+ #
96
+ # @param [ActiveRecord::Base, Fixnum] node
97
+ # @param [Integer, nil] position
98
+ def move_to_child_with_position(node, position)
99
+ movement(node) do |to|
100
+ to.parent = node
101
+ to.position = position
102
+ end
103
+ end
104
+
105
+ private
106
+ def movement(target = nil, options = {}, &block)
107
+ Movement.new(self, target, options, &block).start
108
+ end
109
+ end # module Movements
110
+ end # class Node
111
+ end # module ActsAsOrderedTree