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