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