loyal_awesome_nested_set 0.0.1

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.
@@ -0,0 +1,40 @@
1
+ require 'awesome_nested_set/tree'
2
+
3
+ module CollectiveIdea
4
+ module Acts
5
+ module NestedSet
6
+ module Model
7
+ module Rebuildable
8
+
9
+
10
+ # Rebuilds the left & rights if unset or invalid.
11
+ # Also very useful for converting from acts_as_tree.
12
+ def rebuild!(validate_nodes = true)
13
+ # default_scope with order may break database queries so we do all operation without scope
14
+ unscoped do
15
+ Tree.new(self, validate_nodes).rebuild!
16
+ end
17
+ end
18
+
19
+ def scope_for_rebuild
20
+ scope = proc {}
21
+
22
+ if acts_as_nested_set_options[:scope]
23
+ scope = proc {|node|
24
+ scope_column_names.inject("") {|str, column_name|
25
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name))} "
26
+ }
27
+ }
28
+ end
29
+ scope
30
+ end
31
+
32
+ def order_for_rebuild
33
+ "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{primary_key}"
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,121 @@
1
+ module CollectiveIdea
2
+ module Acts
3
+ module NestedSet
4
+ module Model
5
+ module Relatable
6
+
7
+ # Returns an collection of all parents
8
+ def ancestors
9
+ without_self self_and_ancestors
10
+ end
11
+
12
+ # Returns the collection of all parents and self
13
+ def self_and_ancestors
14
+ nested_set_scope.
15
+ where(arel_table[left_column_name].lteq(left)).
16
+ where(arel_table[right_column_name].gteq(right))
17
+ end
18
+
19
+ # Returns the collection of all children of the parent, except self
20
+ def siblings
21
+ without_self self_and_siblings
22
+ end
23
+
24
+ # Returns the collection of all children of the parent, including self
25
+ def self_and_siblings
26
+ nested_set_scope.children_of parent_id
27
+ end
28
+
29
+ # Returns a set of all of its nested children which do not have children
30
+ def leaves
31
+ descendants.where(
32
+ "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
33
+ )
34
+ end
35
+
36
+ # Returns the level of this object in the tree
37
+ # root level is 0
38
+ def level
39
+ parent_id.nil? ? 0 : compute_level
40
+ end
41
+
42
+ # Returns a collection including all of its children and nested children
43
+ def descendants
44
+ without_self self_and_descendants
45
+ end
46
+
47
+ # Returns a collection including itself and all of its nested children
48
+ def self_and_descendants
49
+ # using _left_ for both sides here lets us benefit from an index on that column if one exists
50
+ nested_set_scope.right_of(left).left_of(right)
51
+ end
52
+
53
+ def is_descendant_of?(other)
54
+ within_node?(other, self) && same_scope?(other)
55
+ end
56
+
57
+ def is_or_is_descendant_of?(other)
58
+ (other == self || within_node?(other, self)) && same_scope?(other)
59
+ end
60
+
61
+ def is_ancestor_of?(other)
62
+ within_node?(self, other) && same_scope?(other)
63
+ end
64
+
65
+ def is_or_is_ancestor_of?(other)
66
+ (self == other || within_node?(self, other)) && same_scope?(other)
67
+ end
68
+
69
+ # Check if other model is in the same scope
70
+ def same_scope?(other)
71
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
72
+ self.send(attr) == other.send(attr)
73
+ end
74
+ end
75
+
76
+ # Find the first sibling to the left
77
+ def left_sibling
78
+ siblings.left_of(left).last
79
+ end
80
+
81
+ # Find the first sibling to the right
82
+ def right_sibling
83
+ siblings.right_of(left).first
84
+ end
85
+
86
+ def root
87
+ return self_and_ancestors.children_of(nil).first if persisted?
88
+
89
+ if parent_id && current_parent = nested_set_scope.find(parent_id)
90
+ current_parent.root
91
+ else
92
+ self
93
+ end
94
+ end
95
+
96
+ protected
97
+
98
+ def compute_level
99
+ node, nesting = determine_depth
100
+
101
+ node == self ? ancestors.count : node.level + nesting
102
+ end
103
+
104
+ def determine_depth(node = self, nesting = 0)
105
+ while (association = node.association(:parent)).loaded? && association.target
106
+ nesting += 1
107
+ node = node.parent
108
+ end if node.respond_to?(:association)
109
+
110
+ [node, nesting]
111
+ end
112
+
113
+ def within_node?(node, within)
114
+ node.left < within.left && within.left < node.right
115
+ end
116
+
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,27 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ module Model
5
+ module Transactable
6
+
7
+ protected
8
+ def in_tenacious_transaction(&block)
9
+ retry_count = 0
10
+ begin
11
+ transaction(&block)
12
+ rescue ActiveRecord::StatementInvalid => error
13
+ raise unless connection.open_transactions.zero?
14
+ raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
15
+ raise unless retry_count < 10
16
+ retry_count += 1
17
+ logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
18
+ sleep(rand(retry_count)*0.1) # Aloha protocol
19
+ retry
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ require 'awesome_nested_set/set_validator'
2
+
3
+ module CollectiveIdea
4
+ module Acts
5
+ module NestedSet
6
+ module Model
7
+ module Validatable
8
+
9
+ def valid?
10
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
11
+ end
12
+
13
+ def left_and_rights_valid?
14
+ SetValidator.new(self).valid?
15
+ end
16
+
17
+ def no_duplicates_for_columns?
18
+ [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
19
+ # No duplicates
20
+ select("#{scope_string}#{column}, COUNT(#{column})").
21
+ group("#{scope_string}#{column}", quoted_primary_key_column_full_name).
22
+ having("COUNT(#{column}) > 1").
23
+ first.nil?
24
+ end
25
+ end
26
+
27
+ # Wrapper for each_root_valid? that can deal with scope.
28
+ def all_roots_valid?
29
+ if acts_as_nested_set_options[:scope]
30
+ all_roots_valid_by_scope?(roots)
31
+ else
32
+ each_root_valid?(roots)
33
+ end
34
+ end
35
+
36
+ def all_roots_valid_by_scope?(roots_to_validate)
37
+ roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots|
38
+ each_root_valid?(grouped_roots)
39
+ end
40
+ end
41
+
42
+ def each_root_valid?(roots_to_validate)
43
+ left = right = 0
44
+ roots_to_validate.all? do |root|
45
+ (root.left > left && root.right > right).tap do
46
+ left = root.left
47
+ right = root.right
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+ def roots_grouped_by_scope(roots_to_group)
54
+ roots_to_group.group_by {|record|
55
+ scope_column_names.collect {|col| record.send(col) }
56
+ }
57
+ end
58
+
59
+ def scope_string
60
+ Array(acts_as_nested_set_options[:scope]).map do |c|
61
+ connection.quote_column_name(c)
62
+ end.push(nil).join(", ")
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,117 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ class Move
5
+ attr_reader :target, :position, :instance
6
+
7
+ def initialize(target, position, instance)
8
+ @target = target
9
+ @position = position
10
+ @instance = instance
11
+ end
12
+
13
+ def move
14
+ prevent_impossible_move
15
+
16
+ bound, other_bound = get_boundaries
17
+
18
+ # there would be no change
19
+ return if bound == right || bound == left
20
+
21
+ # we have defined the boundaries of two non-overlapping intervals,
22
+ # so sorting puts both the intervals and their boundaries in order
23
+ a, b, c, d = [left, right, bound, other_bound].sort
24
+
25
+ lock_nodes_between! a, d
26
+
27
+ nested_set_scope.where(where_statement(a, d)).
28
+ update_all(conditions(a, b, c, d))
29
+ end
30
+
31
+ private
32
+
33
+ delegate :left, :right, :left_column_name, :right_column_name,
34
+ :quoted_left_column_name, :quoted_right_column_name,
35
+ :quoted_parent_column_name, :parent_column_name, :nested_set_scope,
36
+ :to => :instance
37
+
38
+ delegate :arel_table, :class, :to => :instance, :prefix => true
39
+ delegate :base_class, :to => :instance_class, :prefix => :instance
40
+
41
+ def where_statement(left_bound, right_bound)
42
+ instance_arel_table[left_column_name].in(left_bound..right_bound).
43
+ or(instance_arel_table[right_column_name].in(left_bound..right_bound))
44
+ end
45
+
46
+ def conditions(a, b, c, d)
47
+ [
48
+ case_condition_for_direction(:quoted_left_column_name) +
49
+ case_condition_for_direction(:quoted_right_column_name) +
50
+ case_condition_for_parent,
51
+ {:a => a, :b => b, :c => c, :d => d, :id => instance.id, :new_parent => new_parent}
52
+ ]
53
+ end
54
+
55
+ def case_condition_for_direction(column_name)
56
+ column = send(column_name)
57
+ "#{column} = CASE " +
58
+ "WHEN #{column} BETWEEN :a AND :b " +
59
+ "THEN #{column} + :d - :b " +
60
+ "WHEN #{column} BETWEEN :c AND :d " +
61
+ "THEN #{column} + :a - :c " +
62
+ "ELSE #{column} END, "
63
+ end
64
+
65
+ def case_condition_for_parent
66
+ "#{quoted_parent_column_name} = CASE " +
67
+ "WHEN #{instance_base_class.primary_key} = :id THEN :new_parent " +
68
+ "ELSE #{quoted_parent_column_name} END"
69
+ end
70
+
71
+ def lock_nodes_between!(left_bound, right_bound)
72
+ # select the rows in the model between a and d, and apply a lock
73
+ instance_base_class.right_of(left_bound).left_of_right_side(right_bound).
74
+ select(:id).lock(true)
75
+ end
76
+
77
+ def root
78
+ position == :root
79
+ end
80
+
81
+ def new_parent
82
+ case position
83
+ when :child
84
+ target.id
85
+ else
86
+ target[parent_column_name]
87
+ end
88
+ end
89
+
90
+ def get_boundaries
91
+ if (bound = target_bound) > right
92
+ bound -= 1
93
+ other_bound = right + 1
94
+ else
95
+ other_bound = left - 1
96
+ end
97
+ [bound, other_bound]
98
+ end
99
+
100
+ def prevent_impossible_move
101
+ if !root && !instance.move_possible?(target)
102
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
103
+ end
104
+ end
105
+
106
+ def target_bound
107
+ case position
108
+ when :child; right(target)
109
+ when :left; left(target)
110
+ when :right; right(target) + 1
111
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,63 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ class SetValidator
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ @scope = model.all
9
+ @parent = arel_table.alias('parent')
10
+ end
11
+
12
+ def valid?
13
+ query.count == 0
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :model, :parent
19
+ attr_accessor :scope
20
+
21
+ delegate :parent_column_name, :primary_key, :left_column_name, :right_column_name, :arel_table,
22
+ :quoted_table_name, :quoted_parent_column_full_name, :quoted_left_column_full_name, :quoted_right_column_full_name, :quoted_left_column_name, :quoted_right_column_name,
23
+ :to => :model
24
+
25
+ def query
26
+ join_scope
27
+ filter_scope
28
+ end
29
+
30
+ def join_scope
31
+ join_arel = arel_table.join(parent, Arel::Nodes::OuterJoin).on(parent[primary_key].eq(arel_table[parent_column_name]))
32
+ self.scope = scope.joins(join_arel.join_sql)
33
+ end
34
+
35
+ def filter_scope
36
+ self.scope = scope.where(
37
+ bound_is_null(left_column_name).
38
+ or(bound_is_null(right_column_name)).
39
+ or(left_bound_greater_than_right).
40
+ or(parent_not_null.and(bounds_outside_parent))
41
+ )
42
+ end
43
+
44
+ def bound_is_null(column_name)
45
+ arel_table[column_name].eq(nil)
46
+ end
47
+
48
+ def left_bound_greater_than_right
49
+ arel_table[left_column_name].gteq(arel_table[right_column_name])
50
+ end
51
+
52
+ def parent_not_null
53
+ arel_table[parent_column_name].not_eq(nil)
54
+ end
55
+
56
+ def bounds_outside_parent
57
+ arel_table[left_column_name].lteq(parent[left_column_name]).or(arel_table[right_column_name].gteq(parent[right_column_name]))
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,63 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ class Tree
5
+ attr_reader :model, :validate_nodes
6
+ attr_accessor :indices
7
+
8
+ delegate :left_column_name, :right_column_name, :quoted_parent_column_full_name,
9
+ :order_for_rebuild, :scope_for_rebuild,
10
+ :to => :model
11
+
12
+ def initialize(model, validate_nodes)
13
+ @model = model
14
+ @validate_nodes = validate_nodes
15
+ @indices = {}
16
+ end
17
+
18
+ def rebuild!
19
+ # Don't rebuild a valid tree.
20
+ return true if model.valid?
21
+
22
+ root_nodes.each do |root_node|
23
+ # setup index for this scope
24
+ indices[scope_for_rebuild.call(root_node)] ||= 0
25
+ set_left_and_rights(root_node)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def increment_indice!(node)
32
+ indices[scope_for_rebuild.call(node)] += 1
33
+ end
34
+
35
+ def set_left_and_rights(node)
36
+ set_left!(node)
37
+ # find
38
+ node_children(node).each { |n| set_left_and_rights(n) }
39
+ set_right!(node)
40
+
41
+ node.save!(:validate => validate_nodes)
42
+ end
43
+
44
+ def node_children(node)
45
+ model.where(["#{quoted_parent_column_full_name} = ? #{scope_for_rebuild.call(node)}", node]).
46
+ order(order_for_rebuild)
47
+ end
48
+
49
+ def root_nodes
50
+ model.where("#{quoted_parent_column_full_name} IS NULL").order(order_for_rebuild)
51
+ end
52
+
53
+ def set_left!(node)
54
+ node[left_column_name] = increment_indice!(node)
55
+ end
56
+
57
+ def set_right!(node)
58
+ node[right_column_name] = increment_indice!(node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end