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.
- checksums.yaml +7 -0
- data/CHANGELOG +5 -0
- data/README.md +230 -0
- data/lib/awesome_nested_set/awesome_nested_set.rb +66 -692
- data/lib/awesome_nested_set/columns.rb +84 -0
- data/lib/awesome_nested_set/helper.rb +2 -47
- data/lib/awesome_nested_set/iterator.rb +29 -0
- data/lib/awesome_nested_set/model/movable.rb +146 -0
- data/lib/awesome_nested_set/model/prunable.rb +70 -0
- data/lib/awesome_nested_set/model/rebuildable.rb +42 -0
- data/lib/awesome_nested_set/model/relatable.rb +121 -0
- data/lib/awesome_nested_set/model/transactable.rb +27 -0
- data/lib/awesome_nested_set/model/validatable.rb +70 -0
- data/lib/awesome_nested_set/model.rb +248 -0
- data/lib/awesome_nested_set/move.rb +131 -0
- data/lib/awesome_nested_set/set_validator.rb +63 -0
- data/lib/awesome_nested_set/tree.rb +63 -0
- data/lib/awesome_nested_set/version.rb +1 -1
- data/lib/awesome_nested_set.rb +1 -1
- metadata +60 -37
- data/README.rdoc +0 -153
|
@@ -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?(:
|
|
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.
|
|
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
|