locomotive-mongoid-tree 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,195 @@
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
+ field :position, :type => Integer
37
+
38
+ default_scope asc(:position)
39
+
40
+ before_save :assign_default_position
41
+ before_save :reposition_former_siblings, :if => :sibling_reposition_required?
42
+ after_destroy :move_lower_siblings_up
43
+ end
44
+
45
+ ##
46
+ # Returns a chainable criteria for this document's ancestors
47
+ def ancestors
48
+ base_class.unscoped.where(:_id.in => parent_ids)
49
+ end
50
+
51
+ ##
52
+ # Returns siblings below the current document.
53
+ # Siblings with a position greater than this documents's position.
54
+ def lower_siblings
55
+ self.siblings.where(:position.gt => self.position)
56
+ end
57
+
58
+ ##
59
+ # Returns siblings above the current document.
60
+ # Siblings with a position lower than this documents's position.
61
+ def higher_siblings
62
+ self.siblings.where(:position.lt => self.position)
63
+ end
64
+
65
+ ##
66
+ # Returns the lowest sibling (could be self)
67
+ def last_sibling_in_list
68
+ siblings_and_self.last
69
+ end
70
+
71
+ ##
72
+ # Returns the highest sibling (could be self)
73
+ def first_sibling_in_list
74
+ siblings_and_self.first
75
+ end
76
+
77
+ ##
78
+ # Is this the highest sibling?
79
+ def at_top?
80
+ higher_siblings.empty?
81
+ end
82
+
83
+ ##
84
+ # Is this the lowest sibling?
85
+ def at_bottom?
86
+ lower_siblings.empty?
87
+ end
88
+
89
+ ##
90
+ # Move this node above all its siblings
91
+ def move_to_top
92
+ return true if at_top?
93
+ move_above(first_sibling_in_list)
94
+ end
95
+
96
+ ##
97
+ # Move this node below all its siblings
98
+ def move_to_bottom
99
+ return true if at_bottom?
100
+ move_below(last_sibling_in_list)
101
+ end
102
+
103
+ ##
104
+ # Move this node one position up
105
+ def move_up
106
+ return if at_top?
107
+ siblings.where(:position => self.position - 1).first.inc(:position, 1)
108
+ inc(:position, -1)
109
+ end
110
+
111
+ ##
112
+ # Move this node one position down
113
+ def move_down
114
+ return if at_bottom?
115
+ siblings.where(:position => self.position + 1).first.inc(:position, -1)
116
+ inc(:position, 1)
117
+ end
118
+
119
+ ##
120
+ # Move this node above the specified node
121
+ #
122
+ # This method changes the node's parent if nescessary.
123
+ def move_above(other)
124
+ unless sibling_of?(other)
125
+ self.parent_id = other.parent_id
126
+ save!
127
+ end
128
+
129
+ if position > other.position
130
+ new_position = other.position
131
+ other.lower_siblings.where(:position.lt => self.position).each { |s| s.inc(:position, 1) }
132
+ other.inc(:position, 1)
133
+ self.position = new_position
134
+ save!
135
+ else
136
+ new_position = other.position - 1
137
+ other.higher_siblings.where(:position.gt => self.position).each { |s| s.inc(:position, -1) }
138
+ self.position = new_position
139
+ save!
140
+ end
141
+ end
142
+
143
+ ##
144
+ # Move this node below the specified node
145
+ #
146
+ # This method changes the node's parent if nescessary.
147
+ def move_below(other)
148
+ unless sibling_of?(other)
149
+ self.parent_id = other.parent_id
150
+ save!
151
+ end
152
+
153
+ if position > other.position
154
+ new_position = other.position + 1
155
+ other.lower_siblings.where(:position.lt => self.position).each { |s| s.inc(:position, 1) }
156
+ self.position = new_position
157
+ save!
158
+ else
159
+ new_position = other.position
160
+ other.higher_siblings.where(:position.gt => self.position).each { |s| s.inc(:position, -1) }
161
+ other.inc(:position, -1)
162
+ self.position = new_position
163
+ save!
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def move_lower_siblings_up
170
+ lower_siblings.each { |s| s.inc(:position, -1) }
171
+ end
172
+
173
+ def reposition_former_siblings
174
+ former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
175
+ and(:position.gt => (attribute_was('position') || 0)).
176
+ excludes(:id => self.id)
177
+ former_siblings.each { |s| s.inc(:position, -1) }
178
+ end
179
+
180
+ def sibling_reposition_required?
181
+ parent_id_changed? && persisted?
182
+ end
183
+
184
+ def assign_default_position
185
+ return unless self.position.nil? || self.parent_id_changed?
186
+
187
+ if self.siblings.empty? || self.siblings.collect(&:position).compact.empty?
188
+ self.position = 0
189
+ else
190
+ self.position = self.siblings.max(:position).to_i + 1
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,119 @@
1
+ module Mongoid # :nodoc:
2
+ module Tree
3
+ ##
4
+ # = Mongoid::Tree::Traversal
5
+ #
6
+ # Mongoid::Tree::Traversal provides a #traverse method to walk through the tree.
7
+ # It supports these traversal methods:
8
+ #
9
+ # * depth_first
10
+ # * breadth_first
11
+ #
12
+ # == Depth First Traversal
13
+ #
14
+ # See http://en.wikipedia.org/wiki/Depth-first_search for a proper description.
15
+ #
16
+ # Given a tree like:
17
+ #
18
+ # node1:
19
+ # - node2:
20
+ # - node3
21
+ # - node4:
22
+ # - node5
23
+ # - node6
24
+ # - node7
25
+ #
26
+ # Traversing the tree using depth first traversal would visit each node in this order:
27
+ #
28
+ # node1, node2, node3, node4, node5, node6, node7
29
+ #
30
+ # == Breadth First Traversal
31
+ #
32
+ # See http://en.wikipedia.org/wiki/Breadth-first_search for a proper description.
33
+ #
34
+ # Given a tree like:
35
+ #
36
+ # node1:
37
+ # - node2:
38
+ # - node5
39
+ # - node3:
40
+ # - node6
41
+ # - node7
42
+ # - node4
43
+ #
44
+ # Traversing the tree using breadth first traversal would visit each node in this order:
45
+ #
46
+ # node1, node2, node3, node4, node5, node6, node7
47
+ #
48
+ module Traversal
49
+ extend ActiveSupport::Concern
50
+
51
+ ##
52
+ # :singleton-method: traverse
53
+ # Traverses the entire tree, one root at a time, using the given traversal
54
+ # method (Default is :depth_first).
55
+ #
56
+ # See Mongoid::Tree::Traversal for available traversal methods.
57
+ #
58
+ # Example:
59
+ #
60
+ # # Say we have the following tree, and want to print its hierarchy:
61
+ # # root_1
62
+ # # child_1_a
63
+ # # root_2
64
+ # # child_2_a
65
+ # # child_2_a_1
66
+ #
67
+ # Node.traverse(:depth_first) do |node|
68
+ # indentation = ' ' * node.depth
69
+ #
70
+ # puts "#{indentation}#{node.name}"
71
+ # end
72
+ #
73
+
74
+ ##
75
+ # The methods in this module are class-level methods documented above.
76
+ # They're extended into the base class automatically.
77
+ module ClassMethods # :nodoc:
78
+ def traverse(type = :depth_first, &block)
79
+ raise ArgumentError, "No block given" unless block_given?
80
+ roots.each { |root| root.traverse(type, &block) }
81
+ nil
82
+ end
83
+ end
84
+
85
+ ##
86
+ # Traverses the tree using the given traversal method (Default is :depth_first)
87
+ # and passes each document node to the block.
88
+ #
89
+ # See Mongoid::Tree::Traversal for available traversal methods.
90
+ #
91
+ # Example:
92
+ #
93
+ # results = []
94
+ # root.traverse(:depth_first) do |node|
95
+ # results << node
96
+ # end
97
+ def traverse(type = :depth_first, &block)
98
+ raise ArgumentError, "No block given" unless block_given?
99
+ send("#{type}_traversal", &block)
100
+ end
101
+
102
+ private
103
+
104
+ def depth_first_traversal(&block)
105
+ block.call(self)
106
+ self.children.each { |c| c.send(:depth_first_traversal, &block) }
107
+ end
108
+
109
+ def breadth_first_traversal(&block)
110
+ queue = [self]
111
+ while queue.any? do
112
+ node = queue.shift
113
+ block.call(node)
114
+ queue += node.children
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,328 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::Tree::Ordering do
4
+
5
+ subject { OrderedNode }
6
+
7
+ it "should store position as an Integer with a default of nil" do
8
+ f = OrderedNode.fields['position']
9
+ f.should_not be_nil
10
+ f.options[:type].should == Integer
11
+ f.options[:default].should == nil
12
+ end
13
+
14
+ describe 'when saved' do
15
+ before(:each) do
16
+ setup_tree <<-ENDTREE
17
+ - root:
18
+ - child:
19
+ - subchild:
20
+ - subsubchild
21
+ - other_root:
22
+ - other_child
23
+ - another_child
24
+ ENDTREE
25
+ end
26
+
27
+ it "should assign a default position of 0 to each node without a sibling" do
28
+ node(:child).position.should == 0
29
+ node(:subchild).position.should == 0
30
+ node(:subsubchild).position.should == 0
31
+ end
32
+
33
+ it "should place siblings at the end of the list by default" do
34
+ node(:root).position.should == 0
35
+ node(:other_root).position.should == 1
36
+ node(:other_child).position.should == 0
37
+ node(:another_child).position.should == 1
38
+ end
39
+
40
+ it "should move a node to the end of a list when it is moved to a new parent" do
41
+ other_root = node(:other_root)
42
+ child = node(:child)
43
+ child.position.should == 0
44
+ other_root.children << child
45
+ child.reload
46
+ child.position.should == 2
47
+ end
48
+
49
+ it "should correctly reposition siblings when one of them is removed" do
50
+ node(:other_child).destroy
51
+ node(:another_child).position.should == 0
52
+ end
53
+
54
+ it "should correctly reposition siblings when one of them is added to another parent" do
55
+ node(:root).children << node(:other_child)
56
+ node(:another_child).position.should == 0
57
+ end
58
+
59
+ it "should correctly reposition siblings when the parent is changed" do
60
+ other_child = node(:other_child)
61
+ other_child.parent = node(:root)
62
+ other_child.save!
63
+ node(:another_child).position.should == 0
64
+ end
65
+
66
+ it "should not reposition siblings when it's not yet saved" do
67
+ new_node = OrderedNode.new(:name => 'new')
68
+ new_node.parent = node(:root)
69
+ new_node.should_not_receive(:reposition_former_siblings)
70
+ new_node.save
71
+ end
72
+ end
73
+
74
+ describe 'destroy strategies' do
75
+ before(:each) do
76
+ setup_tree <<-ENDTREE
77
+ - root:
78
+ - child:
79
+ - subchild
80
+ - other_child
81
+ - other_root
82
+ ENDTREE
83
+ end
84
+
85
+ describe ':move_children_to_parent' do
86
+ it "should set its childen's parent_id to the documents parent_id" do
87
+ node(:child).move_children_to_parent
88
+ node(:child).should be_leaf
89
+ node(:root).children.to_a.should == [node(:child), node(:other_child), node(:subchild)]
90
+ end
91
+ end
92
+ end
93
+
94
+ describe 'utility methods' do
95
+ before(:each) do
96
+ setup_tree <<-ENDTREE
97
+ - first_root:
98
+ - first_child_of_first_root
99
+ - second_child_of_first_root
100
+ - second_root
101
+ - third_root
102
+ ENDTREE
103
+ end
104
+
105
+ describe '#lower_siblings' do
106
+ it "should return a collection of siblings lower on the list" do
107
+ node(:second_child_of_first_root).reload
108
+ node(:first_root).lower_siblings.to_a.should == [node(:second_root), node(:third_root)]
109
+ node(:second_root).lower_siblings.to_a.should == [node(:third_root)]
110
+ node(:third_root).lower_siblings.to_a.should == []
111
+ node(:first_child_of_first_root).lower_siblings.to_a.should == [node(:second_child_of_first_root)]
112
+ node(:second_child_of_first_root).lower_siblings.to_a.should == []
113
+ end
114
+ end
115
+
116
+ describe '#higher_siblings' do
117
+ it "should return a collection of siblings lower on the list" do
118
+ node(:first_root).higher_siblings.to_a.should == []
119
+ node(:second_root).higher_siblings.to_a.should == [node(:first_root)]
120
+ node(:third_root).higher_siblings.to_a.should == [node(:first_root), node(:second_root)]
121
+ node(:first_child_of_first_root).higher_siblings.to_a.should == []
122
+ node(:second_child_of_first_root).higher_siblings.to_a.should == [node(:first_child_of_first_root)]
123
+ end
124
+ end
125
+
126
+ describe '#at_top?' do
127
+ it "should return true when the node is first in the list" do
128
+ node(:first_root).should be_at_top
129
+ node(:first_child_of_first_root).should be_at_top
130
+ end
131
+
132
+ it "should return false when the node is not first in the list" do
133
+ node(:second_root).should_not be_at_top
134
+ node(:third_root).should_not be_at_top
135
+ node(:second_child_of_first_root).should_not be_at_top
136
+ end
137
+ end
138
+
139
+ describe '#at_bottom?' do
140
+ it "should return true when the node is last in the list" do
141
+ node(:third_root).should be_at_bottom
142
+ node(:second_child_of_first_root).should be_at_bottom
143
+ end
144
+
145
+ it "should return false when the node is not last in the list" do
146
+ node(:first_root).should_not be_at_bottom
147
+ node(:second_root).should_not be_at_bottom
148
+ node(:first_child_of_first_root).should_not be_at_bottom
149
+ end
150
+ end
151
+
152
+ describe '#last_sibling_in_list' do
153
+ it "should return the last sibling in the list containing the current sibling" do
154
+ node(:first_root).last_sibling_in_list.should == node(:third_root)
155
+ node(:second_root).last_sibling_in_list.should == node(:third_root)
156
+ node(:third_root).last_sibling_in_list.should == node(:third_root)
157
+ end
158
+ end
159
+
160
+ describe '#first_sibling_in_list' do
161
+ it "should return the first sibling in the list containing the current sibling" do
162
+ node(:first_root).first_sibling_in_list.should == node(:first_root)
163
+ node(:second_root).first_sibling_in_list.should == node(:first_root)
164
+ node(:third_root).first_sibling_in_list.should == node(:first_root)
165
+ end
166
+ end
167
+
168
+ describe 'ancestors' do
169
+ it "#ancestors should be returned in the correct order" do
170
+ setup_tree <<-ENDTREE
171
+ - root:
172
+ - level_1_a
173
+ - level_1_b:
174
+ - level_2_a:
175
+ - leaf
176
+ ENDTREE
177
+
178
+ node(:leaf).ancestors.to_a.should == [node(:root), node(:level_1_b), node(:level_2_a)]
179
+ end
180
+ end
181
+ end
182
+
183
+ describe 'moving nodes around' do
184
+ before(:each) do
185
+ setup_tree <<-ENDTREE
186
+ - first_root:
187
+ - first_child_of_first_root
188
+ - second_child_of_first_root
189
+ - second_root:
190
+ - first_child_of_second_root
191
+ - third_root:
192
+ - first
193
+ - second
194
+ - third
195
+ ENDTREE
196
+ end
197
+
198
+ describe '#move_below' do
199
+ it 'should fix positions within the current list when moving an sibling away from its current parent' do
200
+ node_to_move = node(:first_child_of_first_root)
201
+ node_to_move.move_below(node(:first_child_of_second_root))
202
+ node(:second_child_of_first_root).position.should == 0
203
+ end
204
+
205
+ it 'should work when moving to a different parent' do
206
+ node_to_move = node(:first_child_of_first_root)
207
+ new_parent = node(:second_root)
208
+ node_to_move.move_below(node(:first_child_of_second_root))
209
+ node_to_move.reload
210
+ node_to_move.should be_at_bottom
211
+ node(:first_child_of_second_root).should be_at_top
212
+ end
213
+
214
+ it 'should be able to move the first node below the second node' do
215
+ first_node = node(:first_root)
216
+ second_node = node(:second_root)
217
+ first_node.move_below(second_node)
218
+ first_node.reload
219
+ second_node.reload
220
+ second_node.should be_at_top
221
+ first_node.higher_siblings.to_a.should == [second_node]
222
+ end
223
+
224
+ it 'should be able to move the last node below the first node' do
225
+ first_node = node(:first_root)
226
+ last_node = node(:third_root)
227
+ last_node.move_below(first_node)
228
+ first_node.reload
229
+ last_node.reload
230
+ last_node.should_not be_at_bottom
231
+ node(:second_root).should be_at_bottom
232
+ last_node.higher_siblings.to_a.should == [first_node]
233
+ end
234
+ end
235
+
236
+ describe '#move_above' do
237
+ it 'should fix positions within the current list when moving an sibling away from its current parent' do
238
+ node_to_move = node(:first_child_of_first_root)
239
+ node_to_move.move_above(node(:first_child_of_second_root))
240
+ node(:second_child_of_first_root).position.should == 0
241
+ end
242
+
243
+ it 'should work when moving to a different parent' do
244
+ node_to_move = node(:first_child_of_first_root)
245
+ new_parent = node(:second_root)
246
+ node_to_move.move_above(node(:first_child_of_second_root))
247
+ node_to_move.reload
248
+ node_to_move.should be_at_top
249
+ node(:first_child_of_second_root).should be_at_bottom
250
+ end
251
+
252
+ it 'should be able to move the last node above the second node' do
253
+ last_node = node(:third_root)
254
+ second_node = node(:second_root)
255
+ last_node.move_above(second_node)
256
+ last_node.reload
257
+ second_node.reload
258
+ second_node.should be_at_bottom
259
+ last_node.higher_siblings.to_a.should == [node(:first_root)]
260
+ end
261
+
262
+ it 'should be able to move the first node above the last node' do
263
+ first_node = node(:first_root)
264
+ last_node = node(:third_root)
265
+ first_node.move_above(last_node)
266
+ first_node.reload
267
+ last_node.reload
268
+ node(:second_root).should be_at_top
269
+ first_node.higher_siblings.to_a.should == [node(:second_root)]
270
+ end
271
+ end
272
+
273
+ describe "#move_to_top" do
274
+ it "should return true when attempting to move the first sibling" do
275
+ node(:first_root).move_to_top.should == true
276
+ node(:first_child_of_first_root).move_to_top.should == true
277
+ end
278
+
279
+ it "should be able to move the last sibling to the top" do
280
+ first_node = node(:first_root)
281
+ last_node = node(:third_root)
282
+ last_node.move_to_top
283
+ first_node.reload
284
+ last_node.should be_at_top
285
+ first_node.should_not be_at_top
286
+ first_node.higher_siblings.to_a.should == [last_node]
287
+ last_node.lower_siblings.to_a.should == [first_node, node(:second_root)]
288
+ end
289
+ end
290
+
291
+ describe "#move_to_bottom" do
292
+ it "should return true when attempting to move the last sibling" do
293
+ node(:third_root).move_to_bottom.should == true
294
+ node(:second_child_of_first_root).move_to_bottom.should == true
295
+ end
296
+
297
+ it "should be able to move the first sibling to the bottom" do
298
+ first_node = node(:first_root)
299
+ middle_node = node(:second_root)
300
+ last_node = node(:third_root)
301
+ first_node.move_to_bottom
302
+ middle_node.reload
303
+ last_node.reload
304
+ first_node.should_not be_at_top
305
+ first_node.should be_at_bottom
306
+ last_node.should_not be_at_bottom
307
+ last_node.should_not be_at_top
308
+ middle_node.should be_at_top
309
+ first_node.lower_siblings.to_a.should == []
310
+ last_node.higher_siblings.to_a.should == [middle_node]
311
+ end
312
+ end
313
+
314
+ describe "#move_up" do
315
+ it "should correctly move nodes up" do
316
+ node(:third).move_up
317
+ node(:third_root).children.should == [node(:first), node(:third), node(:second)]
318
+ end
319
+ end
320
+
321
+ describe "#move_down" do
322
+ it "should correctly move nodes down" do
323
+ node(:first).move_down
324
+ node(:third_root).children.should == [node(:second), node(:first), node(:third)]
325
+ end
326
+ end
327
+ end # moving nodes around
328
+ end # Mongoid::Tree::Ordering