awesome_nested_set 2.1.6 → 3.0.0.rc.6

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,84 @@
1
+ # Mixed into both classes and instances to provide easy access to the column names
2
+ module CollectiveIdea #:nodoc:
3
+ module Acts #:nodoc:
4
+ module NestedSet #:nodoc:
5
+ module Columns
6
+ def left_column_name
7
+ acts_as_nested_set_options[:left_column]
8
+ end
9
+
10
+ def right_column_name
11
+ acts_as_nested_set_options[:right_column]
12
+ end
13
+
14
+ def depth_column_name
15
+ acts_as_nested_set_options[:depth_column]
16
+ end
17
+
18
+ def parent_column_name
19
+ acts_as_nested_set_options[:parent_column]
20
+ end
21
+
22
+ def primary_column_name
23
+ acts_as_nested_set_options[:primary_column]
24
+ end
25
+
26
+ def order_column
27
+ acts_as_nested_set_options[:order_column] || left_column_name
28
+ end
29
+
30
+ def scope_column_names
31
+ Array(acts_as_nested_set_options[:scope])
32
+ end
33
+
34
+ def quoted_left_column_name
35
+ ActiveRecord::Base.connection.quote_column_name(left_column_name)
36
+ end
37
+
38
+ def quoted_right_column_name
39
+ ActiveRecord::Base.connection.quote_column_name(right_column_name)
40
+ end
41
+
42
+ def quoted_depth_column_name
43
+ ActiveRecord::Base.connection.quote_column_name(depth_column_name)
44
+ end
45
+
46
+ def quoted_primary_column_name
47
+ ActiveRecord::Base.connection.quote_column_name(primary_column_name)
48
+ end
49
+
50
+ def quoted_parent_column_name
51
+ ActiveRecord::Base.connection.quote_column_name(parent_column_name)
52
+ end
53
+
54
+ def quoted_scope_column_names
55
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
56
+ end
57
+
58
+ def quoted_order_column_name
59
+ ActiveRecord::Base.connection.quote_column_name(order_column)
60
+ end
61
+
62
+ def quoted_primary_key_column_full_name
63
+ "#{quoted_table_name}.#{quoted_primary_column_name}"
64
+ end
65
+
66
+ def quoted_order_column_full_name
67
+ "#{quoted_table_name}.#{quoted_order_column_name}"
68
+ end
69
+
70
+ def quoted_left_column_full_name
71
+ "#{quoted_table_name}.#{quoted_left_column_name}"
72
+ end
73
+
74
+ def quoted_right_column_full_name
75
+ "#{quoted_table_name}.#{quoted_right_column_name}"
76
+ end
77
+
78
+ def quoted_parent_column_full_name
79
+ "#{quoted_table_name}.#{quoted_parent_column_name}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -25,64 +25,19 @@ module CollectiveIdea #:nodoc:
25
25
  if class_or_item.is_a? Array
26
26
  items = class_or_item.reject { |e| !e.root? }
27
27
  else
28
- class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
28
+ class_or_item = class_or_item.roots if class_or_item.respond_to?(:scope)
29
29
  items = Array(class_or_item)
30
30
  end
31
31
  result = []
32
32
  items.each do |root|
33
33
  result += root.class.associate_parents(root.self_and_descendants).map do |i|
34
34
  if mover.nil? || mover.new_record? || mover.move_possible?(i)
35
- [yield(i), i.id]
35
+ [yield(i), i.primary_id]
36
36
  end
37
37
  end.compact
38
38
  end
39
39
  result
40
40
  end
41
-
42
- # Returns options for select as nested_set_options, sorted by an specific column
43
- # It requires passing a string with the name of the column to sort the set with
44
- # You can exclude some items from the tree.
45
- # You can pass a block receiving an item and returning the string displayed in the select.
46
- #
47
- # == Params
48
- # * +class_or_item+ - Class name or top level times
49
- # * +:column+ - Column to sort the set (this will sort each children for all root elements)
50
- # * +mover+ - The item that is being move, used to exlude impossible moves
51
- # * +&block+ - a block that will be used to display: { |item| ... item.name }
52
- #
53
- # == Usage
54
- #
55
- # <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
56
- # "#{'–' * i.level} #{i.name}"
57
- # }) %>
58
- #
59
- def sorted_nested_set_options(class_or_item, order, mover = nil)
60
- if class_or_item.is_a? Array
61
- items = class_or_item.reject { |e| !e.root? }
62
- else
63
- class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
64
- items = Array(class_or_item)
65
- end
66
- result = []
67
- children = []
68
- items.each do |root|
69
- root.class.associate_parents(root.self_and_descendants).map do |i|
70
- if mover.nil? || mover.new_record? || mover.move_possible?(i)
71
- if !i.leaf?
72
- children.sort_by! &order
73
- children.each { |c| result << [yield(c), c.id] }
74
- children = []
75
- result << [yield(i), i.id]
76
- else
77
- children << i
78
- end
79
- end
80
- end.compact
81
- end
82
- children.sort_by! &order
83
- children.each { |c| result << [yield(c), c.id] }
84
- result
85
- end
86
41
  end
