locomotive-mongoid-tree 0.6.2

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.
@@ -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