mongoid_nested_set 0.1.0
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.
- 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
|