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 CHANGED
@@ -1,20 +1,20 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mongoid-tree (0.3.1)
5
- mongoid (>= 2.0.0.beta.17)
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.0)
11
- activesupport (= 3.0.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.0)
15
- autotest (4.4.1)
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.1)
26
- mongo (1.0.9)
27
- bson (>= 1.0.5)
28
- mongoid (2.0.0.beta.19)
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 (= 1.0.9)
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.0.rc)
36
- rspec-core (= 2.0.0.rc)
37
- rspec-expectations (= 2.0.0.rc)
38
- rspec-mocks (= 2.0.0.rc)
39
- rspec-core (2.0.0.rc)
40
- rspec-expectations (2.0.0.rc)
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.0.rc)
43
- rspec-core (= 2.0.0.rc)
44
- rspec-expectations (= 2.0.0.rc)
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.17)
55
+ mongoid (>= 2.0.0.beta.20)
56
56
  mongoid-tree!
57
- rspec (>= 2.0.0.beta.18)
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.17)
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 documents position in the tree:
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. See Mongoid::Tree::Traversal for details
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
- include Traversal
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 - [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
- private
283
-
292
+ private
284
293
  def rearrange
285
294
  if self.parent_id
286
- self.parent_ids = base_class.find(self.parent_id).parent_ids + [self.parent_id]
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
- end
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
@@ -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, print_ids = false, depth = 0)
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.id})" if print_ids
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, print_ids, depth + 1) }
24
+ node.children.each { |c| print_tree(c, inspect, depth + 1) }
21
25
  end
22
26
 
23
- private
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] = Node.create(:name => name)
43
+ @nodes[name] = node_class.create(:name => name)
40
44
  end
41
-
42
45
  end
43
46
 
44
47
  RSpec.configure do |config|
@@ -1,6 +1,7 @@
1
1
  class Node
2
2
  include Mongoid::Document
3
3
  include Mongoid::Tree
4
+ include Mongoid::Tree::Traversal
4
5
 
5
6
  field :name
6
7
  end
@@ -8,3 +9,6 @@ end
8
9
  class SubclassedNode < Node
9
10
  end
10
11
 
12
+ class OrderedNode < Node
13
+ include Mongoid::Tree::Ordering
14
+ end
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: 17
4
+ hash: 15
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 1
10
- version: 0.3.1
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-10-09 00:00:00 +02:00
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: 62196417
29
+ hash: 62196427
30
30
  segments:
31
31
  - 2
32
32
  - 0
33
33
  - 0
34
34
  - beta
35
- - 17
36
- version: 2.0.0.beta.17
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: 62196423
47
+ hash: 3
48
48
  segments:
49
49
  - 2
50
50
  - 0
51
- - 0
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