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 +20 -0
- data/README.rdoc +121 -0
- data/lib/locale/en.yml +8 -0
- data/lib/mm-tree.rb +5 -0
- data/lib/mongo_mapper/plugins/tree.rb +439 -0
- data/lib/mongo_mapper/plugins/tree_info.rb +18 -0
- data/lib/version.rb +6 -0
- data/test/helper.rb +44 -0
- data/test/models/category.rb +7 -0
- data/test/models/ordered_category.rb +10 -0
- data/test/models/shapes.rb +18 -0
- data/test/test_order.rb +41 -0
- data/test/test_search_class.rb +120 -0
- data/test/test_search_class_multi.rb +78 -0
- data/test/test_tree.rb +296 -0
- metadata +84 -0
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,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
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,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
|
data/test/test_order.rb
ADDED
@@ -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
|