mongoid_nested_set 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +147 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/mongoid_nested_set.rb +40 -0
- data/lib/mongoid_nested_set/base.rb +90 -0
- data/lib/mongoid_nested_set/document.rb +207 -0
- data/lib/mongoid_nested_set/fields.rb +60 -0
- data/lib/mongoid_nested_set/outline_number.rb +122 -0
- data/lib/mongoid_nested_set/rebuild.rb +41 -0
- data/lib/mongoid_nested_set/relations.rb +100 -0
- data/lib/mongoid_nested_set/remove_order_by.rb +11 -0
- data/lib/mongoid_nested_set/update.rb +233 -0
- data/lib/mongoid_nested_set/validation.rb +59 -0
- data/mongoid_nested_set.gemspec +100 -0
- data/spec/matchers/nestedset_pos.rb +46 -0
- data/spec/models/circle_node.rb +4 -0
- data/spec/models/node.rb +10 -0
- data/spec/models/node_without_nested_set.rb +6 -0
- data/spec/models/numbering_node.rb +10 -0
- data/spec/models/renamed_fields.rb +7 -0
- data/spec/models/shape_node.rb +18 -0
- data/spec/models/square_node.rb +4 -0
- data/spec/models/test_document.rb +35 -0
- data/spec/models/unscoped_node.rb +9 -0
- data/spec/mongoid_nested_set_spec.rb +723 -0
- data/spec/spec_helper.rb +44 -0
- metadata +196 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
# Mixed int both classes and instances to provide easy access to the field names
|
4
|
+
module Fields
|
5
|
+
|
6
|
+
def left_field_name
|
7
|
+
acts_as_nested_set_options[:left_field]
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def right_field_name
|
12
|
+
acts_as_nested_set_options[:right_field]
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def parent_field_name
|
17
|
+
acts_as_nested_set_options[:parent_field]
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def outline_number_field_name
|
22
|
+
acts_as_nested_set_options[:outline_number_field]
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def scope_field_names
|
27
|
+
Array(acts_as_nested_set_options[:scope])
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def scope_class
|
32
|
+
acts_as_nested_set_options[:klass]
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def quoted_left_field_name
|
37
|
+
# TODO
|
38
|
+
left_field_name
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def quoted_right_field_name
|
43
|
+
# TODO
|
44
|
+
right_field_name
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def quoted_parent_field_name
|
49
|
+
# TODO
|
50
|
+
parent_field_name
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def quoted_scope_field_names
|
55
|
+
# TODO
|
56
|
+
scope_field_names
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module OutlineNumber
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
base.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Iterates over tree elements and determines the current outline number in
|
13
|
+
# the tree.
|
14
|
+
# Only accepts default ordering, ordering by an other field than lft
|
15
|
+
# does not work.
|
16
|
+
# This method does not used the cached number field.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# Category.each_with_outline_number(Category.root.self_and_descendants) do |o, level|
|
20
|
+
#
|
21
|
+
def each_with_outline_number(objects, parent_number=nil)
|
22
|
+
objects = Array(objects) unless objects.is_a? Array
|
23
|
+
|
24
|
+
stack = []
|
25
|
+
last_num = parent_number
|
26
|
+
objects.each_with_index do |o, i|
|
27
|
+
if i == 0 && last_num == nil && !o.root?
|
28
|
+
last_num = o.parent.outline_number
|
29
|
+
end
|
30
|
+
|
31
|
+
if stack.last.nil? || o.parent_id != stack.last[:parent_id]
|
32
|
+
# we are on a new level, did we descend or ascend?
|
33
|
+
if stack.any? { |h| h[:parent_id] == o.parent_id }
|
34
|
+
# ascend
|
35
|
+
stack.pop while stack.last[:parent_id] != o.parent_id
|
36
|
+
else
|
37
|
+
# descend
|
38
|
+
stack << {:parent_id => o.parent_id, :parent_number => last_num, :siblings => []}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if o.root? && !roots_have_outline_numbers?
|
43
|
+
num = nil
|
44
|
+
else
|
45
|
+
num = o.send(:build_outline_number,
|
46
|
+
o.root? ? '' : stack.last[:parent_number],
|
47
|
+
o.send(:outline_number_sequence, stack.last[:siblings])
|
48
|
+
)
|
49
|
+
end
|
50
|
+
yield(o, num)
|
51
|
+
|
52
|
+
stack.last[:siblings] << o
|
53
|
+
last_num = num
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def update_outline_numbers(objects, parent_number=nil)
|
59
|
+
each_with_outline_number(objects, parent_number) do |o, num|
|
60
|
+
o.update_attributes(outline_number_field_name => num)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Do root nodes have outline numbers
|
66
|
+
def roots_have_outline_numbers?
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
module InstanceMethods
|
73
|
+
|
74
|
+
def outline_number
|
75
|
+
self[outline_number_field_name]
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def update_outline_number
|
80
|
+
self.class.update_outline_numbers(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def update_self_and_descendants_outline_number
|
85
|
+
self.class.update_outline_numbers(self_and_descendants)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def update_descendants_outline_number
|
90
|
+
self.class.update_outline_numbers(self.descendants, self.outline_number)
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# Gets the outline sequence number for this node
|
97
|
+
#
|
98
|
+
# For example, if the parent's outline number is 1.2 and this is the
|
99
|
+
# 3rd sibling this will return 3.
|
100
|
+
#
|
101
|
+
def outline_number_sequence(prev_siblings)
|
102
|
+
prev_siblings.count + 1
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Constructs the full outline number
|
107
|
+
#
|
108
|
+
def build_outline_number(parent_number, sequence)
|
109
|
+
if parent_number && parent_number != ''
|
110
|
+
parent_number + outline_number_seperator + sequence.to_s
|
111
|
+
else
|
112
|
+
sequence.to_s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def outline_number_seperator
|
117
|
+
'.'
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Rebuild
|
4
|
+
|
5
|
+
# Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
|
6
|
+
# Warning: Very expensive!
|
7
|
+
def rebuild!(options = {})
|
8
|
+
# Don't rebuild a valid tree.
|
9
|
+
return true if valid?
|
10
|
+
|
11
|
+
scope = lambda{ |node| {} }
|
12
|
+
if acts_as_nested_set_options[:scope]
|
13
|
+
scope = lambda { |node| node.nested_set_scope.scoped }
|
14
|
+
end
|
15
|
+
indices = {}
|
16
|
+
|
17
|
+
set_left_and_rights = lambda do |node|
|
18
|
+
# set left
|
19
|
+
left = (indices[scope.call(node)] += 1)
|
20
|
+
# find
|
21
|
+
node.nested_set_scope.where(parent_field_name => node.id).asc(left_field_name).asc(right_field_name).each { |n| set_left_and_rights.call(n) }
|
22
|
+
# set right
|
23
|
+
right = (indices[scope.call(node)] += 1)
|
24
|
+
|
25
|
+
node.class.collection.update(
|
26
|
+
{:_id => node.id},
|
27
|
+
{"$set" => {left_field_name => left, right_field_name => right}},
|
28
|
+
{:safe => true}
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Find root node(s)
|
33
|
+
root_nodes = self.where(parent_field_name => nil).asc(left_field_name).asc(right_field_name).asc(:_id).each do |root_node|
|
34
|
+
# setup index for this scope
|
35
|
+
indices[scope.call(root_node)] ||= 0
|
36
|
+
set_left_and_rights.call(root_node)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Relations
|
4
|
+
|
5
|
+
# Returns root
|
6
|
+
def root
|
7
|
+
self_and_ancestors.first
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
# Returns the array of all parents and self
|
12
|
+
def self_and_ancestors
|
13
|
+
nested_set_scope.where(
|
14
|
+
left_field_name => {"$lte" => left},
|
15
|
+
right_field_name => {"$gte" => right}
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Returns an array of all parents
|
21
|
+
def ancestors
|
22
|
+
without_self self_and_ancestors
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Returns the array of all children of the parent, including self
|
27
|
+
def self_and_siblings
|
28
|
+
nested_set_scope.where(parent_field_name => parent_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Returns the array of all children of the parent, except self
|
33
|
+
def siblings
|
34
|
+
without_self self_and_siblings
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Returns a set of all of its nested children which do not have children
|
39
|
+
def leaves
|
40
|
+
descendants.where("this.#{right_field_name} - this.#{left_field_name} == 1")
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Returns the level of this object in the tree
|
45
|
+
# root level is 0
|
46
|
+
def level
|
47
|
+
parent_id.nil? ? 0 : ancestors.count
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Returns a set of itself and all of its nested children
|
52
|
+
def self_and_descendants
|
53
|
+
nested_set_scope.where(
|
54
|
+
left_field_name => {"$gte" => left},
|
55
|
+
right_field_name => {"$lte" => right}
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Returns a set of all of its children and nested children
|
61
|
+
def descendants
|
62
|
+
without_self self_and_descendants
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def is_descendant_of?(other)
|
67
|
+
other.left < self.left && self.left < other.right && same_scope?(other)
|
68
|
+
end
|
69
|
+
alias :descendant_of? is_descendant_of?
|
70
|
+
|
71
|
+
|
72
|
+
def is_or_is_descendant_of?(other)
|
73
|
+
other.left <= self.left && self.left < other.right && same_scope?(other)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def is_ancestor_of?(other)
|
78
|
+
self.left < other.left && other.left < self.right && same_scope?(other)
|
79
|
+
end
|
80
|
+
alias :ancestor_of? is_ancestor_of?
|
81
|
+
|
82
|
+
|
83
|
+
def is_or_is_ancestor_of?(other)
|
84
|
+
self.left <= other.left && other.left < self.right && same_scope?(other)
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# Find the first sibling to the left
|
89
|
+
def left_sibling
|
90
|
+
siblings.where(left_field_name => {"$lt" => left}).remove_order_by.desc(left_field_name).first
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Find the first sibling to the right
|
95
|
+
def right_sibling
|
96
|
+
siblings.where(left_field_name => {"$gt" => left}).asc(left_field_name).first
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Update
|
4
|
+
|
5
|
+
# Shorthand method for finding the left sibling and moving to the left of it
|
6
|
+
def move_left
|
7
|
+
move_to_left_of left_sibling
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
# Shorthand method for finding the right sibling and moving to the right of it
|
12
|
+
def move_right
|
13
|
+
move_to_right_of right_sibling
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# Move the node to the left of another node (you can pass id only)
|
18
|
+
def move_to_left_of(node)
|
19
|
+
move_to node, :left
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Move the node to the right of another node (you can pass id only)
|
24
|
+
def move_to_right_of(node)
|
25
|
+
move_to node, :right
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Move the node to the child of another node (you can pass id only)
|
30
|
+
def move_to_child_of(node)
|
31
|
+
move_to node, :child
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Move the node to root nodes
|
36
|
+
def move_to_root
|
37
|
+
move_to nil, :root
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def move_possible?(target)
|
42
|
+
self != target && # Can't target self
|
43
|
+
same_scope?(target) && # can't be in different scopes
|
44
|
+
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def store_new_parent
|
52
|
+
@move_to_new_parent_id = ((self.new_record? && parent_id) || send("#{parent_field_name}_changed?")) ? parent_id : false
|
53
|
+
true # force callback to return true
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def move_to_new_parent
|
58
|
+
if @move_to_new_parent_id.nil?
|
59
|
+
move_to_root
|
60
|
+
elsif @move_to_new_parent_id
|
61
|
+
move_to_child_of(@move_to_new_parent_id)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# on creation, set automatically lft and rgt to the end of the tree
|
67
|
+
def set_default_left_and_right
|
68
|
+
maxright = nested_set_scope.max(right_field_name) || 0
|
69
|
+
self[left_field_name] = maxright + 1
|
70
|
+
self[right_field_name] = maxright + 2
|
71
|
+
self[:depth] = 0
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def move_to(target, position)
|
76
|
+
raise Mongoid::Errors::MongoidError, "You cannot move a new node" if self.new_record?
|
77
|
+
|
78
|
+
res = run_callbacks :move do
|
79
|
+
|
80
|
+
# No transaction support in MongoDB.
|
81
|
+
# ACID is not guaranteed
|
82
|
+
# TODO
|
83
|
+
|
84
|
+
if target.is_a? scope_class
|
85
|
+
target.reload_nested_set
|
86
|
+
elsif position != :root
|
87
|
+
# load object if node is not an object
|
88
|
+
target = nested_set_scope.where(:_id => target).first
|
89
|
+
end
|
90
|
+
self.reload_nested_set
|
91
|
+
|
92
|
+
unless position == :root || target
|
93
|
+
raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be found."
|
94
|
+
end
|
95
|
+
|
96
|
+
unless position == :root || move_possible?(target)
|
97
|
+
raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be inside moved tree."
|
98
|
+
end
|
99
|
+
|
100
|
+
bound = case position
|
101
|
+
when :child; target[right_field_name]
|
102
|
+
when :left; target[left_field_name]
|
103
|
+
when :right; target[right_field_name] + 1
|
104
|
+
when :root; 1
|
105
|
+
else raise Mongoid::Errors::MongoidError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
106
|
+
end
|
107
|
+
|
108
|
+
if bound > self[right_field_name]
|
109
|
+
bound = bound - 1
|
110
|
+
other_bound = self[right_field_name] + 1
|
111
|
+
else
|
112
|
+
other_bound = self[left_field_name] - 1
|
113
|
+
end
|
114
|
+
|
115
|
+
# there would be no change
|
116
|
+
return self if bound == self[right_field_name] || bound == self[left_field_name]
|
117
|
+
|
118
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
119
|
+
# so sorting puts both the intervals and their boundaries in order
|
120
|
+
a, b, c, d = [self[left_field_name], self[right_field_name], bound, other_bound].sort
|
121
|
+
|
122
|
+
old_parent = self[parent_field_name]
|
123
|
+
new_parent = case position
|
124
|
+
when :child; target.id
|
125
|
+
when :root; nil
|
126
|
+
else target[parent_field_name]
|
127
|
+
end
|
128
|
+
|
129
|
+
# TODO: Worst case O(n) queries, improve?
|
130
|
+
# MongoDB 1.9 may allow javascript in updates: http://jira.mongodb.org/browse/SERVER-458
|
131
|
+
nested_set_scope.only(left_field_name, right_field_name, parent_field_name).remove_order_by.each do |node|
|
132
|
+
updates = {}
|
133
|
+
if (a..b).include? node.left
|
134
|
+
updates[left_field_name] = node.left + d - b
|
135
|
+
elsif (c..d).include? node.left
|
136
|
+
updates[left_field_name] = node.left + a - c
|
137
|
+
end
|
138
|
+
|
139
|
+
if (a..b).include? node.right
|
140
|
+
updates[right_field_name] = node.right + d - b
|
141
|
+
elsif (c..d).include? node.right
|
142
|
+
updates[right_field_name] = node.right + a - c
|
143
|
+
end
|
144
|
+
|
145
|
+
updates[parent_field_name] = new_parent if self.id == node.id
|
146
|
+
|
147
|
+
node.class.collection.update(
|
148
|
+
{:_id => node.id },
|
149
|
+
{"$set" => updates},
|
150
|
+
{:safe => true}
|
151
|
+
) unless updates.empty?
|
152
|
+
end
|
153
|
+
|
154
|
+
self.reload_nested_set
|
155
|
+
self.update_self_and_descendants_depth
|
156
|
+
|
157
|
+
if outline_numbering?
|
158
|
+
if old_parent && old_parent != new_parent
|
159
|
+
scope_class.where(:_id => old_parent).first.update_descendants_outline_number
|
160
|
+
end
|
161
|
+
if new_parent
|
162
|
+
scope_class.where(:_id => new_parent).first.update_descendants_outline_number
|
163
|
+
else
|
164
|
+
update_self_and_descendants_outline_number
|
165
|
+
end
|
166
|
+
self.reload_nested_set
|
167
|
+
end
|
168
|
+
|
169
|
+
target.reload_nested_set if target
|
170
|
+
end
|
171
|
+
self
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
# Update cached level attribute
|
176
|
+
def update_depth
|
177
|
+
if depth?
|
178
|
+
self.update_attributes(:depth => level)
|
179
|
+
end
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Update cached level attribute for self and descendants
|
185
|
+
def update_self_and_descendants_depth
|
186
|
+
if depth?
|
187
|
+
scope_class.each_with_level(self_and_descendants) do |node, level|
|
188
|
+
node.class.collection.update(
|
189
|
+
{:_id => node.id},
|
190
|
+
{"$set" => {:depth => level}},
|
191
|
+
{:safe => true}
|
192
|
+
) unless node.depth == level
|
193
|
+
end
|
194
|
+
self.reload
|
195
|
+
end
|
196
|
+
self
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
201
|
+
# back to the left so the counts still work
|
202
|
+
def destroy_descendants
|
203
|
+
return if right.nil? || left.nil? || skip_before_destroy
|
204
|
+
|
205
|
+
if acts_as_nested_set_options[:dependent] == :destroy
|
206
|
+
descendants.each do |model|
|
207
|
+
model.skip_before_destroy = true
|
208
|
+
model.destroy
|
209
|
+
end
|
210
|
+
else
|
211
|
+
c = nested_set_scope.fuse(:where => {left_field_name => {"$gt" => left}, right_field_name => {"$lt" => right}})
|
212
|
+
scope_class.delete_all(:conditions => c.selector)
|
213
|
+
end
|
214
|
+
|
215
|
+
# update lefts and rights for remaining nodes
|
216
|
+
diff = right - left + 1
|
217
|
+
scope_class.collection.update(
|
218
|
+
nested_set_scope.fuse(:where => {left_field_name => {"$gt" => right}}).selector,
|
219
|
+
{"$inc" => { left_field_name => -diff }},
|
220
|
+
{:safe => true, :multi => true}
|
221
|
+
)
|
222
|
+
scope_class.collection.update(
|
223
|
+
nested_set_scope.fuse(:where => {right_field_name => {"$gt" => right}}).selector,
|
224
|
+
{"$inc" => { right_field_name => -diff }},
|
225
|
+
{:safe => true, :multi => true}
|
226
|
+
)
|
227
|
+
|
228
|
+
# Don't allow multiple calls to destroy to corrupt the set
|
229
|
+
self.skip_before_destroy = true
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
end
|