87
42
  end
88
43
  end
@@ -0,0 +1,29 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ class Iterator
5
+ attr_reader :objects
6
+
7
+ def initialize(objects)
8
+ @objects = objects
9
+ end
10
+
11
+ def each_with_level
12
+ path = [nil]
13
+ objects.each do |o|
14
+ if o.parent_id != path.last
15
+ # we are on a new level, did we descend or ascend?
16
+ if path.include?(o.parent_id)
17
+ # remove wrong tailing paths elements
18
+ path.pop while path.last != o.parent_id
19
+ else
20
+ path << o.parent_id
21
+ end
22
+ end
23
+ yield(o, path.length - 1)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,146 @@
1
+ require 'awesome_nested_set/move'
2
+
3
+ module CollectiveIdea #:nodoc:
4
+ module Acts #:nodoc:
5
+ module NestedSet #:nodoc:
6
+ module Model
7
+ module Movable
8
+
9
+ def move_possible?(target)
10
+ self != target && # Can't target self
11
+ same_scope?(target) && # can't be in different scopes
12
+ # detect impossible move
13
+ within_bounds?(target.left, target.left) &&
14
+ within_bounds?(target.right, target.right)
15
+ end
16
+
17
+ # Shorthand method for finding the left sibling and moving to the left of it.
18
+ def move_left
19
+ move_to_left_of left_sibling
20
+ end
21
+
22
+ # Shorthand method for finding the right sibling and moving to the right of it.
23
+ def move_right
24
+ move_to_right_of right_sibling
25
+ end
26
+
27
+ # Move the node to the left of another node
28
+ def move_to_left_of(node)
29
+ move_to node, :left
30
+ end
31
+
32
+ # Move the node to the right of another node
33
+ def move_to_right_of(node)
34
+ move_to node, :right
35
+ end
36
+
37
+ # Move the node to the child of another node
38
+ def move_to_child_of(node)
39
+ move_to node, :child
40
+ end
41
+
42
+ # Move the node to the child of another node with specify index
43
+ def move_to_child_with_index(node, index)
44
+ if node.children.empty?
45
+ move_to_child_of(node)
46
+ elsif node.children.count == index
47
+ move_to_right_of(node.children.last)
48
+ else
49
+ my_position = node.children.index(self)
50
+ if my_position && my_position < index
51
+ # e.g. if self is at position 0 and we want to move self to position 1 then self
52
+ # needs to move to the *right* of the node at position 1. That's because the node
53
+ # that is currently at position 1 will be at position 0 after the move completes.
54
+ move_to_right_of(node.children[index])
55
+ elsif my_position && my_position == index
56
+ # do nothing. already there.
57
+ else
58
+ move_to_left_of(node.children[index])
59
+ end
60
+ end
61
+ end
62
+
63
+ # Move the node to root nodes
64
+ def move_to_root
65
+ move_to self, :root
66
+ end
67
+
68
+ # Order children in a nested set by an attribute
69
+ # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
70
+ # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
71
+ def move_to_ordered_child_of(parent, order_attribute, ascending = true)
72
+ self.move_to_root and return unless parent
73
+
74
+ left_neighbor = find_left_neighbor(parent, order_attribute, ascending)
75
+ self.move_to_child_of(parent)
76
+
77
+ return unless parent.children.many?
78
+
79
+ if left_neighbor
80
+ self.move_to_right_of(left_neighbor)
81
+ else # Self is the left most node.
82
+ self.move_to_left_of(parent.children[0])
83
+ end
84
+ end
85
+
86
+ # Find the node immediately to the left of this node.
87
+ def find_left_neighbor(parent, order_attribute, ascending)
88
+ left = nil
89
+ parent.children.each do |n|
90
+ if ascending
91
+ left = n if n.send(order_attribute) < self.send(order_attribute)
92
+ else
93
+ left = n if n.send(order_attribute) > self.send(order_attribute)
94
+ end
95
+ end
96
+ left
97
+ end
98
+
99
+ def move_to(target, position)
100
+ prevent_unpersisted_move
101
+
102
+ run_callbacks :move do
103
+ in_tenacious_transaction do
104
+ target = reload_target(target, position)
105
+ self.reload_nested_set
106
+
107
+ Move.new(target, position, self).move
108
+ end
109
+ after_move_to(target, position)
110
+ end
111
+ end
112
+
113
+ protected
114
+
115
+ def after_move_to(target, position)
116
+ target.reload_nested_set if target
117
+ self.set_depth_for_self_and_descendants!
118
+ self.reload_nested_set
119
+ end
120
+
121
+ def move_to_new_parent
122
+ if @move_to_new_parent_id.nil?
123
+ move_to_root
124
+ elsif @move_to_new_parent_id
125
+ move_to_child_of(@move_to_new_parent_id)
126
+ end
127
+ end
128
+
129
+ def out_of_bounds?(left_bound, right_bound)
130
+ left <= left_bound && right >= right_bound
131
+ end
132
+
133
+ def prevent_unpersisted_move
134
+ if self.new_record?
135
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node"
136
+ end
137
+ end
138
+
139
+ def within_bounds?(left_bound, right_bound)
140
+ !out_of_bounds?(left_bound, right_bound)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,70 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ module Model
5
+ module Prunable
6
+
7
+ # Prunes a branch off of the tree, shifting all of the elements on the right
8
+ # back to the left so the counts still work.
9
+ def destroy_descendants
10
+ return if right.nil? || left.nil? || skip_before_destroy
11
+
12
+ in_tenacious_transaction do
13
+ reload_nested_set
14
+ # select the rows in the model that extend past the deletion point and apply a lock
15
+ nested_set_scope.right_of(left).select(primary_id).lock(true)
16
+
17
+ return false unless destroy_or_delete_descendants
18
+
19
+ # update lefts and rights for remaining nodes
20
+ update_siblings_for_remaining_nodes
21
+
22
+ # Reload is needed because children may have updated their parent (self) during deletion.
23
+ reload
24
+
25
+ # Don't allow multiple calls to destroy to corrupt the set
26
+ self.skip_before_destroy = true
27
+ end
28
+ end
29
+
30
+ def destroy_or_delete_descendants
31
+ if acts_as_nested_set_options[:dependent] == :destroy
32
+ descendants.each do |model|
33
+ model.skip_before_destroy = true
34
+ model.destroy
35
+ end
36
+ elsif acts_as_nested_set_options[:dependent] == :restrict_with_exception
37
+ raise ActiveRecord::DeleteRestrictionError.new(:children) unless leaf?
38
+ elsif acts_as_nested_set_options[:dependent] == :restrict_with_error
39
+ unless leaf?
40
+ record = self.class.human_attribute_name(:children).downcase
41
+ errors.add(:base, :"restrict_dependent_destroy.many", record: record)
42
+ return false
43
+ end
44
+ return true
45
+ else
46
+ descendants.delete_all
47
+ end
48
+ end
49
+
50
+ def update_siblings_for_remaining_nodes
51
+ update_siblings(:left)
52
+ update_siblings(:right)
53
+ end
54
+
55
+ def update_siblings(direction)
56
+ full_column_name = send("quoted_#{direction}_column_full_name")
57
+ column_name = send("quoted_#{direction}_column_name")
58
+
59
+ nested_set_scope.where(["#{full_column_name} > ?", right]).
60
+ update_all(["#{column_name} = (#{column_name} - ?)", diff])
61
+ end
62
+
63
+ def diff
64
+ right - left + 1
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,42 @@
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
+ column_value = node.send(column_name)
26
+ cond = column_value.nil? ? "IS NULL" : "= #{connection.quote(column_value)}"
27
+ str << "AND #{connection.quote_column_name(column_name)} #{cond} "
28
+ }
29
+ }
30
+ end
31
+ scope
32
+ end
33
+
34
+ def order_for_rebuild
35
+ "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{primary_key}"
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ 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.where(primary_column_name => parent_id).first!
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 self.class.connection.open_transactions.zero?
14
+ raise unless error.message =~ /[Dd]eadlock|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,70 @@
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}) as _count").
21
+ group("#{scope_string}#{column}", quoted_primary_key_column_full_name).
22
+ having("COUNT(#{column}) > 1").
23
+ order(quoted_primary_key_column_full_name).
24
+ first.nil?
25
+ end
26
+ end
27
+
28
+ # Wrapper for each_root_valid? that can deal with scope.
29
+ def all_roots_valid?
30
+ if acts_as_nested_set_options[:scope]
31
+ all_roots_valid_by_scope?(roots)
32
+ else
33
+ each_root_valid?(roots)
34
+ end
35
+ end
36
+
37
+ def all_roots_valid_by_scope?(roots_to_validate)
38
+ roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots|
39
+ each_root_valid?(grouped_roots)
40
+ end
41
+ end
42
+
43
+ def each_root_valid?(roots_to_validate)
44
+ left = right = 0
45
+ roots_to_validate.all? do |root|
46
+ (root.left > left && root.right > right).tap do
47
+ left = root.left
48
+ right = root.right
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+ def roots_grouped_by_scope(roots_to_group)
55
+ roots_to_group.group_by {|record|
56
+ scope_column_names.collect {|col| record.send(col) }
57
+ }
58
+ end
59
+
60
+ def scope_string
61
+ Array(acts_as_nested_set_options[:scope]).map do |c|
62
+ connection.quote_column_name(c)
63
+ end.push(nil).join(", ")
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end