mm-tree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Leif Ringstad
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,121 @@
1
+ = mm-tree
2
+
3
+ This is a tree structure for MongoMapper documents that support rational numbers for positioning.
4
+ Read about rational numbers in tree structures here: http://arxiv.org/pdf/0806.3115v1.pdf
5
+
6
+ The reason for the changed implementation to use rational numbers is to be able to query a tree and get the tree structure by sorting on the rational number. It also makes it easy to query parts of a tree as well.
7
+
8
+ Rational numbers is even better than left/right trees, as you can remove parts of a tree or a node without reordering the entire tree. It is a bit more complicated, but there are some really good benefits.
9
+
10
+ {<img src="https://secure.travis-ci.org/leifcr/mm-tree.png?branch=master" alt="Build Status" />}[http://travis-ci.org/leifcr/mm-tree]
11
+
12
+ == Installation
13
+
14
+ I assume you are using bundler, so add this to your Gemfile:
15
+ gem 'mm-tree', :git => 'http://github.com/leifcr/mm-tree.git'
16
+
17
+ == Usage
18
+
19
+ Enable the tree functionality by adding the plugin on your model
20
+
21
+ class Category
22
+ include MongoMapper::Document
23
+ plugin MongoMapper::Plugins::Tree
24
+
25
+ key :name, String
26
+ end
27
+
28
+ *Note:* Rational numbers positioning is enabled by default!
29
+
30
+ This adds one embedded tree_info (non-changeable) and the following class attributes:
31
+
32
+ * _tree_parent_id_field_ overrides the field used for parent_id (default: parent_id)
33
+ * _tree_search_class_ expects a Class that is a MongoMapper::Document to be used for search (So you can have one collection with inherited models and trees for each model, not conflicting with each other)
34
+ * _tree_use_rational_numbers_ use rational numbers for sorting. set to false if you don't want it.
35
+ * _tree_order_ controls the order if rational numbers aren't used (format :field_name.[asc|desc]), else it will sort by rational numbers.
36
+
37
+ If you want to use explicit _tree_order_, you *have to* set _tree_use_rational_numbers_ to false.
38
+
39
+ == Configuration Examples
40
+ Not using rational numbers, sorting by name, and using a different ID field.
41
+ class Category
42
+ include MongoMapper::Document
43
+ plugin MongoMapper::Plugins::Tree
44
+ self.tree_parent_id_field = "my_super_parent_id"
45
+ self.tree_use_rational_numbers = false
46
+ self.tree_order = :name.asc
47
+
48
+ key :name, String
49
+ end
50
+
51
+ Using rational numbers, and using search classes to have inherited models in same collection but different trees:
52
+ class Shape
53
+ include MongoMapper::Document
54
+ plugin MongoMapper::Plugins::Tree
55
+ self.tree_search_class = Shape
56
+
57
+ key :name, String
58
+ end
59
+
60
+ class Circle < Shape
61
+ self.tree_search_class = Circle
62
+ end
63
+
64
+ class Square < Shape
65
+ self.tree_search_class = Square
66
+ end
67
+
68
+ Using rational numbers, and using search classes to have inherited models in same collection and same tree:
69
+ class Shape
70
+ include MongoMapper::Document
71
+ plugin MongoMapper::Plugins::Tree
72
+ self.tree_search_class = Shape
73
+
74
+ key :name, String
75
+ end
76
+
77
+ class Circle < Shape
78
+ end
79
+
80
+ class Square < Shape
81
+ end
82
+
83
+ == Example for moving parents
84
+ To move a child node from one parent to another you can do either move to a specific rational number, or just set the parent.
85
+
86
+ = Move using parent
87
+ node_1 = Category.create(:name => "Node 1")
88
+ node_1_1 = Category.create(:name => "Node 1.1", :parent => @node_1)
89
+ node_2 = Category.create(:name => "Node 2")
90
+ node_1_1.parent = node_2
91
+ node_1_1.save
92
+ node_1_1.parent.name # => "Node 2"
93
+
94
+ = Move using rational values (nv/dv)
95
+ node_1 = Category.create(:name => "Node 1")
96
+ node_1_1 = Category.create(:name => "Node 1.1", :parent => @node_1)
97
+ node_2 = Category.create(:name => "Node 2")
98
+ node_2.set_position(node_1_1.tree_info.nv, node_1_1.tree_info.dv) # move to position of node_1_1
99
+ node_2.save
100
+ node_2.siblings.first.name # => "Node 1.1"
101
+ node_2.parent.name # => "Node 1"
102
+ # Node 2 is now in front of Node 1.1 as it has taken node 1.1's place.
103
+
104
+ Check test_order.rb, test_tree.rb and test_search_class.rb for more examples and details on usage.
105
+
106
+ == Note on Patches/Pull Requests
107
+
108
+ * Fork the project.
109
+ * Make your feature addition or bug fix.
110
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
111
+ * Send me a pull request, if you have features you like to see implemented.
112
+
113
+ == Thanks
114
+ _Jakob Vidmar_ (For the original MongoMapper Tree)
115
+ _Joel Junström_ (I based this tree on his refactoring of Jakobs MongoMapper Tree)
116
+ _MongoMapper devels_ (John Nunemaker, Brandon Keepers & others)
117
+
118
+ == Copyright
119
+ Original ideas are Copyright Jakob Vidmar and Joel Junström. Please see their github repositories for details
120
+ Copyright (c) 2012 Leif Ringstad.
121
+ See LICENSE for details.
data/lib/locale/en.yml ADDED
@@ -0,0 +1,8 @@
1
+ en:
2
+ mongo_mapper:
3
+ errors:
4
+ messages:
5
+ tree:
6
+ cyclic: "Can't be children of a descendant"
7
+ incorrect_parent_nv_dv: "Positional values doesn't match parent (check nv/dv values)"
8
+ search_class_mismatch: "Mismatch between search classes. Parent: %{parent_search_class} Node: %{node_search_class}. They must be equal"
data/lib/mm-tree.rb ADDED
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+ require 'mongo_mapper'
3
+ I18n.load_path << File.expand_path('../locale/en.yml', __FILE__)
4
+ require 'mongo_mapper/plugins/tree'
5
+ require 'mongo_mapper/plugins/tree_info'
@@ -0,0 +1,439 @@
1
+ require 'pp'
2
+ # encoding: UTF-8
3
+ module MongoMapper
4
+ module Plugins
5
+ module Tree
6
+ @@_disable_timestamp_count = 0
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ def roots
12
+ self.where(tree_parent_id_field => nil).sort(tree_sort_order()).all
13
+ end
14
+
15
+ def position_from_nv_dv(nv, dv)
16
+ anc_tree_keys = ancestor_tree_keys(nv, dv)
17
+ (nv - anc_tree_keys[:nv]) / anc_tree_keys[:snv]
18
+ end
19
+
20
+ # returns ancestor nv, dv, snv, sdv values as hash
21
+ def ancestor_tree_keys(nv,dv)
22
+ numerator = nv
23
+ denominator = dv
24
+ ancnv = 0
25
+ ancdv = 1
26
+ ancsnv = 1
27
+ ancsdv = 0
28
+ rethash = {:nv => ancnv, :dv => ancdv, :snv => ancsnv, :sdv => ancsdv}
29
+ # make sure we break if we get root values! (numerator == 0 + denominator == 0)
30
+ #max_levels = 10
31
+ while ((ancnv < nv) && (ancdv < dv)) && ((numerator > 0) && (denominator > 0))# && (max_levels > 0)
32
+ #max_levels -= 1
33
+ div = numerator / denominator
34
+ mod = numerator % denominator
35
+ # set return values to previous values, as they are the parent values
36
+ rethash = {:nv => ancnv, :dv => ancdv, :snv => ancsnv, :sdv => ancsdv}
37
+
38
+ ancnv = ancnv + (div * ancsnv)
39
+ ancdv = ancdv + (div * ancsdv)
40
+ ancsnv = ancnv + ancsnv
41
+ ancsdv = ancdv + ancsdv
42
+
43
+ numerator = mod
44
+ if (numerator != 0)
45
+ denominator = denominator % mod
46
+ if denominator == 0
47
+ denominator = 1
48
+ end
49
+ end
50
+ end
51
+ return rethash
52
+ end #get_ancestor_keys(nv,dv)
53
+
54
+ def tree_sort_order
55
+ if !tree_use_rational_numbers
56
+ "#{tree_order} tree_info.depth.asc"
57
+ else
58
+ "tree_info.nv_div_dv.asc"
59
+ end
60
+ end
61
+
62
+ end # Module ClassMethods
63
+
64
+ def initialize(*args)
65
+ @_will_move = false
66
+ @_set_nv_dv = false
67
+ if (self.tree_info == nil)
68
+ self.tree_info = TreeInfo.new
69
+ end
70
+ super
71
+ end
72
+
73
+
74
+ included do
75
+ # Tree search class will be used as the base from which to
76
+ # find tree objects. This is handy should you have a tree of objects that are of different types, but
77
+ # might be related through single table inheritance.
78
+ #
79
+ # self.tree_search_class = Shape
80
+ #
81
+ # In the above example, you could have a working tree ofShape, Circle and Square types (assuming
82
+ # Circle and Square were subclasses of Shape). If you want to do the same thing and you don't provide
83
+ # tree_search_class, nesting mixed types will not work.
84
+ class_attribute :tree_search_class
85
+ self.tree_search_class ||= self
86
+
87
+ class_attribute :tree_parent_id_field
88
+ self.tree_parent_id_field ||= "parent_id"
89
+
90
+ class_attribute :tree_use_rational_numbers
91
+ self.tree_use_rational_numbers ||= true
92
+
93
+ class_attribute :tree_order
94
+
95
+ key tree_parent_id_field, ObjectId
96
+ one :tree_info
97
+ #
98
+ # An index for path field, left_field and right_field is recommended for faster queries.
99
+
100
+ belongs_to :parent, :class => tree_search_class
101
+
102
+ # FIX VALIDATIONS... this is messy!
103
+ validate :will_save_tree
104
+
105
+ before_validation :set_nv_dv_if_missing
106
+
107
+ after_validation :update_tree_info
108
+ after_save :move_children
109
+ before_destroy :destroy_descendants
110
+ end
111
+
112
+ def tree_search_class
113
+ self.class.tree_search_class
114
+ end
115
+
116
+ def will_save_tree
117
+ if parent
118
+ errors.add(:base, I18n.t(:cyclic, :scope => [:mongo_mapper, :errors, :messages, :tree])) if self.descendants.include?(parent)
119
+ if (parent.tree_search_class != self.tree_search_class)
120
+ errors.add(:base, I18n.t(:search_class_mismatch, { \
121
+ :parent_search_class => parent.class.tree_search_class, \
122
+ :node_search_class => self.class.tree_search_class, \
123
+ :scope => [:mongo_mapper, :errors, :messages, :tree]}))
124
+ end
125
+ end
126
+ if (self.tree_info.changes.include?("nv") && self.tree_info.changes.include?("dv") && self.changes.include?(tree_parent_id_field))
127
+ if !correct_parent?(self.tree_info.nv, self.tree_info.dv)
128
+ errors.add(:base, I18n.t(:cyclic, :scope => [:mongo_mapper, :errors, :messages, :tree]))
129
+ end
130
+ end
131
+ end
132
+
133
+ def update_tree_info
134
+ update_path();
135
+ update_nv_dv();
136
+ end
137
+
138
+ def update_path(opts = {})
139
+ if parent.nil?
140
+ self[tree_parent_id_field] = nil
141
+ self.tree_info.path = []
142
+ self.tree_info.depth = 0
143
+ elsif !!opts[:force] || self.changes.include?(tree_parent_id_field)
144
+ @_will_move = true
145
+ self.tree_info.path = parent.tree_info.path + [parent._id]
146
+ self.tree_info.depth = parent.tree_info.depth + 1
147
+ end
148
+ end
149
+
150
+ def update_path!
151
+ update_path(:force => true)
152
+ end
153
+
154
+ def set_position(nv, dv)
155
+ self.tree_info.nv = nv
156
+ self.tree_info.dv = dv
157
+ end
158
+
159
+ # TODO: what if we move parent without providing NV/DV??? NEED TO SUPPORT THAT AS WELL!
160
+ # Should calculate next free nv/dv and set that if parent has changed. (set values to "missing and call missing function should work")
161
+ def update_nv_dv(opts = {})
162
+ return if !tree_use_rational_numbers
163
+ if @_set_nv_dv == true
164
+ @_set_nv_dv = false
165
+ return
166
+ end
167
+ # if changes include both parent_id, tree_info.nv and tree_info.dv,
168
+ # checking in validatioon that the parent is correct.
169
+ # if change is only nv/dv, check if parent is correct, move it...
170
+ if (self.tree_info.changes.include?("nv") && self.tree_info.changes.include?("dv"))
171
+ self.move_nv_dv(self.tree_info.nv, self.tree_info.dv)
172
+ elsif (self.changes.include?(tree_parent_id_field)) || opts[:force]
173
+ # only changed parent, needs to find next free position
174
+ # use function for "missing nv/dv"
175
+ # TODO CHECK THIS!!!! might only need self.has_siblings? instead of + 1
176
+ new_keys = self.next_keys_available(self[tree_parent_id_field], (self.has_siblings? + 1)) if !opts[:position]
177
+ new_keys = self.next_keys_available(self[tree_parent_id_field], (opts[:position] + 1)) if opts[:position]
178
+ self.move_nv_dv(new_keys[:nv], new_keys[:dv])
179
+ end
180
+ end
181
+
182
+ def update_nv_dv!(opts = {})
183
+ update_nv_dv({:force => true}.merge(opts))
184
+ end
185
+
186
+ # sets initial nv, dv, snv and sdv values
187
+ def set_nv_dv_if_missing
188
+ return if !tree_use_rational_numbers
189
+ if (self.tree_info.nv == 0 || self.tree_info.dv == 0 )
190
+ new_keys = self.next_keys_available(self[tree_parent_id_field], (self.has_siblings? + 1) )
191
+ self.tree_info.nv = new_keys[:nv]
192
+ self.tree_info.dv = new_keys[:dv]
193
+ self.tree_info.snv = new_keys[:snv]
194
+ self.tree_info.sdv = new_keys[:sdv]
195
+ self.tree_info.nv_div_dv = Float(new_keys[:nv]/Float(new_keys[:dv]))
196
+ @_set_nv_dv = true
197
+ end
198
+ end
199
+
200
+
201
+ # if conflcting item on new position, shift all siblings right and insertg
202
+ # can force move without updating conflicting siblings
203
+ def move_nv_dv(nv, dv, opts = {})
204
+ return if !tree_use_rational_numbers
205
+ # return
206
+ # nv_div_dv = Float(nv)/Float(dv)
207
+ # find nv_div_dv?
208
+ position = self.class.position_from_nv_dv(nv, dv)
209
+ if !self.root?
210
+ anc_keys = self.class.ancestor_tree_keys(nv, dv)
211
+ rnv = anc_keys[:nv] + ((position + 1) * anc_keys[:snv])
212
+ rdv = anc_keys[:dv] + ((position + 1) * anc_keys[:sdv])
213
+ else
214
+ rnv = position + 1
215
+ rdv = 1
216
+ end
217
+
218
+ # don't check for conflict if forced move
219
+ if (!opts[:ignore_conflict])
220
+ conflicting_sibling = tree_search_class.where("tree_info.nv" => nv).where("tree_info.dv" => dv).first
221
+ if (conflicting_sibling != nil)
222
+ self.disable_timestamp_callback()
223
+ # find nv/dv to the right of conflict
224
+ # find position/count for this item
225
+ next_keys = conflicting_sibling.next_sibling_keys
226
+ conflicting_sibling.set_position(next_keys[:nv], next_keys[:dv])
227
+ conflicting_sibling.save
228
+ self.enable_timestamp_callback()
229
+ end
230
+ end
231
+
232
+ # shouldn't be any conflicting sibling now...
233
+ self.tree_info.nv = nv
234
+ self.tree_info.dv = dv
235
+ self.tree_info.snv = rnv
236
+ self.tree_info.sdv = rdv
237
+ self.tree_info.nv_div_dv = Float(self.tree_info.nv)/Float(self.tree_info.dv)
238
+ # as this is triggered from after_validation, save should be triggered by the caller.
239
+ end
240
+ # change this require ancestor data + position,
241
+ # next position can be found using: self.has_siblings? + 1
242
+ # as when moving children, the sibling_count won't work
243
+ def next_keys_available(parent_id, position)
244
+ _parent = tree_search_class.where(:_id => parent_id).first
245
+ _parent = nil if ((_parent.nil?) || (_parent == []))
246
+ ancnv = 0
247
+ ancsnv = 1
248
+ ancdv = 1
249
+ ancsdv = 0
250
+ if _parent != nil
251
+ ancnv = _parent.tree_info.nv
252
+ ancsnv = _parent.tree_info.snv
253
+ ancdv = _parent.tree_info.dv
254
+ ancsdv = _parent.tree_info.sdv
255
+ end
256
+ if (position == 0) && (_parent.nil?)
257
+ rethash = {:nv => 1, :dv => 1, :snv => 2, :sdv => 1}
258
+ else
259
+ # get values from sibling_count
260
+ _nv = ancnv + (position * ancsnv)
261
+ _dv = ancdv + (position * ancsdv)
262
+ rethash = {
263
+ :nv => _nv,
264
+ :dv => _dv,
265
+ :snv => ancnv + ((position + 1) * ancsnv),
266
+ :sdv => ancdv + ((position + 1) * ancsdv)
267
+ }
268
+ end
269
+ rethash
270
+ end
271
+
272
+ def next_sibling_keys
273
+ next_keys_available(self[tree_parent_id_field], self.class.position_from_nv_dv(self.tree_info.nv, self.tree_info.dv) +1)
274
+ end
275
+
276
+ # to save queries, this will calculate ancestor tree keys instead of query them
277
+ def ancestor_tree_keys
278
+ self.class.ancestor_tree_keys(self.tree_info.nv,self.tree_info.dv)
279
+ end
280
+
281
+ def query_ancestor_tree_keys
282
+ check_parent = tree_search_class.where(:_id => self[tree_parent_id_field]).first
283
+ return nil if (check_parent.nil? || check_parent == [])
284
+ rethash = {:nv => check_parent.tree_info.nv,
285
+ :dv => check_parent.tree_info.dv,
286
+ :snv => check_parent.tree_info.snv,
287
+ :sdv => check_parent.tree_info.sdv}
288
+ end
289
+
290
+ def tree_keys
291
+ { :nv => self.tree_info.nv,
292
+ :dv => self.tree_info.dv,
293
+ :snv => self.tree_info.snv,
294
+ :sdv => self.tree_info.sdv}
295
+ end
296
+
297
+ # verifies parent keys from calculation and query
298
+ # this might not work for nested saves...
299
+ def correct_parent?(nv, dv)
300
+ # get nv/dv from parent
301
+ check_ancestor_keys = query_ancestor_tree_keys()
302
+ return false if (check_ancestor_keys == nil)
303
+ calc_ancestor_keys = self.class.ancestor_tree_keys(nv, dv)
304
+ if ( (calc_ancestor_keys[:nv] == check_ancestor_keys[:nv]) \
305
+ && (calc_ancestor_keys[:dv] == check_ancestor_keys[:dv]) \
306
+ && (calc_ancestor_keys[:snv] == check_ancestor_keys[:snv]) \
307
+ && (calc_ancestor_keys[:sdv] == check_ancestor_keys[:sdv]) \
308
+ )
309
+ return true
310
+ end
311
+ end
312
+
313
+ def disable_timestamp_callback
314
+ if self.respond_to?("updated_at")
315
+ @@_disable_timestamp_count += 1
316
+ self.class.skip_callback(:save, :before, :update_timestamps )
317
+ end
318
+ end
319
+
320
+ def enable_timestamp_callback
321
+ if self.respond_to?("updated_at")
322
+ @@_disable_timestamp_count -= 1
323
+ self.class.set_callback(:save, :before, :update_timestamps ) if @@_disable_timestamp_count <= 0
324
+ end
325
+ end
326
+
327
+ def root?
328
+ self[tree_parent_id_field].nil?
329
+ end
330
+
331
+ def root
332
+ self.tree_info.path.first.nil? ? self : tree_search_class.find(self.tree_info.path.first)
333
+ end
334
+
335
+ def ancestors
336
+ return [] if root?
337
+ tree_search_class.find(self.tree_info.path)
338
+ end
339
+
340
+ def self_and_ancestors
341
+ ancestors << self
342
+ end
343
+
344
+ def has_siblings?
345
+ tree_search_class.where(:_id => { "$ne" => self._id }) \
346
+ .where(tree_parent_id_field => self[tree_parent_id_field]) \
347
+ .sort(self.class.tree_sort_order()).count
348
+ end
349
+
350
+ def siblings
351
+ tree_search_class.where({
352
+ :_id => { "$ne" => self._id },
353
+ tree_parent_id_field => self[tree_parent_id_field]
354
+ }).sort(self.class.tree_sort_order()).all
355
+ end
356
+
357
+ def self_and_siblings
358
+ tree_search_class.where({
359
+ tree_parent_id_field => self[tree_parent_id_field]
360
+ }).sort(self.class.tree_sort_order()).all
361
+ end
362
+
363
+ def children?
364
+ return false if ((self.children == nil) || (self.children == []))
365
+ return true
366
+ end
367
+
368
+ def children
369
+ tree_search_class.where(tree_parent_id_field => self._id).sort(self.class.tree_sort_order()).all
370
+ end
371
+
372
+ def descendants
373
+ return [] if new_record?
374
+ tree_search_class.where("tree_info.path" => self._id).sort(self.class.tree_sort_order()).all
375
+ end
376
+
377
+ def self_and_descendants
378
+ [self] + self.descendants
379
+ end
380
+
381
+ def is_ancestor_of?(other)
382
+ other.tree_info.path.include?(self._id)
383
+ end
384
+
385
+ def is_or_is_ancestor_of?(other)
386
+ (other == self) or is_ancestor_of?(other)
387
+ end
388
+
389
+ def is_descendant_of?(other)
390
+ self.tree_info.path.include?(other._id)
391
+ end
392
+
393
+ def is_or_is_descendant_of?(other)
394
+ (other == self) or is_descendant_of?(other)
395
+ end
396
+
397
+ def is_sibling_of?(other)
398
+ (other != self) and (other[tree_parent_id_field] == self[tree_parent_id_field])
399
+ end
400
+
401
+ def is_or_is_sibling_of?(other)
402
+ (other == self) or is_sibling_of?(other)
403
+ end
404
+
405
+ def move_children
406
+ if @_will_move
407
+ @_will_move = false
408
+ _position = 0
409
+ self.disable_timestamp_callback()
410
+ self.children.each do |child|
411
+ child.update_path!
412
+ child.update_nv_dv!(:position => _position)
413
+ # puts "Update Child - #{child.name.inspect} #{child.changes.inspect}"
414
+ # puts "#{child.updated_at.to_f}"
415
+ child.save
416
+ child.reload
417
+ # puts "#{child.updated_at.to_f}"
418
+ child.save
419
+ child.reload
420
+ # puts "#{child.updated_at.to_f}"
421
+ child.reload
422
+ # puts "#{child.updated_at.to_f}"
423
+
424
+ _position += 1
425
+ end
426
+ self.enable_timestamp_callback()
427
+
428
+ # enable_tree_callbacks()
429
+ @_will_move = true
430
+ end
431
+ end
432
+
433
+ def destroy_descendants
434
+ tree_search_class.destroy(self.descendants.map(&:_id))
435
+ end
436
+
437
+ end # Module Tree
438
+ end # Module Plugins
439
+ end # Module MongoMapper
@@ -0,0 +1,18 @@
1
+ class TreeInfo
2
+ include MongoMapper::EmbeddedDocument
3
+ plugin MongoMapper::Plugins::Dirty
4
+ attr_accessible :nv, :dv, :snv, :sdv, :path, :depth, :position #, :parent_id
5
+
6
+ key :nv, Integer, :default => 0
7
+ key :dv, Integer, :default => 0
8
+ key :nv_div_dv, Float, :default => 0
9
+ key :snv, Integer, :default => 0
10
+ key :sdv, Integer, :default => 0
11
+ key :path, Array, :typecast => 'ObjectId' # might need to be string instead?
12
+ key :depth, Integer
13
+ # key :position, Integer (might not use this?)
14
+ # key :parent_id, ObjectId
15
+
16
+ timestamps!
17
+
18
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,6 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Tree
4
+ Version = '0.1.0'
5
+ end
6
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ # require 'test/unit'
4
+ # require 'shoulda'
5
+ # require 'database_cleaner'
6
+
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
9
+ require 'mm-tree'
10
+
11
+ Bundler.require(:default, :test)
12
+
13
+ MongoMapper.database = "mm-tree-test"
14
+
15
+ Dir["#{File.dirname(__FILE__)}/models/*.rb"].each {|file| require file}
16
+
17
+ DatabaseCleaner.strategy = :truncation
18
+
19
+ class Test::Unit::TestCase
20
+ # Drop all collections after each test case.
21
+ def setup
22
+ DatabaseCleaner.start
23
+ end
24
+
25
+ def teardown
26
+ DatabaseCleaner.clean
27
+ end
28
+
29
+ # Make sure that each test case has a teardown
30
+ # method to clear the db after each test.
31
+ def inherited(base)
32
+ base.define_method setup do
33
+ super
34
+ end
35
+
36
+ base.define_method teardown do
37
+ super
38
+ end
39
+ end
40
+
41
+ def eql_arrays?(first, second)
42
+ first.collect(&:_id).to_set == second.collect(&:_id).to_set
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ class Category
2
+ include MongoMapper::Document
3
+ plugin MongoMapper::Plugins::Tree
4
+
5
+ key :name, String
6
+ timestamps!
7
+ end
@@ -0,0 +1,10 @@
1
+ class OrderedCategory
2
+ include MongoMapper::Document
3
+ plugin MongoMapper::Plugins::Tree
4
+
5
+ key :name, String
6
+ key :value, Integer
7
+
8
+ self.tree_order = :value.asc
9
+ self.tree_use_rational_numbers = false
10
+ end
@@ -0,0 +1,18 @@
1
+ class Shape
2
+ include MongoMapper::Document
3
+ plugin MongoMapper::Plugins::Tree
4
+ self.tree_search_class = Shape
5
+
6
+ key :name, String
7
+ end
8
+
9
+ class Circle < Shape; end
10
+ class Square < Shape; end
11
+
12
+ class Triangle < Shape
13
+ self.tree_search_class = Triangle
14
+ end
15
+
16
+ class Cube < Shape
17
+ self.tree_search_class = Cube
18
+ end
@@ -0,0 +1,41 @@
1
+ require 'helper'
2
+ class TestMongomapperActsAsTree < Test::Unit::TestCase
3
+ context "Ordered tree" do
4
+ setup do
5
+ @node_1 = OrderedCategory.create(:name => "Node 1", :value => 2)
6
+ @node_1_1 = OrderedCategory.create(:name => "Node 1", :parent => @node_1, :value => 1)
7
+ @node_1_2 = OrderedCategory.create(:name => "Node 1.2", :parent => @node_1, :value => 9)
8
+ @node_1_2_1 = OrderedCategory.create(:name => "Node 1.2.1", :parent => @node_1_2, :value => 2)
9
+ @node_1_3 = OrderedCategory.create(:name => "Node 1.3", :parent => @node_1, :value => 5)
10
+ @node_2 = OrderedCategory.create(:name => "Node 2", :value => 1)
11
+ end
12
+
13
+ should "root nodes should be in order" do
14
+ OrderedCategory.roots.should eql?([@node_2, @node_1])
15
+ end
16
+
17
+ should "Node 1 children should be in order" do
18
+ @node_1.children.should eql?([@node_1_1, @node_1_3, @node_1_2])
19
+ end
20
+
21
+ should "Node 1 descendants should be in order" do
22
+ @node_1.descendants.should eql?([@node_1_1, @node_1_3, @node_1_2, @node_1_2_1])
23
+ end
24
+
25
+ should "Node 1 self and descendants should be in order" do
26
+ @node_1.self_and_descendants.should eql?([@node_1, @node_1_1, @node_1_3, @node_1_2, @node_1_2_1])
27
+ end
28
+
29
+ should "Node 1.2 siblings should be in order" do
30
+ @node_1_2.siblings.should eql?([@node_1_1, @node_1_3])
31
+ end
32
+
33
+ should "Node 1.2 self and siblings should be in order" do
34
+ @node_1_2.self_and_siblings.should eql?([@node_1_1, @node_1_3, @node_1_2])
35
+ end
36
+
37
+ should "Node 1 self and siblings should be in order" do
38
+ @node_1.self_and_siblings.should eql?([@node_2, @node_1])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,120 @@
1
+ require 'helper'
2
+
3
+ class TestSearchScope < Test::Unit::TestCase
4
+ context "Simple, mixed type tree" do
5
+ setup do
6
+ shape = Shape.create(:name => "Root")
7
+ Circle.create(:name => "Circle", :parent => shape)
8
+ Square.create(:name => "Square", :parent => shape)
9
+ end
10
+
11
+ setup do
12
+ # We are loading from the database here because this process proves the point. If we never did it this
13
+ # way, there would be no reason to change the code.
14
+ @shape, @circle, @square = Shape.first, Circle.first, Square.first
15
+ end
16
+
17
+ should "return circle and square as children of shape" do
18
+ [@circle, @square].should == @shape.children
19
+ end
20
+
21
+ should("return shape as parent of circle") do
22
+ @shape.should == @circle.parent
23
+ end
24
+ should("return shape as parent of square") do
25
+ @shape.should == @square.parent
26
+ end
27
+
28
+ should("return square as exclusive sibling of circle") do
29
+ [@square].should ==@circle.siblings
30
+ end
31
+
32
+ should "return self and square as inclusive siblings of circle" do
33
+ [@circle, @square].should == @circle.self_and_siblings
34
+ end
35
+
36
+ should("return circle as exclusive sibling of square") do
37
+ [@circle].should == @square.siblings
38
+ end
39
+ should "return self and circle as inclusive siblings of square" do
40
+ [@circle, @square].should == @square.self_and_siblings
41
+ end
42
+
43
+ should "return circle and square as exclusive descendants of shape" do
44
+ [@circle, @square].should == @shape.descendants
45
+ end
46
+ should "return shape, circle and square as inclusive descendants of shape" do
47
+ [@shape, @circle, @square].should == @shape.self_and_descendants
48
+ end
49
+
50
+ should("return shape as exclusive ancestor of circle") do
51
+ [@shape].should == @circle.ancestors
52
+ end
53
+
54
+ should "return self and shape as inclusive ancestors of circle" do
55
+ [@shape, @circle].should == @circle.self_and_ancestors
56
+ end
57
+
58
+ should("return shape as exclusive ancestor of square") do
59
+ [@shape].should == @square.ancestors
60
+ end
61
+ should "return self and shape as inclusive ancestors of square" do
62
+ [@shape, @square].should == @square.self_and_ancestors
63
+ end
64
+
65
+ should("return shape as root of circle") do
66
+ @shape.should == @square.root
67
+ end
68
+ should("return shape as root of square") do
69
+ @shape.should == @circle.root
70
+ end
71
+ end
72
+
73
+ context "A tree with mixed types on either side of a branch" do
74
+ setup do
75
+ shape = Shape.create(:name => "Root")
76
+ circle = Circle.create(:name => "Circle", :parent => shape)
77
+ Square.create(:name => "Square", :parent => circle)
78
+ end
79
+
80
+ setup do
81
+ @shape, @circle, @square = Shape.first, Circle.first, Square.first
82
+ end
83
+
84
+ should("return circle as child of shape") do
85
+ [@circle].should == @shape.children
86
+ end
87
+ should("return square as child of circle") do
88
+ [@square].should == @circle.children
89
+ end
90
+ should("return circle as parent of square") do
91
+ @circle.should == @square.parent
92
+ end
93
+ should("return shape as parent of circle") do
94
+ @shape.should == @circle.parent
95
+ end
96
+
97
+ should "return circle and square as descendants of shape" do
98
+ [@circle, @square].should == @shape.descendants
99
+ end
100
+
101
+ should("return square as descendant of circle") do
102
+ [@square].should == @circle.descendants
103
+ end
104
+
105
+ should "return shape and circle as ancestors of square" do
106
+ [@shape, @circle].should == @square.ancestors
107
+ end
108
+
109
+ should("return shape as ancestor of circle") do
110
+ [@shape].should == @circle.ancestors
111
+ end
112
+
113
+ should "destroy descendants of shape" do
114
+ @shape.destroy_descendants
115
+ assert_nil Shape.find(@circle._id)
116
+ assert_nil Shape.find(@square._id)
117
+ end
118
+ end
119
+
120
+ end # TestSearchScope
@@ -0,0 +1,78 @@
1
+ require 'helper'
2
+
3
+ class TestSearchScope < Test::Unit::TestCase
4
+ context "Mixed type tree with unique search classes" do
5
+ setup do
6
+ @shape_1 = Shape.create(:name => "Shape 1")
7
+ @shape_1_1 = Shape.create(:name => "Shape 1.1", :parent => @shape_1)
8
+ @shape_1_2 = Shape.create(:name => "Shape 1.2", :parent => @shape_1)
9
+ @triangle_1 = Triangle.create(:name => "Triangle 1")
10
+ @triangle_1_1 = Triangle.create(:name => "Triangle 1.1", :parent => @triangle_1)
11
+ @triangle_1_2 = Triangle.create(:name => "Triangle 1.2", :parent => @triangle_1)
12
+ @cube_1 = Cube.create(:name => "Cube 1")
13
+ @cube_1_1 = Cube.create(:name => "Cube 1.1", :parent => @cube_1)
14
+ @cube_1_2 = Cube.create(:name => "Cube 1.2", :parent => @cube_1)
15
+ @cube_1_2_1 = Cube.create(:name => "Cube 1.2.1", :parent => @cube_1_2)
16
+ @cube_1_2_2 = Cube.create(:name => "Cube 1.2.2", :parent => @cube_1_2)
17
+ @cube_1_2_2_1 = Cube.create(:name => "Cube 1.2.2.1", :parent => @cube_1_2_1)
18
+ end
19
+
20
+ should "return cubes as children of cube_1" do
21
+ @cube_1.children.should == [@cube_1_1, @cube_1_2]
22
+ end
23
+
24
+ should "return two shapes as children of shape_1" do
25
+ @cube_1.children.count.should == 2
26
+ end
27
+
28
+ should "return triangles as children of triangles" do
29
+ @triangle_1.children.should == [@triangle_1_1, @triangle_1_2]
30
+ end
31
+
32
+ should "move a cube child within cubes" do
33
+ @cube_1_2_2.parent = @cube_1
34
+ @cube_1_2_2.save
35
+ @cube_1.reload
36
+ @cube_1_1.reload
37
+ @cube_1_2.reload
38
+ @cube_1_2_1.reload
39
+ @cube_1_2_2.reload
40
+ @cube_1_2_2_1.reload
41
+ @cube_1_1.siblings.should == [@cube_1_2, @cube_1_2_2]
42
+ @cube_1.descendants.should == [@cube_1_1, @cube_1_2, @cube_1_2_1, @cube_1_2_2, @cube_1_2_2_1]
43
+ end
44
+
45
+ should "not return any triangles or cubes descendants of shape_1" do
46
+ @shape_1.descendants.each do |ddant|
47
+ ddant.name.should_not =~ /Cube/
48
+ ddant.name.should_not =~ /Triangle/
49
+ end
50
+ end
51
+
52
+ should "return cube_1 as parent of cube_1_1" do
53
+ @cube_1_1.parent.should == @cube_1
54
+ end
55
+
56
+ should "return shape_1 as parent of shape_1_2" do
57
+ @shape_1_2.parent.should == @shape_1
58
+ end
59
+
60
+ should "not allow to set a cube_1 as child of triangle_1" do
61
+ # TODO: add validation that search class of parent and child is same
62
+ @cube_1.parent = @triangle_1
63
+ @cube_1.save
64
+ @cube_1.errors.count.should == 1 #should have an error
65
+ @cube_1.errors.each do |attribute, errmsg|
66
+ attribute.to_s.should == "base"
67
+ errmsg.should == ("Mismatch between search classes. Parent: Triangle Node: Cube. They must be equal")
68
+ end
69
+ end
70
+
71
+ should "destroy descendants of shape_1" do
72
+ @shape_1.destroy_descendants
73
+ Shape.find(@shape_1_2._id).should == nil
74
+ Shape.find(@shape_1_1._id).should == nil
75
+ end
76
+ end # context "Mixed type tree with unique search classes" do
77
+
78
+ end # TestSearchScope
data/test/test_tree.rb ADDED
@@ -0,0 +1,296 @@
1
+ require 'helper'
2
+ class TestMongomapperActsAsTree < Test::Unit::TestCase
3
+ context "Tree" do
4
+ setup do
5
+ @node_1 = Category.create(:name => "Node 1")
6
+ @node_1_1 = Category.create(:name => "Node 1.1", :parent => @node_1)
7
+ @node_1_2 = Category.create(:name => "Node 1.2", :parent => @node_1)
8
+ @node_1_2_1 = Category.create(:name => "Node 1.2.1", :parent => @node_1_2)
9
+ @node_1_2_2 = Category.create(:name => "Node 1.2.2", :parent => @node_1_2)
10
+ @node_1_3 = Category.create(:name => "Node 1.3", :parent => @node_1)
11
+ #@node_4 = Category.create(:name => "Node 3", :parent => @node_1)
12
+ @node_2 = Category.create(:name => "Node 2")
13
+ @node_2_1 = Category.create(:name => "Node 2.1", :parent => @node_2)
14
+ @node_2_2 = Category.create(:name => "Node 2.2", :parent => @node_2)
15
+ @node_2_3 = Category.create(:name => "Node 2.3", :parent => @node_2)
16
+ @node_2_4 = Category.create(:name => "Node 2.4", :parent => @node_2)
17
+ @node_2_4_1 = Category.create(:name => "Node 2.4.1", :parent => @node_2_4)
18
+ @node_2_4_2 = Category.create(:name => "Node 2.4.2", :parent => @node_2_4)
19
+ @node_2_4_3 = Category.create(:name => "Node 2.4.3", :parent => @node_2_4)
20
+ @node_2_4_1_1 = Category.create(:name => "Node 2.4.1.1", :parent => @node_2_4_1)
21
+ end #setup do
22
+
23
+ should "create node from id " do
24
+ assert Category.create(:name => "Node 1.4", :parent_id => @node_1.id).parent == @node_1
25
+ end
26
+
27
+ should "have roots" do
28
+ Category.roots.should == [@node_1, @node_2]
29
+ end
30
+
31
+ context "node" do
32
+ should "have a root" do
33
+ @node_1.root.should == @node_1
34
+ @node_1.root.should_not == @node_2.root
35
+ @node_1.should == @node_1_2_1.root
36
+ end
37
+
38
+ should "have ancestors" do
39
+ @node_1.ancestors.should == []
40
+ @node_1_2_1.ancestors.should == [@node_1, @node_1_2]
41
+ @node_1.self_and_ancestors.should == [@node_1]
42
+ @node_1_2_1.self_and_ancestors.should == [@node_1, @node_1_2, @node_1_2_1]
43
+ end
44
+
45
+ should "have siblings" do
46
+ @node_1.siblings.should == [@node_2]
47
+ @node_1_2.siblings.should == [@node_1_1, @node_1_3]
48
+ @node_1_2_1.siblings.should == [@node_1_2_2]
49
+ @node_1.self_and_siblings.should == [@node_1, @node_2]
50
+ @node_1_2.self_and_siblings.should == [@node_1_1, @node_1_2, @node_1_3]
51
+ @node_1_2_1.self_and_siblings.should == [@node_1_2_1, @node_1_2_2]
52
+ @node_1_2_2.self_and_siblings.should == [@node_1_2_1, @node_1_2_2]
53
+ end
54
+
55
+ should "set depth" do
56
+ @node_1.tree_info.depth.should == 0
57
+ @node_1_1.tree_info.depth.should == 1
58
+ @node_1_2_1.tree_info.depth.should == 2
59
+ end
60
+
61
+ should "have children" do
62
+ assert @node_1_2_1.children.empty?
63
+ @node_1.children.should == [@node_1_1, @node_1_2, @node_1_3]
64
+ end
65
+
66
+ should "have descendants" do
67
+ @node_1.descendants.should == [@node_1_1, @node_1_2, @node_1_2_1, @node_1_2_2, @node_1_3]
68
+ @node_1_2.descendants.should == [@node_1_2_1, @node_1_2_2]
69
+ assert @node_1_2_1.descendants.empty?
70
+ @node_1.self_and_descendants.should == [@node_1, @node_1_1, @node_1_2, @node_1_2_1, @node_1_2_2, @node_1_3]
71
+ @node_1_2.self_and_descendants.should == [@node_1_2, @node_1_2_1, @node_1_2_2]
72
+ @node_1_2_1.self_and_descendants.should == [@node_1_2_1]
73
+ end
74
+
75
+ should "be able to tell if ancestor" do
76
+ assert @node_1.is_ancestor_of?(@node_1_1)
77
+ assert ! @node_2.is_ancestor_of?(@node_1_2_1)
78
+ assert ! @node_1_2.is_ancestor_of?(@node_1_2)
79
+
80
+ assert @node_1.is_or_is_ancestor_of?(@node_1_1)
81
+ assert ! @node_2.is_or_is_ancestor_of?(@node_1_2_1)
82
+ assert @node_1_2.is_or_is_ancestor_of?(@node_1_2)
83
+ end
84
+
85
+ should "be able to tell if descendant" do
86
+ assert ! @node_1.is_descendant_of?(@node_1_1)
87
+ assert @node_1_1.is_descendant_of?(@node_1)
88
+ assert ! @node_1_2.is_descendant_of?(@node_1_2)
89
+
90
+ assert ! @node_1.is_or_is_descendant_of?(@node_1_1)
91
+ assert @node_1_1.is_or_is_descendant_of?(@node_1)
92
+ assert @node_1_2.is_or_is_descendant_of?(@node_1_2)
93
+ end
94
+
95
+ should "be able to tell if sibling" do
96
+ assert ! @node_1.is_sibling_of?(@node_1_1)
97
+ assert ! @node_1_1.is_sibling_of?(@node_1_1)
98
+ assert ! @node_1_2.is_sibling_of?(@node_1_2)
99
+
100
+ assert ! @node_1.is_or_is_sibling_of?(@node_1_1)
101
+ assert @node_1_1.is_or_is_sibling_of?(@node_1_2)
102
+ assert @node_1_2.is_or_is_sibling_of?(@node_1_2)
103
+ end
104
+
105
+ should "destroy descendants when destroyed" do
106
+ @node_1_2.destroy
107
+ assert_nil Category.find(@node_1_2_1._id)
108
+ end
109
+
110
+ context "when moving" do
111
+ should "recalculate path and depth" do
112
+ @node_1_3.parent = @node_1_2
113
+ @node_1_3.save
114
+
115
+ assert @node_1_2.is_or_is_ancestor_of?(@node_1_3)
116
+ assert @node_1_3.is_or_is_descendant_of?(@node_1_2)
117
+ assert @node_1_2.children.include?(@node_1_3)
118
+ assert @node_1_2.descendants.include?(@node_1_3)
119
+ assert @node_1_2_1.is_or_is_sibling_of?(@node_1_3)
120
+ assert @node_1_2_2.is_or_is_sibling_of?(@node_1_3)
121
+ @node_1_3.tree_info.depth.should == 2
122
+ end
123
+
124
+ should "move children on save" do
125
+ @node_1_2.parent = @node_2
126
+
127
+ assert ! @node_2.is_or_is_ancestor_of?(@node_1_2_1)
128
+ assert ! @node_1_2_1.is_or_is_descendant_of?(@node_2)
129
+ assert ! @node_2.descendants.include?(@node_1_2_1)
130
+
131
+ @node_1_2.save
132
+ @node_1_2_1.reload
133
+
134
+ assert @node_2.is_or_is_ancestor_of?(@node_1_2_1)
135
+ assert @node_1_2_1.is_or_is_descendant_of?(@node_2)
136
+ assert @node_2.descendants.include?(@node_1_2_1)
137
+ end
138
+
139
+ should "move children on save and don't touch timestamps for children" do
140
+ @node_2_4.parent = @node_1
141
+
142
+ before_created_at_2_4_1 = @node_2_4_1.created_at
143
+ before_updated_at_2_4_1 = @node_2_4_1.updated_at
144
+ before_created_at_2_4_1_1 = @node_2_4_1_1.created_at
145
+ before_updated_at_2_4_1_1 = @node_2_4_1_1.updated_at
146
+
147
+ Timecop.freeze(Time.now + 2.seconds) do
148
+ @node_2_4.save
149
+ end
150
+ @node_2_4_1.reload
151
+
152
+ # until mongo_mapper implements timefix, do
153
+ @node_2_4_1.created_at.to_f.should be_close(before_created_at_2_4_1.to_f, 0.001)
154
+ @node_2_4_1.updated_at.to_f.should be_close(before_updated_at_2_4_1.to_f, 0.001)
155
+ # @node_2_4_1_1.created_at.to_f.should be_close(before_created_at_2_4_1_1.to_f, 0.001)
156
+ # @node_2_4_1_1.updated_at.to_f.should be_close(before_updated_at_2_4_1_1.to_f, 0.001)
157
+
158
+ # when mongo_mapper implements timefix:
159
+ # @node_2_4_1.created_at.should eql?(before_created_at_2_4_1)
160
+ # @node_2_4_1.updated_at.should eql?(before_updated_at_2_4_1)
161
+ # @node_2_4_1_1.created_at.should be_close(before_created_at_2_4_1_1)
162
+ # @node_2_4_1_1.updated_at.should be_close(before_updated_at_2_4_1_1)
163
+
164
+ end
165
+
166
+ should "check against cyclic graph" do
167
+ @node_1.parent = @node_1_2_1
168
+ @node_1.save
169
+ @node_1.valid?.should == false
170
+ I18n.t(:'mongo_mapper.errors.messages.tree.cyclic').should == @node_1.errors[:base].first
171
+ end
172
+
173
+ should "be able to become root" do
174
+ @node_1_2.parent = nil
175
+ @node_1_2.save
176
+ @node_1_2.reload
177
+ assert_nil @node_1_2.parent
178
+ @node_1_2_1.reload
179
+ assert (@node_1_2_1.tree_info.path == [@node_1_2.id])
180
+ end
181
+ end # context "when moving" do
182
+ end # context "node" do
183
+
184
+ context "root node" do
185
+ should "not have a parent" do
186
+ assert_nil @node_1.parent
187
+ end
188
+ end
189
+
190
+ context "node_node" do
191
+ should "have a parent" do
192
+ assert_equal @node_1_2, @node_1_2_1.parent
193
+ end
194
+ end
195
+
196
+ context "node (keys)" do
197
+ should "find keys from id" do
198
+ assert Category.find(@node_1._id).tree_keys == @node_1.tree_keys, "Query doesn't match created object #{@node_1.name}"
199
+ assert Category.find(@node_2_4_1_1._id).tree_keys == @node_2_4_1_1.tree_keys, "Query doesn't match created object #{@node_2_4_1_1.name}"
200
+ end
201
+
202
+ should "have correct keys" do
203
+ @node_1.tree_keys.should == Hash[:nv => 1, :dv => 1, :snv => 2, :sdv => 1]
204
+ @node_2.tree_keys.should == Hash[:nv => 2, :dv => 1, :snv => 3, :sdv => 1]
205
+ @node_2_1.tree_keys.should == Hash[:nv => 5, :dv => 2, :snv => 8, :sdv => 3]
206
+ @node_1_3.tree_keys.should == Hash[:nv => 7, :dv => 4, :snv => 9, :sdv => 5]
207
+ @node_2_4.tree_keys.should == Hash[:nv => 14, :dv => 5, :snv => 17, :sdv => 6]
208
+ @node_2_4_1.tree_keys.should == Hash[:nv => 31, :dv => 11, :snv => 48, :sdv => 17]
209
+ @node_2_4_3.tree_keys.should == Hash[:nv => 65, :dv => 23, :snv => 82, :sdv => 29]
210
+ @node_2_4_1_1.tree_keys.should == Hash[:nv => 79, :dv => 28, :snv => 127, :sdv => 45]
211
+ end
212
+
213
+ should "find and calculate ancestor keys from given keys" do
214
+ assert Category.ancestor_tree_keys(@node_1.tree_info.nv, @node_1.tree_info.dv) == Hash[:nv => 0, :dv => 1, :snv => 1, :sdv => 0], "Ancestor keys for #{@node_1.name} is wrong"
215
+ assert Category.ancestor_tree_keys(@node_2_1.tree_info.nv, @node_2_1.tree_info.dv) == @node_2.tree_keys(), "Ancestor keys for #{@node_2_1.name} is not matching keys for #{@node_2.name}"
216
+ assert Category.ancestor_tree_keys(@node_2_2.tree_info.nv, @node_2_2.tree_info.dv) == @node_2.tree_keys(), "Ancestor keys for #{@node_2_2.name} is not matching keys for #{@node_2.name}"
217
+ assert Category.ancestor_tree_keys(@node_2_4_1.tree_info.nv, @node_2_4_1.tree_info.dv) == @node_2_4.tree_keys(), "Ancestor keys for #{@node_2_4_1.name} is not matching keys for #{@node_2_4.name}"
218
+ assert Category.ancestor_tree_keys(@node_2_4_1_1.tree_info.nv, @node_2_4_1_1.tree_info.dv) == @node_2_4_1.tree_keys(), "Ancestor keys for #{@node_2_4_1_1.name} is not matching keys for #{@node_2_4_1.name}"
219
+ end
220
+
221
+ should "find positions from given keys" do
222
+ assert Category.position_from_nv_dv(@node_1.tree_info.nv, @node_1.tree_info.dv) == 1, "Wrong position for #{@node_1.name}, got #{Category.position_from_nv_dv(@node_1.tree_info.nv, @node_1.tree_info.dv)}, expected: 1"
223
+ assert Category.position_from_nv_dv(@node_2_1.tree_info.nv, @node_2_1.tree_info.dv) == 1, "Wrong position for #{@node_2_1.name}, got #{Category.position_from_nv_dv(@node_2_1.tree_info.nv, @node_2_1.tree_info.dv)}, expected: 1"
224
+ assert Category.position_from_nv_dv(@node_2_2.tree_info.nv, @node_2_2.tree_info.dv) == 2, "Wrong position for #{@node_2_2.name}, got #{Category.position_from_nv_dv(@node_2_2.tree_info.nv, @node_2_2.tree_info.dv)}, expected: 2"
225
+ assert Category.position_from_nv_dv(@node_2_3.tree_info.nv, @node_2_3.tree_info.dv) == 3, "Wrong position for #{@node_2_3.name}, got #{Category.position_from_nv_dv(@node_2_3.tree_info.nv, @node_2_3.tree_info.dv)}, expected: 3"
226
+ assert Category.position_from_nv_dv(@node_2_4.tree_info.nv, @node_2_4.tree_info.dv) == 4, "Wrong position for #{@node_2_4.name}, got #{Category.position_from_nv_dv(@node_2_4.tree_info.nv, @node_2_4.tree_info.dv)}, expected: 4"
227
+ assert Category.position_from_nv_dv(@node_2_4_1.tree_info.nv, @node_2_4_1.tree_info.dv) == 1, "Wrong position for #{@node_2_4_1.name}, got #{Category.position_from_nv_dv(@node_2_4_1.tree_info.nv, @node_2_4_1.tree_info.dv)}, expected: 1"
228
+ assert Category.position_from_nv_dv(@node_2_4_1_1.tree_info.nv, @node_2_4_1_1.tree_info.dv) == 1, "Wrong position for #{@node_2_4_1_1.name}, got #{Category.position_from_nv_dv(@node_2_4_1_1.tree_info.nv, @node_2_4_1_1.tree_info.dv)}, expected: 1"
229
+ end
230
+
231
+ should "verify ancestor keys" do
232
+ assert @node_1_2.ancestor_tree_keys() == @node_1.tree_keys(), "#{@node_1_2.name} ancestor keys doesn't match #{@node_1.name} tree keys"
233
+ assert @node_1_2_1.ancestor_tree_keys() == @node_1_2.tree_keys(), "#{@node_1_2_1.name} ancestor keys doesn't match #{@node_1_2.name} tree keys"
234
+ assert @node_2_4_1_1.ancestor_tree_keys() == @node_2_4_1.tree_keys(), "#{@node_2_4_1_1.name} ancestor keys doesn't match #{@node_2_4_1.name} tree keys"
235
+ end
236
+
237
+ should "move to new specific nv, dv location and move conflicting items" do
238
+ assert @node_2_4.ancestor_tree_keys() == @node_2.tree_keys(), "Before move: #{@node_2_4.name} ancestor keys should match #{@node_2.name} got: #{@node_2_4.ancestor_tree_keys()} expected: #{@node_2.tree_keys()}"
239
+ assert @node_2_4_1.tree_info.depth == 2, "Before move: Depth of #{@node_2_4_1.name} should be 2"
240
+ old_1_2_keys = @node_1_2.tree_keys()
241
+ new_node_1_2_keys = @node_1_2.next_sibling_keys
242
+ @node_2_4.set_position(@node_1_2.tree_info.nv, @node_1_2.tree_info.dv)
243
+ @node_2_4.save
244
+ @node_1_2.reload
245
+ @node_2_4.reload
246
+ @node_2_4_1.reload
247
+
248
+ assert @node_1_2.tree_keys() != old_1_2_keys, "After move: #{@node_1_2.name} should have moved to new position, got #{@node_1_2.tree_keys} expected: #{new_node_1_2_keys}"
249
+ assert @node_1_2.tree_keys() == new_node_1_2_keys, "After move: #{@node_1_2.name} should have moved to new position, got #{@node_1_2.tree_keys} expected: #{new_node_1_2_keys}"
250
+ assert @node_2_4.tree_keys() == old_1_2_keys, "After move: #{@node_2_4.name} should have taken #{@node_1_2.name}'s position, got: #{@node_2_4.tree_keys}, expected: #{old_1_2_keys}"
251
+ assert @node_2_4_1.ancestor_tree_keys() == @node_2_4.tree_keys(), "After move: #{@node_2_4_1.name} ancestor keys should match #{@node_2_4.name} got: #{@node_2_4_1.ancestor_tree_keys()} expected: #{@node_2_4.tree_keys()}"
252
+ end
253
+
254
+ should "move @node_2_4 to root position" do
255
+ assert @node_2_4.ancestor_tree_keys() == @node_2.tree_keys(), "Before move: #{@node_2_4.name} ancestor keys should match #{@node_2.name} got: #{@node_2_4.ancestor_tree_keys()} expected: #{@node_2.tree_keys()}"
256
+ assert @node_2_4_1.tree_info.depth == 2, "Before move: Depth of #{@node_2_4_1.name} should be 2"
257
+ @node_2_4.parent = nil
258
+ @node_2_4.save
259
+ @node_2_4.reload
260
+ assert @node_2_4.root?, "#{@node_2_4.name} is not root"
261
+ assert @node_2_4.tree_keys() == @node_2.next_sibling_keys(), "After move: #{@node_2_4.name} keys should match keys #{@node_2.name} sibling keys. got: #{@node_2_4.tree_keys()} expected: #{@node_2.next_sibling_keys()}"
262
+ assert @node_2_4.ancestor_tree_keys() == Hash[:nv => 0, :dv => 1, :snv => 1, :sdv => 0], "After move: #{@node_2_4.name} ancestor keys should match root keys. got: #{@node_2_4.ancestor_tree_keys()} expected: #{Hash[:nv => 0, :dv => 1, :snv => 1, :sdv => 0]}"
263
+ # TODO: OVERRIDE RELOAD TO LOAD ALL CHILDREN IN MEMORY/CACHE/ASSOCS
264
+ @node_2_4_1.reload
265
+ assert @node_2_4_1.tree_info.path.count == 1, "After move: Path length of #{@node_2_4_1.name} should only be 1"
266
+ assert @node_2_4_1.tree_info.depth == 1, "After move: Depth of #{@node_2_4_1.name} should be 1"
267
+ assert @node_2_4_1.ancestor_tree_keys() == @node_2_4.tree_keys(), "After move: #{@node_2_4_1.name} ancestor keys should match #{@node_2_4.name} got: #{@node_2_4_1.ancestor_tree_keys()} expected: #{@node_2_4.tree_keys()}"
268
+ # TODO: OVERRIDE RELOAD TO LOAD ALL CHILDREN IN MEMORY/CACHE/ASSOCS
269
+ @node_2_4_1_1.reload
270
+ assert @node_2_4_1_1.ancestor_tree_keys() == @node_2_4_1.tree_keys(), "After move: #{@node_2_4_1_1.name} ancestor keys should match #{@node_2_4_1.name} got: #{@node_2_4_1_1.ancestor_tree_keys()} expected: #{@node_2_4_1.tree_keys()}"
271
+ end
272
+
273
+ should "should have changed nv/dv after changing parent (id)" do
274
+ old_keys = @node_1_2.tree_keys()
275
+ @node_1_2.parent = @node_2
276
+ # before saved
277
+ assert @node_1_2.ancestor_tree_keys() == @node_1.tree_keys(), "Before move: #{@node_1_2.name} ancestor keys should match #{@node_1.name} got: #{@node_1_2.ancestor_tree_keys()} expected: #{@node_1.tree_keys()}"
278
+ assert @node_1_2_1.ancestor_tree_keys() == @node_1_2.tree_keys(), "Before move #{@node_1_2_1.name} ancestor keys should match #{@node_1_2.name} got: #{@node_1_2_1.ancestor_tree_keys()} expected: #{@node_1_2.tree_keys()}"
279
+
280
+ @node_1_2.save
281
+ @node_1_2.reload
282
+ @node_1_2_1.reload
283
+
284
+ assert @node_1_2.tree_keys() != old_keys, "#{@node_1_2} keys should not be same as old"
285
+ assert @node_1_2.ancestor_tree_keys() == @node_2.tree_keys(), "After move: #{@node_1_2.name} ancestor keys should match #{@node_2.name} got: #{@node_1_2.ancestor_tree_keys()} expected: #{@node_2.tree_keys()}"
286
+ # should still be able to find correct keys for child of moved item
287
+ assert @node_1_2_1.ancestor_tree_keys() == @node_1_2.tree_keys(), "After move #{@node_1_2_1.name} ancestor keys should match #{@node_1_2.name} got: #{@node_1_2_1.ancestor_tree_keys()} expected: #{@node_1_2.tree_keys()}"
288
+ end
289
+ end # tree keys
290
+
291
+ should "rekey the entire treestructre" do
292
+ # TODO
293
+ end
294
+
295
+ end #Context "Tree" do
296
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mm-tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Leif Ringstad
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo_mapper
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.11.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.11.2
30
+ description: Tree structure for MongoMapper with rational number sorting
31
+ email:
32
+ - leifcr@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/locale/en.yml
38
+ - lib/mm-tree.rb
39
+ - lib/mongo_mapper/plugins/tree.rb
40
+ - lib/mongo_mapper/plugins/tree_info.rb
41
+ - lib/version.rb
42
+ - test/helper.rb
43
+ - test/models/category.rb
44
+ - test/models/ordered_category.rb
45
+ - test/models/shapes.rb
46
+ - test/test_order.rb
47
+ - test/test_search_class.rb
48
+ - test/test_search_class_multi.rb
49
+ - test/test_tree.rb
50
+ - LICENSE
51
+ - README.rdoc
52
+ homepage: http://github.com/leifcr/mm-tree
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.24
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Tree structure for MongoMapper
76
+ test_files:
77
+ - test/helper.rb
78
+ - test/models/category.rb
79
+ - test/models/ordered_category.rb
80
+ - test/models/shapes.rb
81
+ - test/test_order.rb
82
+ - test/test_search_class.rb
83
+ - test/test_search_class_multi.rb
84
+ - test/test_tree.rb