mm-tree 0.1.0

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