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,20 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ class Tree
5
+ class Association
6
+ attr_reader :tree
7
+
8
+ delegate :klass, :to => :tree
9
+
10
+ def initialize(tree)
11
+ @tree = tree
12
+ end
13
+
14
+ protected
15
+ def class_name
16
+ "::#{tree.base_class.name}"
17
+ end
18
+ end # class Association
19
+ end # class Tree
20
+ end # module ActsAsOrderedTree
@@ -0,0 +1,57 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/core_ext/hash/slice'
4
+
5
+ module ActsAsOrderedTree
6
+ class Tree
7
+ # Tree callbacks storage
8
+ #
9
+ # @example
10
+ # MyModel.ordered_tree.callbacks.before_add(parent, child)
11
+ #
12
+ # @api private
13
+ class Callbacks
14
+ VALID_KEYS = :before_add,
15
+ :after_add,
16
+ :before_remove,
17
+ :after_remove
18
+
19
+ def initialize(klass, options)
20
+ @klass = klass
21
+ @callbacks = {}
22
+
23
+ options.slice(*VALID_KEYS).each do |k, v|
24
+ @callbacks[k] = v if v
25
+ end
26
+ end
27
+
28
+ # generate accessors and predicates
29
+ VALID_KEYS.each do |method|
30
+ define_method(method) do |parent, record| # def before_add(parent, record)
31
+ run_callbacks(method, parent, record)
32
+ end
33
+ end
34
+
35
+ private
36
+ def run_callbacks(method, parent, record)
37
+ callback = callback_for(method)
38
+
39
+ case callback
40
+ when Symbol
41
+ parent.send(callback, record)
42
+ when Proc
43
+ callback.call(parent, record)
44
+ when nil, false
45
+ # do nothing
46
+ else
47
+ # parent.before_add(record)
48
+ callback.send(method, parent, record)
49
+ end
50
+ end
51
+
52
+ def callback_for(method)
53
+ @callbacks[method]
54
+ end
55
+ end # class Callbacks
56
+ end # class Tree
57
+ end # module ActsAsOrderedTree
@@ -0,0 +1,120 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/compatibility'
4
+ require 'acts_as_ordered_tree/tree/association'
5
+ require 'acts_as_ordered_tree/relation/iterable'
6
+
7
+ module ActsAsOrderedTree
8
+ class Tree
9
+ # @api private
10
+ class ChildrenAssociation < Association
11
+ # CounterCache extensions will allow to use cached value
12
+ #
13
+ # @api private
14
+ module CounterCache
15
+ def size
16
+ ordered_tree_node.parent_id_changed? ? super : ordered_tree_node.counter_cache
17
+ end
18
+
19
+ def empty?
20
+ size == 0
21
+ end
22
+
23
+ private
24
+ def ordered_tree_node
25
+ @association.owner.ordered_tree_node
26
+ end
27
+ end
28
+
29
+ # Builds association object
30
+ def build
31
+ Compatibility.version '< 4.0.0' do
32
+ opts = options.merge(:conditions => conditions, :order => order)
33
+
34
+ klass.has_many(:children, opts)
35
+ end
36
+
37
+ Compatibility.version '>= 4.0.0' do
38
+ klass.has_many(:children, scope, options)
39
+ end
40
+ end
41
+
42
+ private
43
+ def options
44
+ Hash[
45
+ :class_name => class_name,
46
+ :foreign_key => tree.columns.parent,
47
+ :inverse_of => inverse_of,
48
+ :dependent => :destroy,
49
+ :extend => [extension, Relation::Iterable].compact
50
+ ]
51
+ end
52
+
53
+ def inverse_of
54
+ :parent unless tree.options[:polymorphic]
55
+ end
56
+
57
+ # rails 4.x scope for has_many association
58
+ def scope
59
+ assoc_scope = method(:association_scope)
60
+ join_scope = method(:join_association_scope)
61
+
62
+ ->(join_or_parent) {
63
+ if join_or_parent.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation)
64
+ join_scope[join_or_parent]
65
+ elsif join_or_parent.is_a?(ActiveRecord::Base)
66
+ assoc_scope[join_or_parent]
67
+ else
68
+ where(nil)
69
+ end.extending(Relation::Iterable)
70
+ }
71
+ end
72
+
73
+ # Rails 3.x :conditions options for has_many association
74
+ def conditions
75
+ return nil unless tree.columns.scope?
76
+
77
+ assoc_scope = method(:association_scope)
78
+ join_scope = method(:join_association_scope)
79
+
80
+ Proc.new do |join_association|
81
+ conditions = if join_association.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation)
82
+ join_scope[join_association]
83
+ elsif self.is_a?(ActiveRecord::Base)
84
+ assoc_scope[self]
85
+ else
86
+ where(nil)
87
+ end.where_values.reduce(:and)
88
+
89
+ conditions.try(:to_sql)
90
+ end
91
+ end
92
+
93
+ def order
94
+ tree.columns.position
95
+ end
96
+
97
+ def extension
98
+ if tree.columns.counter_cache?
99
+ CounterCache
100
+ end
101
+ end
102
+
103
+ def join_association_scope(join_association)
104
+ parent = join_association.respond_to?(:parent) ?
105
+ join_association.parent.table :
106
+ join_association.base_klass.arel_table
107
+
108
+ child = join_association.table
109
+
110
+ conditions = tree.columns.scope.map { |column| parent[column].eq child[column] }.reduce(:and)
111
+
112
+ klass.unscoped.where(conditions)
113
+ end
114
+
115
+ def association_scope(owner)
116
+ owner.ordered_tree_node.scope.order(tree.columns.position)
117
+ end
118
+ end # class ChildrenAssociation
119
+ end # class Tree
120
+ end # module ActsAsOrderedTree
@@ -0,0 +1,102 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ class Tree
5
+ # Ordered tree columns store
6
+ #
7
+ # @example
8
+ # MyModel.tree.columns.parent # => "parent_id"
9
+ # MyModel.tree.columns.counter_cache # => nil
10
+ # MyModel.tree.columns.counter_cache? # => false
11
+ class Columns
12
+ # This error is raised when unknown column given in :scope option
13
+ UnknownColumn = Class.new(StandardError)
14
+
15
+ # @api private
16
+ def self.column_accessor(*names)
17
+ names.each do |name|
18
+ define_method "#{name}=" do |value|
19
+ @columns[name] = value.to_s if column_exists?(value)
20
+ end
21
+ private "#{name}=".to_sym
22
+
23
+ define_method "#{name}?" do
24
+ @columns[name].present?
25
+ end
26
+
27
+ define_method name do
28
+ @columns[name]
29
+ end
30
+ end
31
+ end
32
+
33
+ # @!method parent
34
+ # @!method parent?
35
+ # @!method parent=(value)
36
+ # @!method position
37
+ # @!method position?
38
+ # @!method position=(value)
39
+ # @!method depth
40
+ # @!method depth?
41
+ # @!method depth=(value)
42
+ # @!method counter_cache
43
+ # @!method counter_cache?
44
+ # @!method counter_cache=(value)
45
+ # @!method scope
46
+ # @!method scope?
47
+ column_accessor :parent,
48
+ :position,
49
+ :depth,
50
+ :counter_cache,
51
+ :scope
52
+
53
+ def initialize(klass, options = {})
54
+ @klass = klass
55
+ @columns = { :id => id }
56
+
57
+ self.parent = options[:parent_column]
58
+ self.position = options[:position_column]
59
+ self.depth = options[:depth_column]
60
+ self.counter_cache = counter_cache_name(options[:counter_cache])
61
+ self.scope = options[:scope]
62
+ end
63
+
64
+ def [](name)
65
+ @columns[name]
66
+ end
67
+
68
+ def id
69
+ @klass.primary_key
70
+ end
71
+
72
+ # Returns array of columns names associated with ordered tree structure
73
+ def to_a
74
+ @columns.values.flatten.compact
75
+ end
76
+
77
+ private
78
+ undef_method :scope=
79
+ def scope=(value)
80
+ columns = Array.wrap(value)
81
+
82
+ unknown = columns.reject { |name| column_exists?(name) }
83
+
84
+ raise UnknownColumn, "Unknown column#{'s' if unknown.size > 1} passed to :scope option: #{unknown.join(', ')}" if unknown.any?
85
+
86
+ @columns[:scope] = columns.map(&:to_s)
87
+ end
88
+
89
+ def counter_cache_name(value)
90
+ if value == true
91
+ "#{@klass.name.demodulize.underscore.pluralize}_count"
92
+ else
93
+ value
94
+ end
95
+ end
96
+
97
+ def column_exists?(name)
98
+ name.present? && @klass.columns_hash.include?(name.to_s)
99
+ end
100
+ end # class Columns
101
+ end # class Tree
102
+ end # module ActsAsOrderedTree
@@ -0,0 +1,24 @@
1
+ module ActsAsOrderedTree
2
+ class Tree
3
+ # @deprecated Use `ordered_tree.columns` object
4
+ module DeprecatedColumnsAccessors
5
+ class << self
6
+ # @api private
7
+ def deprecated_method(method, delegate)
8
+ define_method(method) do
9
+ ActiveSupport::Deprecation.warn("#{name}.#{method} is deprecated in favor of #{name}.ordered_tree.columns.#{delegate}", caller(1))
10
+
11
+ ordered_tree.columns.send(delegate)
12
+ end
13
+ end
14
+ private :deprecated_method
15
+ end
16
+
17
+ deprecated_method :parent_column, :parent
18
+ deprecated_method :position_column, :position
19
+ deprecated_method :depth_column, :depth
20
+ deprecated_method :children_counter_cache_column, :counter_cache
21
+ deprecated_method :scope_column_name, :scope
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/tree/association'
4
+
5
+ module ActsAsOrderedTree
6
+ class Tree
7
+ class ParentAssociation < Association
8
+ # create parent association
9
+ #
10
+ # we cannot use native :counter_cache callbacks because they suck! :(
11
+ # they act like this:
12
+ # node.parent = new_parent # and here counters are updated, outside of transaction!
13
+ def build
14
+ klass.belongs_to(:parent, options)
15
+ end
16
+
17
+ private
18
+ def options
19
+ Hash[
20
+ :class_name => class_name,
21
+ :foreign_key => tree.columns.parent,
22
+ :inverse_of => inverse_of
23
+ ]
24
+ end
25
+
26
+ def inverse_of
27
+ :children unless tree.options[:polymorphic]
28
+ end
29
+ end # class ParentAssociation
30
+ end # class Tree
31
+ end # module ActsAsOrderedTree
@@ -0,0 +1,19 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/persevering_transaction'
4
+
5
+ module ActsAsOrderedTree
6
+ class Tree
7
+ # This module contains overridden :with_transaction_returning_status method
8
+ # which wraps itself into PerseveringTransaction.
9
+ #
10
+ # This module is mixed in into Class after Class.acts_as_ordered_tree invocation.
11
+ #
12
+ # @api private
13
+ module Perseverance
14
+ def with_transaction_returning_status
15
+ PerseveringTransaction.new(self.class.connection).start { super }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ class Tree
5
+ module Scopes
6
+ # Returns nodes ordered by their position.
7
+ #
8
+ # @return [ActiveRecord::Relation]
9
+ def preorder
10
+ order arel_table[ordered_tree.columns.position].asc
11
+ end
12
+
13
+ # Returns all nodes that don't have parent.
14
+ #
15
+ # @return [ActiveRecord::Relation]
16
+ def roots
17
+ preorder.where arel_table[ordered_tree.columns.parent].eq nil
18
+ end
19
+
20
+ # Returns all nodes that do not have any children. May be quite inefficient.
21
+ #
22
+ # @return [ActiveRecord::Relation]
23
+ def root
24
+ roots.first
25
+ end
26
+
27
+ # Returns all nodes that do not have any children. May be quite inefficient.
28
+ #
29
+ # @return [ActiveRecord::Relation]
30
+ def leaves
31
+ if ordered_tree.columns.counter_cache?
32
+ leaves_with_counter_cache
33
+ else
34
+ leaves_without_counter_cache
35
+ end
36
+ end
37
+
38
+ private
39
+ def leaves_without_counter_cache
40
+ aliaz = Arel::Nodes::TableAlias.new(arel_table, 't')
41
+
42
+ subquery = unscoped.select('1').
43
+ from(aliaz).
44
+ where(aliaz[ordered_tree.columns.parent].eq(arel_table[primary_key])).
45
+ limit(1).
46
+ reorder(nil)
47
+
48
+ where "NOT EXISTS (#{subquery.to_sql})"
49
+ end
50
+
51
+ def leaves_with_counter_cache
52
+ where arel_table[ordered_tree.columns.counter_cache].eq 0
53
+ end
54
+ end # module Scopes
55
+ end # class Tree
56
+ end # module ActsAsOrderedTree
@@ -8,7 +8,7 @@ module ActsAsOrderedTree
8
8
 
9
9
  class ScopeValidator < ActiveModel::Validator
10
10
  def validate(record)
11
- record.errors.add(:parent, :scope) unless record.same_scope?(record.parent)
11
+ record.errors.add(:parent, :scope) unless record.ordered_tree_node.same_scope?(record.parent)
12
12
  end
13
13
  end
14
14
  end