mongoid-tree 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +24 -24
- data/README.rdoc +51 -5
- data/lib/mongoid/tree.rb +19 -11
- data/lib/mongoid/tree/ordering.rb +175 -0
- data/lib/mongoid/tree/traversal.rb +1 -1
- data/spec/mongoid/tree/ordering_spec.rb +307 -0
- data/spec/mongoid/tree/traversal_spec.rb +22 -0
- data/spec/mongoid/tree_spec.rb +19 -1
- data/spec/support/macros/tree_macros.rb +17 -14
- data/spec/support/models/node.rb +4 -0
- metadata +13 -14
data/Gemfile.lock
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mongoid-tree (0.
|
5
|
-
mongoid (>= 2.0.0.beta.
|
4
|
+
mongoid-tree (0.4.0)
|
5
|
+
mongoid (>= 2.0.0.beta.20)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: http://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activemodel (3.0.
|
11
|
-
activesupport (= 3.0.
|
10
|
+
activemodel (3.0.1)
|
11
|
+
activesupport (= 3.0.1)
|
12
12
|
builder (~> 2.1.2)
|
13
13
|
i18n (~> 0.4.1)
|
14
|
-
activesupport (3.0.
|
15
|
-
autotest (4.4.
|
16
|
-
bson (1.1)
|
17
|
-
bson_ext (1.1)
|
14
|
+
activesupport (3.0.1)
|
15
|
+
autotest (4.4.2)
|
16
|
+
bson (1.1.1)
|
17
|
+
bson_ext (1.1.1)
|
18
18
|
builder (2.1.2)
|
19
19
|
diff-lcs (1.1.2)
|
20
20
|
haml (2.2.24)
|
@@ -22,26 +22,26 @@ GEM
|
|
22
22
|
haml (~> 2.2.8)
|
23
23
|
rake (~> 0.8.2)
|
24
24
|
rdoc (~> 2.3.0)
|
25
|
-
i18n (0.4.
|
26
|
-
mongo (1.
|
27
|
-
bson (>= 1.
|
28
|
-
mongoid (2.0.0.beta.
|
25
|
+
i18n (0.4.2)
|
26
|
+
mongo (1.1.1)
|
27
|
+
bson (>= 1.1.1)
|
28
|
+
mongoid (2.0.0.beta.20)
|
29
29
|
activemodel (~> 3.0)
|
30
|
-
mongo (
|
30
|
+
mongo (~> 1.1)
|
31
31
|
tzinfo (~> 0.3.22)
|
32
32
|
will_paginate (~> 3.0.pre)
|
33
33
|
rake (0.8.7)
|
34
34
|
rdoc (2.3.0)
|
35
|
-
rspec (2.0.
|
36
|
-
rspec-core (
|
37
|
-
rspec-expectations (
|
38
|
-
rspec-mocks (
|
39
|
-
rspec-core (2.0.
|
40
|
-
rspec-expectations (2.0.
|
35
|
+
rspec (2.0.1)
|
36
|
+
rspec-core (~> 2.0.1)
|
37
|
+
rspec-expectations (~> 2.0.1)
|
38
|
+
rspec-mocks (~> 2.0.1)
|
39
|
+
rspec-core (2.0.1)
|
40
|
+
rspec-expectations (2.0.1)
|
41
41
|
diff-lcs (>= 1.1.2)
|
42
|
-
rspec-mocks (2.0.
|
43
|
-
rspec-core (
|
44
|
-
rspec-expectations (
|
42
|
+
rspec-mocks (2.0.1)
|
43
|
+
rspec-core (~> 2.0.1)
|
44
|
+
rspec-expectations (~> 2.0.1)
|
45
45
|
tzinfo (0.3.23)
|
46
46
|
will_paginate (3.0.pre2)
|
47
47
|
|
@@ -52,6 +52,6 @@ DEPENDENCIES
|
|
52
52
|
autotest (>= 4.3.2)
|
53
53
|
bson_ext (>= 1.0.4)
|
54
54
|
hanna (>= 0.1.12)
|
55
|
-
mongoid (>= 2.0.0.beta.
|
55
|
+
mongoid (>= 2.0.0.beta.20)
|
56
56
|
mongoid-tree!
|
57
|
-
rspec (
|
57
|
+
rspec (~> 2.0)
|
data/README.rdoc
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
A tree structure for Mongoid documents using the materialized path pattern
|
4
4
|
|
5
|
+
|
5
6
|
== Requirements
|
6
7
|
|
7
|
-
* mongoid (>= 2.0.0.beta.
|
8
|
+
* mongoid (>= 2.0.0.beta.20)
|
9
|
+
|
8
10
|
|
9
11
|
== Install
|
10
12
|
|
@@ -20,17 +22,17 @@ You might want to remove the <tt>:require => 'mongoid/tree'</tt> option and expl
|
|
20
22
|
|
21
23
|
bundle install
|
22
24
|
|
25
|
+
|
23
26
|
== Usage
|
24
27
|
|
25
28
|
Read the API documentation at http://benedikt.github.com/mongoid-tree and take a look at the Mongoid::Tree module
|
26
29
|
|
27
|
-
require 'mongoid/tree'
|
28
|
-
|
29
30
|
class Node
|
30
31
|
include Mongoid::Document
|
31
32
|
include Mongoid::Tree
|
32
33
|
end
|
33
34
|
|
35
|
+
|
34
36
|
=== Utility methods
|
35
37
|
|
36
38
|
There are several utility methods that help getting to other related documents in the tree:
|
@@ -50,24 +52,64 @@ There are several utility methods that help getting to other related documents i
|
|
50
52
|
node.siblings_and_self
|
51
53
|
node.leaves
|
52
54
|
|
53
|
-
In addition it's possible to check certain aspects of the
|
55
|
+
In addition it's possible to check certain aspects of the document's position in the tree:
|
54
56
|
|
55
57
|
node.root?
|
56
58
|
node.leaf?
|
57
59
|
node.depth
|
58
60
|
node.ancestor_of?(other)
|
59
61
|
node.descendant_of?(other)
|
62
|
+
node.sibling_of?(other)
|
60
63
|
|
61
64
|
See Mongoid::Tree for more information on these methods.
|
62
65
|
|
66
|
+
|
67
|
+
=== Ordering
|
68
|
+
|
69
|
+
Mongoid::Tree doesn't order children by default. To enable ordering of tree nodes include the Mongoid::Tree::Ordering module. This will add a <tt>position</tt> field to your document and provide additional utility methods:
|
70
|
+
|
71
|
+
node.lower_siblings
|
72
|
+
node.higher_siblings
|
73
|
+
node.first_sibling_in_list
|
74
|
+
node.last_sibling_in_list
|
75
|
+
|
76
|
+
node.move_up
|
77
|
+
node.move_down
|
78
|
+
node.move_to_top
|
79
|
+
node.move_to_bottom
|
80
|
+
node.move_above(other)
|
81
|
+
node.move_below(other)
|
82
|
+
|
83
|
+
node.at_top?
|
84
|
+
node.at_bottom?
|
85
|
+
|
86
|
+
Example:
|
87
|
+
|
88
|
+
class Node
|
89
|
+
include Mongoid::Document
|
90
|
+
include Mongoid::Tree
|
91
|
+
include Mongoid::Tree::Ordering
|
92
|
+
end
|
93
|
+
|
94
|
+
See Mongoid::Tree::Ordering for more information on these methods.
|
95
|
+
|
63
96
|
=== Traversal
|
64
97
|
|
65
|
-
It's possible to traverse the tree using different traversal methods
|
98
|
+
It's possible to traverse the tree using different traversal methods using the Mongoid::Tree::Traversal module.
|
99
|
+
|
100
|
+
Example:
|
101
|
+
|
102
|
+
class Node
|
103
|
+
include Mongoid::Document
|
104
|
+
include Mongoid::Tree
|
105
|
+
include Mongoid::Tree::Traversal
|
106
|
+
end
|
66
107
|
|
67
108
|
node.traverse(:breadth_first) do |n|
|
68
109
|
# Do something with Node n
|
69
110
|
end
|
70
111
|
|
112
|
+
|
71
113
|
=== Destroying
|
72
114
|
|
73
115
|
Mongoid::Tree does not handle destroying of nodes by default. However it provides several strategies that help you to deal with children of deleted documents. You can simply add them as <tt>before_destroy</tt> callbacks.
|
@@ -88,6 +130,7 @@ Example:
|
|
88
130
|
before_destroy :nullify_children
|
89
131
|
end
|
90
132
|
|
133
|
+
|
91
134
|
=== Callbacks
|
92
135
|
|
93
136
|
There are two callbacks that are called before and after the rearranging process. This enables you to do additional computations after the documents position in the tree is updated. See Mongoid::Tree for details.
|
@@ -110,14 +153,17 @@ Example:
|
|
110
153
|
end
|
111
154
|
end
|
112
155
|
|
156
|
+
|
113
157
|
== Known issues
|
114
158
|
|
115
159
|
See http://github.com/benedikt/mongoid-tree/issues
|
116
160
|
|
161
|
+
|
117
162
|
== Repository
|
118
163
|
|
119
164
|
See http://github.com/benedikt/mongoid-tree and feel free to fork it!
|
120
165
|
|
166
|
+
|
121
167
|
== Copyright
|
122
168
|
|
123
169
|
Copyright (c) 2010 Benedikt Deicke. See LICENSE for details.
|
data/lib/mongoid/tree.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'mongoid/tree/traversal'
|
2
|
-
|
3
1
|
module Mongoid # :nodoc:
|
4
2
|
##
|
5
3
|
# = Mongoid::Tree
|
@@ -81,10 +79,16 @@ module Mongoid # :nodoc:
|
|
81
79
|
module Tree
|
82
80
|
extend ActiveSupport::Concern
|
83
81
|
|
84
|
-
|
82
|
+
autoload :Ordering, 'mongoid/tree/ordering'
|
83
|
+
autoload :Traversal, 'mongoid/tree/traversal'
|
85
84
|
|
86
85
|
included do
|
87
|
-
references_many :children, :class_name => self.name, :foreign_key => :parent_id, :inverse_of => :parent
|
86
|
+
references_many :children, :class_name => self.name, :foreign_key => :parent_id, :inverse_of => :parent do
|
87
|
+
def <<(*objects)
|
88
|
+
super; objects.each { |c| c.parent = @parent }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
88
92
|
referenced_in :parent, :class_name => self.name, :inverse_of => :children, :index => true
|
89
93
|
|
90
94
|
field :parent_ids, :type => Array, :default => []
|
@@ -228,7 +232,7 @@ module Mongoid # :nodoc:
|
|
228
232
|
##
|
229
233
|
# Returns this document's siblings
|
230
234
|
def siblings
|
231
|
-
siblings_and_self
|
235
|
+
siblings_and_self.excludes(:id => self.id)
|
232
236
|
end
|
233
237
|
|
234
238
|
##
|
@@ -237,6 +241,12 @@ module Mongoid # :nodoc:
|
|
237
241
|
base_class.where(:parent_id => self.parent_id)
|
238
242
|
end
|
239
243
|
|
244
|
+
##
|
245
|
+
# Is this document a sibling of the other document?
|
246
|
+
def sibling_of?(other)
|
247
|
+
self.parent_id == other.parent_id
|
248
|
+
end
|
249
|
+
|
240
250
|
##
|
241
251
|
# Returns all leaves of this document (be careful, currently involves two queries)
|
242
252
|
def leaves
|
@@ -279,11 +289,10 @@ module Mongoid # :nodoc:
|
|
279
289
|
children.destroy_all
|
280
290
|
end
|
281
291
|
|
282
|
-
|
283
|
-
|
292
|
+
private
|
284
293
|
def rearrange
|
285
294
|
if self.parent_id
|
286
|
-
self.parent_ids =
|
295
|
+
self.parent_ids = parent.parent_ids + [self.parent_id]
|
287
296
|
else
|
288
297
|
self.parent_ids = []
|
289
298
|
end
|
@@ -299,6 +308,5 @@ module Mongoid # :nodoc:
|
|
299
308
|
def position_in_tree
|
300
309
|
errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id)
|
301
310
|
end
|
302
|
-
|
303
|
-
|
304
|
-
end
|
311
|
+
end # Tree
|
312
|
+
end # Mongoid
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Tree
|
3
|
+
##
|
4
|
+
# = Mongoid::Tree::Ordering
|
5
|
+
#
|
6
|
+
# Mongoid::Tree doesn't order the tree by default. To enable ordering of children
|
7
|
+
# include both Mongoid::Tree and Mongoid::Tree::Ordering into your document.
|
8
|
+
#
|
9
|
+
# == Utility methods
|
10
|
+
#
|
11
|
+
# This module adds methods to get related siblings depending on their position:
|
12
|
+
#
|
13
|
+
# node.lower_siblings
|
14
|
+
# node.higher_siblings
|
15
|
+
# node.first_sibling_in_list
|
16
|
+
# node.last_sibling_in_list
|
17
|
+
#
|
18
|
+
# There are several methods to move nodes around in the list:
|
19
|
+
#
|
20
|
+
# node.move_up
|
21
|
+
# node.move_down
|
22
|
+
# node.move_to_top
|
23
|
+
# node.move_to_bottom
|
24
|
+
# node.move_above(other)
|
25
|
+
# node.move_below(other)
|
26
|
+
#
|
27
|
+
# Additionally there are some methods to check aspects of the document
|
28
|
+
# in the list of children:
|
29
|
+
#
|
30
|
+
# node.at_top?
|
31
|
+
# node.at_bottom?
|
32
|
+
module Ordering
|
33
|
+
extend ActiveSupport::Concern
|
34
|
+
|
35
|
+
included do
|
36
|
+
reflect_on_association(:children).options[:default_order] = :position.asc
|
37
|
+
|
38
|
+
field :position, :type => Integer
|
39
|
+
|
40
|
+
before_save :assign_default_position
|
41
|
+
before_save :reposition_former_siblings, :if => :parent_id_changed?
|
42
|
+
after_destroy :move_lower_siblings_up
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns siblings below the current document.
|
47
|
+
# Siblings with a position greater than this documents's position.
|
48
|
+
def lower_siblings
|
49
|
+
self.siblings.where(:position.gt => self.position)
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns siblings above the current document.
|
54
|
+
# Siblings with a position lower than this documents's position.
|
55
|
+
def higher_siblings
|
56
|
+
self.siblings.where(:position.lt => self.position)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Returns the lowest sibling (could be self)
|
61
|
+
def last_sibling_in_list
|
62
|
+
siblings_and_self.asc(:position).last
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Returns the highest sibling (could be self)
|
67
|
+
def first_sibling_in_list
|
68
|
+
siblings_and_self.asc(:position).first
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Is this the highest sibling?
|
73
|
+
def at_top?
|
74
|
+
higher_siblings.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Is this the lowest sibling?
|
79
|
+
def at_bottom?
|
80
|
+
lower_siblings.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Move this node above all its siblings
|
85
|
+
def move_to_top
|
86
|
+
return true if at_top?
|
87
|
+
move_above(first_sibling_in_list)
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Move this node below all its siblings
|
92
|
+
def move_to_bottom
|
93
|
+
return true if at_bottom?
|
94
|
+
move_below(last_sibling_in_list)
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Move this node one position up
|
99
|
+
def move_up
|
100
|
+
return if at_top?
|
101
|
+
siblings.where(:position => self.position - 1).first.inc(:position, 1)
|
102
|
+
inc(:position, -1)
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Move this node one position down
|
107
|
+
def move_down
|
108
|
+
return if at_bottom?
|
109
|
+
siblings.where(:position => self.position + 1).first.inc(:position, -1)
|
110
|
+
inc(:position, 1)
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Move this node above the specified node
|
115
|
+
#
|
116
|
+
# This method changes the node's parent if nescessary.
|
117
|
+
def move_above(other)
|
118
|
+
update_attributes!(:parent_id => other.parent_id) unless sibling_of?(other)
|
119
|
+
|
120
|
+
if position > other.position
|
121
|
+
new_position = other.position
|
122
|
+
other.lower_siblings.where(:position.lt => self.position).each { |s| s.inc(:position, 1) }
|
123
|
+
other.inc(:position, 1)
|
124
|
+
update_attributes!(:position => new_position)
|
125
|
+
else
|
126
|
+
new_position = other.position - 1
|
127
|
+
other.higher_siblings.where(:position.gt => self.position).each { |s| s.inc(:position, -1) }
|
128
|
+
update_attributes!(:position => new_position)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Move this node below the specified node
|
134
|
+
#
|
135
|
+
# This method changes the node's parent if nescessary.
|
136
|
+
def move_below(other)
|
137
|
+
update_attributes!(:parent_id => other.parent_id) unless sibling_of?(other)
|
138
|
+
|
139
|
+
if position > other.position
|
140
|
+
new_position = other.position + 1
|
141
|
+
other.lower_siblings.where(:position.lt => self.position).each { |s| s.inc(:position, 1) }
|
142
|
+
update_attributes!(:position => new_position)
|
143
|
+
else
|
144
|
+
new_position = other.position
|
145
|
+
other.higher_siblings.where(:position.gt => self.position).each { |s| s.inc(:position, -1) }
|
146
|
+
other.inc(:position, -1)
|
147
|
+
update_attributes!(:position => new_position)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def move_lower_siblings_up
|
154
|
+
lower_siblings.each { |s| s.inc(:position, -1) }
|
155
|
+
end
|
156
|
+
|
157
|
+
def reposition_former_siblings
|
158
|
+
former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
|
159
|
+
and(:position.gt => (attribute_was('position') || 0)).
|
160
|
+
excludes(:id => self.id)
|
161
|
+
former_siblings.each { |s| s.inc(:position, -1) }
|
162
|
+
end
|
163
|
+
|
164
|
+
def assign_default_position
|
165
|
+
return unless self.position.nil? || self.parent_id_changed?
|
166
|
+
|
167
|
+
if self.siblings.empty? || self.siblings.collect(&:position).compact.empty?
|
168
|
+
self.position = 0
|
169
|
+
else
|
170
|
+
self.position = self.siblings.max(:position) + 1
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -3,7 +3,7 @@ module Mongoid # :nodoc:
|
|
3
3
|
##
|
4
4
|
# = Mongoid::Tree::Traversal
|
5
5
|
#
|
6
|
-
# Mongoid::Tree provides a #traverse method to walk through the tree.
|
6
|
+
# Mongoid::Tree::Traversal provides a #traverse method to walk through the tree.
|
7
7
|
# It supports these traversal methods:
|
8
8
|
#
|
9
9
|
# * depth_first
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Tree::Ordering do
|
4
|
+
|
5
|
+
it "should store position as an Integer with a default of nil" do
|
6
|
+
f = OrderedNode.fields['position']
|
7
|
+
f.should_not be_nil
|
8
|
+
f.options[:type].should == Integer
|
9
|
+
f.options[:default].should == nil
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'when saved' do
|
13
|
+
before(:each) do
|
14
|
+
setup_ordered_tree <<-ENDTREE
|
15
|
+
- root:
|
16
|
+
- child:
|
17
|
+
- subchild:
|
18
|
+
- subsubchild
|
19
|
+
- other_root:
|
20
|
+
- other_child
|
21
|
+
- another_child
|
22
|
+
ENDTREE
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should assign a default position of 0 to each node without a sibling" do
|
26
|
+
node(:child).position.should == 0
|
27
|
+
node(:subchild).position.should == 0
|
28
|
+
node(:subsubchild).position.should == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should place siblings at the end of the list by default" do
|
32
|
+
node(:root).position.should == 0
|
33
|
+
node(:other_root).position.should == 1
|
34
|
+
node(:other_child).position.should == 0
|
35
|
+
node(:another_child).position.should == 1
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should move a node to the end of a list when it is moved to a new parent" do
|
39
|
+
other_root = node(:other_root)
|
40
|
+
child = node(:child)
|
41
|
+
child.position.should == 0
|
42
|
+
other_root.children << child
|
43
|
+
child.reload
|
44
|
+
child.position.should == 2
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should correctly reposition siblings when one of them is removed" do
|
48
|
+
node(:other_child).destroy
|
49
|
+
node(:another_child).position.should == 0
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should correctly reposition siblings when one of them is added to another parent" do
|
53
|
+
node(:root).children << node(:other_child)
|
54
|
+
node(:another_child).position.should == 0
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should correctly reposition siblings when the parent is changed" do
|
58
|
+
other_child = node(:other_child)
|
59
|
+
other_child.parent = node(:root)
|
60
|
+
other_child.save!
|
61
|
+
node(:another_child).position.should == 0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'destroy strategies' do
|
66
|
+
before(:each) do
|
67
|
+
setup_ordered_tree <<-ENDTREE
|
68
|
+
- root:
|
69
|
+
- child:
|
70
|
+
- subchild
|
71
|
+
- other_child
|
72
|
+
- other_root
|
73
|
+
ENDTREE
|
74
|
+
end
|
75
|
+
|
76
|
+
describe ':move_children_to_parent' do
|
77
|
+
it "should set its childen's parent_id to the documents parent_id" do
|
78
|
+
node(:child).move_children_to_parent
|
79
|
+
node(:child).should be_leaf
|
80
|
+
node(:root).children.to_a.should == [node(:child), node(:other_child), node(:subchild)]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'utility methods' do
|
86
|
+
before(:each) do
|
87
|
+
setup_ordered_tree <<-ENDTREE
|
88
|
+
- first_root:
|
89
|
+
- first_child_of_first_root
|
90
|
+
- second_child_of_first_root
|
91
|
+
- second_root
|
92
|
+
- third_root
|
93
|
+
ENDTREE
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#lower_siblings' do
|
97
|
+
it "should return a collection of siblings lower on the list" do
|
98
|
+
node(:second_child_of_first_root).reload
|
99
|
+
node(:first_root).lower_siblings.to_a.should == [node(:second_root), node(:third_root)]
|
100
|
+
node(:second_root).lower_siblings.to_a.should == [node(:third_root)]
|
101
|
+
node(:third_root).lower_siblings.to_a.should == []
|
102
|
+
node(:first_child_of_first_root).lower_siblings.to_a.should == [node(:second_child_of_first_root)]
|
103
|
+
node(:second_child_of_first_root).lower_siblings.to_a.should == []
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#higher_siblings' do
|
108
|
+
it "should return a collection of siblings lower on the list" do
|
109
|
+
node(:first_root).higher_siblings.to_a.should == []
|
110
|
+
node(:second_root).higher_siblings.to_a.should == [node(:first_root)]
|
111
|
+
node(:third_root).higher_siblings.to_a.should == [node(:first_root), node(:second_root)]
|
112
|
+
node(:first_child_of_first_root).higher_siblings.to_a.should == []
|
113
|
+
node(:second_child_of_first_root).higher_siblings.to_a.should == [node(:first_child_of_first_root)]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#at_top?' do
|
118
|
+
it "should return true when the node is first in the list" do
|
119
|
+
node(:first_root).should be_at_top
|
120
|
+
node(:first_child_of_first_root).should be_at_top
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should return false when the node is not first in the list" do
|
124
|
+
node(:second_root).should_not be_at_top
|
125
|
+
node(:third_root).should_not be_at_top
|
126
|
+
node(:second_child_of_first_root).should_not be_at_top
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#at_bottom?' do
|
131
|
+
it "should return true when the node is last in the list" do
|
132
|
+
node(:third_root).should be_at_bottom
|
133
|
+
node(:second_child_of_first_root).should be_at_bottom
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should return false when the node is not last in the list" do
|
137
|
+
node(:first_root).should_not be_at_bottom
|
138
|
+
node(:second_root).should_not be_at_bottom
|
139
|
+
node(:first_child_of_first_root).should_not be_at_bottom
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#last_sibling_in_list' do
|
144
|
+
it "should return the last sibling in the list containing the current sibling" do
|
145
|
+
node(:first_root).last_sibling_in_list.should == node(:third_root)
|
146
|
+
node(:second_root).last_sibling_in_list.should == node(:third_root)
|
147
|
+
node(:third_root).last_sibling_in_list.should == node(:third_root)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe '#first_sibling_in_list' do
|
152
|
+
it "should return the first sibling in the list containing the current sibling" do
|
153
|
+
node(:first_root).first_sibling_in_list.should == node(:first_root)
|
154
|
+
node(:second_root).first_sibling_in_list.should == node(:first_root)
|
155
|
+
node(:third_root).first_sibling_in_list.should == node(:first_root)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'moving nodes around', :focus => true do
|
161
|
+
before(:each) do
|
162
|
+
setup_ordered_tree <<-ENDTREE
|
163
|
+
- first_root:
|
164
|
+
- first_child_of_first_root
|
165
|
+
- second_child_of_first_root
|
166
|
+
- second_root:
|
167
|
+
- first_child_of_second_root
|
168
|
+
- third_root:
|
169
|
+
- first
|
170
|
+
- second
|
171
|
+
- third
|
172
|
+
ENDTREE
|
173
|
+
end
|
174
|
+
|
175
|
+
describe '#move_below' do
|
176
|
+
it 'should fix positions within the current list when moving an sibling away from its current parent' do
|
177
|
+
node_to_move = node(:first_child_of_first_root)
|
178
|
+
new_parent = node(:second_root)
|
179
|
+
node_to_move.move_below(node(:first_child_of_second_root))
|
180
|
+
node(:second_child_of_first_root).position.should == 0
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'should work when moving to a different parent' do
|
184
|
+
node_to_move = node(:first_child_of_first_root)
|
185
|
+
new_parent = node(:second_root)
|
186
|
+
node_to_move.move_below(node(:first_child_of_second_root))
|
187
|
+
node_to_move.reload
|
188
|
+
node_to_move.should be_at_bottom
|
189
|
+
node(:first_child_of_second_root).should be_at_top
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'should be able to move the first node below the second node' do
|
193
|
+
first_node = node(:first_root)
|
194
|
+
second_node = node(:second_root)
|
195
|
+
first_node.move_below(second_node)
|
196
|
+
first_node.reload
|
197
|
+
second_node.reload
|
198
|
+
second_node.should be_at_top
|
199
|
+
first_node.higher_siblings.to_a.should == [second_node]
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'should be able to move the last node below the first node' do
|
203
|
+
first_node = node(:first_root)
|
204
|
+
last_node = node(:third_root)
|
205
|
+
last_node.move_below(first_node)
|
206
|
+
first_node.reload
|
207
|
+
last_node.reload
|
208
|
+
last_node.should_not be_at_bottom
|
209
|
+
node(:second_root).should be_at_bottom
|
210
|
+
last_node.higher_siblings.to_a.should == [first_node]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe '#move_above' do
|
215
|
+
it 'should fix positions within the current list when moving an sibling away from its current parent' do
|
216
|
+
node_to_move = node(:first_child_of_first_root)
|
217
|
+
new_parent = node(:second_root)
|
218
|
+
node_to_move.move_above(node(:first_child_of_second_root))
|
219
|
+
node(:second_child_of_first_root).position.should == 0
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'should work when moving to a different parent' do
|
223
|
+
node_to_move = node(:first_child_of_first_root)
|
224
|
+
new_parent = node(:second_root)
|
225
|
+
node_to_move.move_above(node(:first_child_of_second_root))
|
226
|
+
node_to_move.reload
|
227
|
+
node_to_move.should be_at_top
|
228
|
+
node(:first_child_of_second_root).should be_at_bottom
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'should be able to move the last node above the second node' do
|
232
|
+
last_node = node(:third_root)
|
233
|
+
second_node = node(:second_root)
|
234
|
+
last_node.move_above(second_node)
|
235
|
+
last_node.reload
|
236
|
+
second_node.reload
|
237
|
+
second_node.should be_at_bottom
|
238
|
+
last_node.higher_siblings.to_a.should == [node(:first_root)]
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'should be able to move the first node above the last node' do
|
242
|
+
first_node = node(:first_root)
|
243
|
+
last_node = node(:third_root)
|
244
|
+
first_node.move_above(last_node)
|
245
|
+
first_node.reload
|
246
|
+
last_node.reload
|
247
|
+
node(:second_root).should be_at_top
|
248
|
+
first_node.higher_siblings.to_a.should == [node(:second_root)]
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "#move_to_top" do
|
253
|
+
it "should return true when attempting to move the first sibling" do
|
254
|
+
node(:first_root).move_to_top.should == true
|
255
|
+
node(:first_child_of_first_root).move_to_top.should == true
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should be able to move the last sibling to the top" do
|
259
|
+
first_node = node(:first_root)
|
260
|
+
last_node = node(:third_root)
|
261
|
+
last_node.move_to_top
|
262
|
+
first_node.reload
|
263
|
+
last_node.should be_at_top
|
264
|
+
first_node.should_not be_at_top
|
265
|
+
first_node.higher_siblings.to_a.should == [last_node]
|
266
|
+
last_node.lower_siblings.to_a.should == [first_node, node(:second_root)]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "#move_to_bottom" do
|
271
|
+
it "should return true when attempting to move the last sibling" do
|
272
|
+
node(:third_root).move_to_bottom.should == true
|
273
|
+
node(:second_child_of_first_root).move_to_bottom.should == true
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should be able to move the first sibling to the bottom" do
|
277
|
+
first_node = node(:first_root)
|
278
|
+
middle_node = node(:second_root)
|
279
|
+
last_node = node(:third_root)
|
280
|
+
first_node.move_to_bottom
|
281
|
+
middle_node.reload
|
282
|
+
last_node.reload
|
283
|
+
first_node.should_not be_at_top
|
284
|
+
first_node.should be_at_bottom
|
285
|
+
last_node.should_not be_at_bottom
|
286
|
+
last_node.should_not be_at_top
|
287
|
+
middle_node.should be_at_top
|
288
|
+
first_node.lower_siblings.to_a.should == []
|
289
|
+
last_node.higher_siblings.to_a.should == [middle_node]
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
describe "#move_up" do
|
294
|
+
it "should correctly move nodes up" do
|
295
|
+
node(:third).move_up
|
296
|
+
node(:third_root).children.should == [node(:first), node(:third), node(:second)]
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
describe "#move_down" do
|
301
|
+
it "should correctly move nodes down" do
|
302
|
+
node(:first).move_down
|
303
|
+
node(:third_root).children.should == [node(:second), node(:first), node(:third)]
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end # moving nodes around
|
307
|
+
end # Mongoid::Tree::Ordering
|
@@ -67,6 +67,28 @@ describe Mongoid::Tree::Traversal do
|
|
67
67
|
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
68
68
|
end
|
69
69
|
|
70
|
+
describe 'with reordered nodes' do
|
71
|
+
before do
|
72
|
+
setup_ordered_tree <<-ENDTREE
|
73
|
+
node1:
|
74
|
+
- node2:
|
75
|
+
- node3
|
76
|
+
- node4:
|
77
|
+
- node6
|
78
|
+
- node5
|
79
|
+
- node7
|
80
|
+
ENDTREE
|
81
|
+
node(:node5).move_above(node(:node6))
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should return the nodes in the correct order' do
|
85
|
+
result = []
|
86
|
+
node(:node1).traverse(:depth_first) { |node| result << node }
|
87
|
+
result.collect { |n| n.name.to_sym }.should == [:node1, :node2, :node3, :node4, :node5, :node6, :node7]
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
70
92
|
end
|
71
93
|
|
72
94
|
describe 'breadth first traversal' do
|
data/spec/mongoid/tree_spec.rb
CHANGED
@@ -28,6 +28,14 @@ describe Mongoid::Tree do
|
|
28
28
|
Node.index_options.should have_key(:parent_ids)
|
29
29
|
end
|
30
30
|
|
31
|
+
describe 'when new' do
|
32
|
+
it "should not require a saved parent when adding children" do
|
33
|
+
root = Node.new(:name => 'root'); child = Node.new(:name => 'child')
|
34
|
+
expect { root.children << child; root.save! }.to_not raise_error(Mongoid::Errors::DocumentNotFound)
|
35
|
+
child.should be_persisted
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
31
39
|
describe 'when saved' do
|
32
40
|
|
33
41
|
before(:each) do
|
@@ -92,7 +100,6 @@ describe Mongoid::Tree do
|
|
92
100
|
child.should_not be_valid
|
93
101
|
child.errors[:parent_id].should_not be_nil
|
94
102
|
end
|
95
|
-
|
96
103
|
end
|
97
104
|
|
98
105
|
describe 'when subclassed' do
|
@@ -276,8 +283,19 @@ describe Mongoid::Tree do
|
|
276
283
|
end
|
277
284
|
|
278
285
|
it "#siblings_and_self should return the documents siblings and itself" do
|
286
|
+
node(:child).siblings_and_self.is_a?(Mongoid::Criteria).should == true
|
279
287
|
node(:child).siblings_and_self.to_a.should == [node(:child), node(:other_child)]
|
280
288
|
end
|
289
|
+
|
290
|
+
describe '#sibling_of?' do
|
291
|
+
it "should return true for siblings" do
|
292
|
+
node(:child).should be_sibling_of(node(:other_child))
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should return false for non-siblings" do
|
296
|
+
node(:root).should_not be_sibling_of(node(:other_child))
|
297
|
+
end
|
298
|
+
end
|
281
299
|
end
|
282
300
|
|
283
301
|
describe '#leaves' do
|
@@ -3,42 +3,45 @@ require 'yaml'
|
|
3
3
|
module Mongoid::Tree::TreeMacros
|
4
4
|
|
5
5
|
def setup_tree(tree)
|
6
|
-
create_tree(YAML.load(tree))
|
6
|
+
create_tree(YAML.load(tree), {:ordered => false})
|
7
|
+
end
|
8
|
+
|
9
|
+
def setup_ordered_tree(tree)
|
10
|
+
create_tree(YAML.load(tree), {:ordered => true})
|
7
11
|
end
|
8
12
|
|
9
13
|
def node(name)
|
10
14
|
@nodes[name].reload
|
11
15
|
end
|
12
16
|
|
13
|
-
def print_tree(node,
|
17
|
+
def print_tree(node, inspect = false, depth = 0)
|
14
18
|
print ' ' * depth
|
15
19
|
print '- ' unless depth == 0
|
16
20
|
print node.name
|
17
|
-
print " (#{node.
|
21
|
+
print " (#{node.inspect})" if inspect
|
18
22
|
print ':' if node.children.any?
|
19
23
|
print "\n"
|
20
|
-
node.children.each { |c| print_tree(c,
|
24
|
+
node.children.each { |c| print_tree(c, inspect, depth + 1) }
|
21
25
|
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
def create_tree(object)
|
27
|
+
private
|
28
|
+
def create_tree(object, opts={})
|
26
29
|
case object
|
27
|
-
when String: return create_node(object)
|
28
|
-
when Array: object.each { |tree| create_tree(tree) }
|
30
|
+
when String: return create_node(object, opts)
|
31
|
+
when Array: object.each { |tree| create_tree(tree, opts) }
|
29
32
|
when Hash:
|
30
33
|
name, children = object.first
|
31
|
-
node = create_node(name)
|
32
|
-
children.each { |c| node.children << create_tree(c) }
|
34
|
+
node = create_node(name, opts)
|
35
|
+
children.each { |c| node.children << create_tree(c, opts) }
|
33
36
|
return node
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
37
|
-
def create_node(name)
|
40
|
+
def create_node(name, opts={})
|
41
|
+
node_class = opts[:ordered] ? OrderedNode : Node
|
38
42
|
@nodes ||= HashWithIndifferentAccess.new
|
39
|
-
@nodes[name] =
|
43
|
+
@nodes[name] = node_class.create(:name => name)
|
40
44
|
end
|
41
|
-
|
42
45
|
end
|
43
46
|
|
44
47
|
RSpec.configure do |config|
|
data/spec/support/models/node.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongoid-tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 15
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 4
|
9
|
+
- 0
|
10
|
+
version: 0.4.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Benedikt Deicke
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-11-03 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -26,14 +26,14 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
hash:
|
29
|
+
hash: 62196427
|
30
30
|
segments:
|
31
31
|
- 2
|
32
32
|
- 0
|
33
33
|
- 0
|
34
34
|
- beta
|
35
|
-
-
|
36
|
-
version: 2.0.0.beta.
|
35
|
+
- 20
|
36
|
+
version: 2.0.0.beta.20
|
37
37
|
type: :runtime
|
38
38
|
version_requirements: *id001
|
39
39
|
- !ruby/object:Gem::Dependency
|
@@ -42,16 +42,13 @@ dependencies:
|
|
42
42
|
requirement: &id002 !ruby/object:Gem::Requirement
|
43
43
|
none: false
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ~>
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
hash:
|
47
|
+
hash: 3
|
48
48
|
segments:
|
49
49
|
- 2
|
50
50
|
- 0
|
51
|
-
|
52
|
-
- beta
|
53
|
-
- 18
|
54
|
-
version: 2.0.0.beta.18
|
51
|
+
version: "2.0"
|
55
52
|
type: :development
|
56
53
|
version_requirements: *id002
|
57
54
|
- !ruby/object:Gem::Dependency
|
@@ -97,8 +94,10 @@ extra_rdoc_files:
|
|
97
94
|
- README.rdoc
|
98
95
|
- LICENSE
|
99
96
|
files:
|
97
|
+
- lib/mongoid/tree/ordering.rb
|
100
98
|
- lib/mongoid/tree/traversal.rb
|
101
99
|
- lib/mongoid/tree.rb
|
100
|
+
- spec/mongoid/tree/ordering_spec.rb
|
102
101
|
- spec/mongoid/tree/traversal_spec.rb
|
103
102
|
- spec/mongoid/tree_spec.rb
|
104
103
|
- spec/spec_helper.rb
|