acts-as-tree-with-dotted-ids 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,78 @@
1
+ = acts_as_tree_with_dotted_ids
2
+
3
+ This is an extension to Rails good old acts_as_tree which uses an extra "dotted_ids" column
4
+ which stores the path of the node as a string of record IDs joined by dots, hence the name.
5
+
6
+ This optimization solves performance issues related to in-database tree structure by allowing
7
+ for direct O(1) ancestor/child verification and O(N) subtree access with one single query.
8
+
9
+ class Category < ActiveRecord::Base
10
+ acts_as_tree_with_dotted_ids :order => "name"
11
+ end
12
+
13
+ Example:
14
+
15
+ root
16
+ \_ child1
17
+ \_ subchild1
18
+ \_ subchild2
19
+
20
+ Usage:
21
+
22
+ root = Category.create("name" => "root")
23
+ child1 = root.children.create("name" => "child1")
24
+ subchild1 = child1.children.create("name" => "subchild1")
25
+
26
+ root.parent # => nil
27
+ child1.parent # => root
28
+ root.children # => [child1]
29
+ root.children.first.children.first # => subchild1
30
+ child1.ancestors_of?(subchild2) # => true
31
+ subchild1.descendant_of?(root) # => true
32
+
33
+ root.id # 1
34
+ child1.id # 2
35
+ subchild1.id # 3
36
+ root.dotted_ids # "1"
37
+ child1.dotted_ids # "1.2"
38
+ subchild1.dotted_ids # "1.2.3"
39
+
40
+ == Improvements
41
+
42
+ The plugin adds the following instance methods:
43
+
44
+ * <tt>ancestor_of?(node)</tt>
45
+ * +self_and_ancestors+
46
+ * <tt>descendant_of?(node)</tt>
47
+ * +all_children+
48
+ * +depth+
49
+
50
+ The following methods of have been rewritten to take advantage of the dotted IDs:
51
+
52
+ * +root+
53
+ * +ancestors+
54
+ * +siblings+
55
+ * +self_and_sibblings+
56
+
57
+
58
+ == Migration
59
+
60
+ If you already have an +acts_as_tree+ model, you can easily upgrade it to take advantage of the dotted IDs.
61
+
62
+ 1. Just add the +dotted_ids+ column to your table. In most case a string should be enough (it's also better for the indexing) but if your tree is very deep you may want to use a text column.
63
+ 2. Call <tt>MyTreeModel.rebuild_dotted_ids!</tt> and you are ready to go.
64
+
65
+
66
+ == Compatibility
67
+
68
+ Tested with Rails 2.x and MySQL 5.x as well as SQLite.
69
+
70
+
71
+ == Thanks
72
+
73
+ Kudos to all the contributors to the original plugin.
74
+
75
+
76
+ Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
77
+
78
+ Copyright (c) 2008 Xavier Defrang, released under the MIT license
@@ -0,0 +1,218 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module TreeWithDottedIds
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
9
+ # association. This requires that you have a foreign key column, which by default is called +parent_id+ and a string or text column called +dotted_ids+ which will be used to store the path to each node in the tree.
10
+ #
11
+ # class Category < ActiveRecord::Base
12
+ # acts_as_tree_with_dotted_ids :order => "name"
13
+ # end
14
+ #
15
+ # Example:
16
+ # root
17
+ # \_ child1
18
+ # \_ subchild1
19
+ # \_ subchild2
20
+ #
21
+ # root = Category.create("name" => "root")
22
+ # child1 = root.children.create("name" => "child1")
23
+ # subchild1 = child1.children.create("name" => "subchild1")
24
+ #
25
+ # root.parent # => nil
26
+ # child1.parent # => root
27
+ # root.children # => [child1]
28
+ # root.children.first.children.first # => subchild1
29
+ #
30
+ # In addition to the parent and children associations, the following instance methods are added to the class
31
+ # after calling <tt>acts_as_tree_with_dotted_ids</tt>:
32
+ # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
33
+ # * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
34
+ # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
35
+ # * <tt>self_and_ancestors</tt> - Returns all the ancestors of the current node (<tt>[subchild2, child1, root]</tt> when called on <tt>subchild2</tt>)
36
+ # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
37
+ # * <tt>depth</tt> - Returns the depth of the current node starting from 0 as the depth of root nodes.
38
+ #
39
+ # The following class methods are added
40
+ # * <tt>traverse</tt> - depth-first traversal of the tree (warning: it does *not* rely on the dotted_ids as it is used to rebuild the tree)
41
+ # * <tt>rebuild_dotted_ids!</tt> - rebuilt the dotted IDs for the whole tree, use this once to migrate an existing +acts_as_tree+ model to +acts_as_tree_with_dotted_ids+
42
+
43
+ module ClassMethods
44
+ # Configuration options are:
45
+ #
46
+ # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
47
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
48
+ # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
49
+ def acts_as_tree_with_dotted_ids(options = {}, &b)
50
+ configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
51
+ configuration.update(options) if options.is_a?(Hash)
52
+
53
+ belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
54
+
55
+
56
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key],
57
+ :order => configuration[:order], :dependent => :destroy, &b
58
+
59
+ after_save :assign_dotted_ids
60
+ after_validation :update_dotted_ids, :on => :update
61
+
62
+ class_eval <<-EOV
63
+ include ActiveRecord::Acts::TreeWithDottedIds::InstanceMethods
64
+
65
+ def self.roots
66
+ res = find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
67
+
68
+ end
69
+
70
+ def self.root
71
+ find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
72
+ end
73
+
74
+ def parent_foreign_key_changed?
75
+ #{configuration[:foreign_key]}_changed?
76
+ end
77
+
78
+ EOV
79
+ end
80
+
81
+ # Performs a depth-first traversal of the tree, yielding each node to the given block
82
+ def traverse(nodes = nil, &block)
83
+ nodes ||= self.roots
84
+ nodes.each do |node|
85
+ yield node
86
+ traverse(node.children, &block)
87
+ end
88
+ end
89
+
90
+ # Traverse the whole tree from roots to leaves and rebuild the dotted_ids path
91
+ # Call it from your migration to upgrade an existing acts_as_tree model.
92
+ def rebuild_dotted_ids!
93
+ transaction do
94
+ traverse { |node| node.dotted_ids = nil; node.save! }
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ module InstanceMethods
101
+
102
+ # Returns list of ancestors, starting from parent until root.
103
+ #
104
+ # subchild1.ancestors # => [child1, root]
105
+ def ancestors
106
+ if self.dotted_ids
107
+ ids = self.dotted_ids.split('.')[0...-1]
108
+ self.class.find(:all, :conditions => {:id => ids}, :order => 'dotted_ids DESC')
109
+ else
110
+ node, nodes = self, []
111
+ nodes << node = node.parent while node.parent
112
+ nodes
113
+ end
114
+ end
115
+
116
+ #
117
+ def self_and_ancestors
118
+ [self] + ancestors
119
+ end
120
+
121
+ # Returns the root node of the tree.
122
+ def root
123
+ if self.dotted_ids
124
+ self.class.find(self.dotted_ids.split('.').first)
125
+ else
126
+ node = self
127
+ node = node.parent while node.parent
128
+ node
129
+ end
130
+ end
131
+
132
+ # Returns all siblings of the current node.
133
+ #
134
+ # subchild1.siblings # => [subchild2]
135
+ def siblings
136
+ self_and_siblings - [self]
137
+ end
138
+
139
+ # Returns all siblings and a reference to the current node.
140
+ #
141
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
142
+ def self_and_siblings
143
+ #parent ? parent.children : self.class.roots
144
+ self.class.find(:all, :conditions => {:parent_id => self.parent_id})
145
+ end
146
+
147
+ #
148
+ # root.ancestor_of?(subchild1) # => true
149
+ # subchild1.ancestor_of?(child1) # => false
150
+ def ancestor_of?(node)
151
+ node.dotted_ids.length > self.dotted_ids.length && node.dotted_ids.starts_with?(self.dotted_ids)
152
+ end
153
+
154
+ #
155
+ # subchild1.descendant_of?(child1) # => true
156
+ # root.descendant_of?(subchild1) # => false
157
+ def descendant_of?(node)
158
+ self.dotted_ids.length > node.dotted_ids.length && self.dotted_ids.starts_with?(node.dotted_ids)
159
+ end
160
+
161
+ # Returns all children of the current node
162
+ # root.all_children # => [child1, subchild1, subchild2]
163
+ def all_children
164
+ find_all_children_with_dotted_ids
165
+ end
166
+
167
+ # Returns all children of the current node
168
+ # root.self_and_all_children # => [root, child1, subchild1, subchild2]
169
+ def self_and_all_children
170
+ [self] + all_children
171
+ end
172
+
173
+ # Returns the depth of the node, root nodes have a depth of 0
174
+ def depth
175
+ self.dotted_ids.scan(/\./).size
176
+ end
177
+
178
+ protected
179
+
180
+ # Tranforms a dotted_id string into a pattern usable with a SQL LIKE statement
181
+ def dotted_id_like_pattern(prefix = nil)
182
+ (prefix || self.dotted_ids) + '.%'
183
+ end
184
+
185
+ # Find all children with the given dotted_id prefix
186
+ # *options* will be passed to to find(:all)
187
+ # FIXME: use merge_conditions when it will be part of the public API
188
+ def find_all_children_with_dotted_ids(prefix = nil, options = {})
189
+ self.class.find(:all, options.update(:conditions => ['dotted_ids LIKE ?', dotted_id_like_pattern(prefix)]))
190
+ end
191
+
192
+ # Generates the dotted_ids for this node
193
+ def build_dotted_ids
194
+ self.parent ? "#{self.parent.dotted_ids}.#{self.id}" : self.id.to_s
195
+ end
196
+
197
+ # After create, adds the dotted id's
198
+ def assign_dotted_ids
199
+ self.update_attribute(:dotted_ids, build_dotted_ids) if self.dotted_ids.blank?
200
+ end
201
+
202
+ # After validation on update, rebuild dotted ids if necessary
203
+ def update_dotted_ids
204
+ return unless parent_foreign_key_changed?
205
+ old_dotted_ids = self.dotted_ids
206
+ old_dotted_ids_regex = Regexp.new("^#{Regexp.escape(old_dotted_ids)}(.*)")
207
+ self.dotted_ids = build_dotted_ids
208
+ replace_pattern = "#{self.dotted_ids}\\1"
209
+ find_all_children_with_dotted_ids(old_dotted_ids).each do |node|
210
+ new_dotted_ids = node.dotted_ids.gsub(old_dotted_ids_regex, replace_pattern)
211
+ node.update_attribute(:dotted_ids, new_dotted_ids)
212
+ end
213
+ end
214
+
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,2 @@
1
+ require 'active_record/acts/tree_with_dotted_ids'
2
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TreeWithDottedIds
@@ -0,0 +1,377 @@
1
+ require 'test/unit'
2
+
3
+ require 'rubygems'
4
+ require 'active_record'
5
+
6
+ $:.unshift File.dirname(__FILE__) + '/../lib'
7
+
8
+ require 'active_record/acts/tree_with_dotted_ids'
9
+
10
+ require File.dirname(__FILE__) + '/../init'
11
+
12
+ class Test::Unit::TestCase
13
+ def assert_queries(num = 1)
14
+ $query_count = 0
15
+ yield
16
+ ensure
17
+ assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
18
+ end
19
+
20
+ def assert_no_queries(&block)
21
+ assert_queries(0, &block)
22
+ end
23
+ end
24
+
25
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
26
+
27
+ # AR keeps printing annoying schema statements
28
+ $stdout = StringIO.new
29
+
30
+ def setup_db
31
+ ActiveRecord::Base.logger
32
+ ActiveRecord::Schema.define(:version => 1) do
33
+ create_table :mixins do |t|
34
+ t.column :type, :string
35
+ t.column :parent_id, :integer
36
+ t.column :dotted_ids, :string
37
+ t.column :name, :string
38
+ end
39
+ end
40
+ end
41
+
42
+ def teardown_db
43
+ ActiveRecord::Base.connection.tables.each do |table|
44
+ ActiveRecord::Base.connection.drop_table(table)
45
+ end
46
+ end
47
+
48
+ class Mixin < ActiveRecord::Base
49
+ end
50
+
51
+ class TreeMixin < Mixin
52
+ acts_as_tree_with_dotted_ids :foreign_key => "parent_id", :order => "id"
53
+ end
54
+
55
+ class TreeMixinWithoutOrder < Mixin
56
+ acts_as_tree_with_dotted_ids :foreign_key => "parent_id"
57
+ end
58
+
59
+ class RecursivelyCascadedTreeMixin < Mixin
60
+ acts_as_tree_with_dotted_ids :foreign_key => "parent_id"
61
+ has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
62
+ end
63
+
64
+ class TreeTest < Test::Unit::TestCase
65
+
66
+ def setup
67
+ setup_db
68
+ @root1 = TreeMixin.create!
69
+ @root_child1 = TreeMixin.create! :parent_id => @root1.id
70
+ @child1_child = TreeMixin.create! :parent_id => @root_child1.id
71
+ @root_child2 = TreeMixin.create! :parent_id => @root1.id
72
+ @root2 = TreeMixin.create!
73
+ @root3 = TreeMixin.create!
74
+ end
75
+
76
+ def teardown
77
+ teardown_db
78
+ end
79
+
80
+ def test_children
81
+ assert_equal @root1.children, [@root_child1, @root_child2]
82
+ assert_equal @root_child1.children, [@child1_child]
83
+ assert_equal @child1_child.children, []
84
+ assert_equal @root_child2.children, []
85
+ end
86
+
87
+ def test_parent
88
+ assert_equal @root_child1.parent, @root1
89
+ assert_equal @root_child1.parent, @root_child2.parent
90
+ assert_nil @root1.parent
91
+ end
92
+
93
+ def test_delete
94
+ assert_equal 6, TreeMixin.count
95
+ @root1.destroy
96
+ assert_equal 2, TreeMixin.count
97
+ @root2.destroy
98
+ @root3.destroy
99
+ assert_equal 0, TreeMixin.count
100
+ end
101
+
102
+ def test_insert
103
+ @extra = @root1.children.create
104
+
105
+ assert @extra
106
+
107
+ assert_equal @extra.parent, @root1
108
+
109
+ assert_equal 3, @root1.children.size
110
+ assert @root1.children.include?(@extra)
111
+ assert @root1.children.include?(@root_child1)
112
+ assert @root1.children.include?(@root_child2)
113
+ end
114
+
115
+ def test_ancestors
116
+ assert_equal [], @root1.ancestors
117
+ assert_equal [@root1], @root_child1.ancestors
118
+ assert_equal [@root_child1, @root1], @child1_child.ancestors
119
+ assert_equal [@root1], @root_child2.ancestors
120
+ assert_equal [], @root2.ancestors
121
+ assert_equal [], @root3.ancestors
122
+ end
123
+
124
+ def test_root
125
+ assert_equal @root1, TreeMixin.root
126
+ assert_equal @root1, @root1.root
127
+ assert_equal @root1, @root_child1.root
128
+ assert_equal @root1, @child1_child.root
129
+ assert_equal @root1, @root_child2.root
130
+ assert_equal @root2, @root2.root
131
+ assert_equal @root3, @root3.root
132
+ end
133
+
134
+ def test_roots
135
+ assert_equal [@root1, @root2, @root3], TreeMixin.roots
136
+ end
137
+
138
+ def test_siblings
139
+ assert_equal [@root2, @root3], @root1.siblings
140
+ assert_equal [@root_child2], @root_child1.siblings
141
+ assert_equal [], @child1_child.siblings
142
+ assert_equal [@root_child1], @root_child2.siblings
143
+ assert_equal [@root1, @root3], @root2.siblings
144
+ assert_equal [@root1, @root2], @root3.siblings
145
+ end
146
+
147
+ def test_self_and_siblings
148
+ assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
149
+ assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
150
+ assert_equal [@child1_child], @child1_child.self_and_siblings
151
+ assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings
152
+ assert_equal [@root1, @root2, @root3], @root2.self_and_siblings
153
+ assert_equal [@root1, @root2, @root3], @root3.self_and_siblings
154
+ end
155
+ end
156
+
157
+ class TreeTestWithEagerLoading < Test::Unit::TestCase
158
+
159
+ def setup
160
+ teardown_db
161
+ setup_db
162
+ @root1 = TreeMixin.create!
163
+ @root_child1 = TreeMixin.create! :parent_id => @root1.id
164
+ @child1_child = TreeMixin.create! :parent_id => @root_child1.id
165
+ @root_child2 = TreeMixin.create! :parent_id => @root1.id
166
+ @root2 = TreeMixin.create!
167
+ @root3 = TreeMixin.create!
168
+
169
+ @rc1 = RecursivelyCascadedTreeMixin.create!
170
+ @rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
171
+ @rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id
172
+ @rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id
173
+ end
174
+
175
+ def teardown
176
+ teardown_db
177
+ end
178
+
179
+ def test_eager_association_loading
180
+ roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id")
181
+ assert_equal [@root1, @root2, @root3], roots
182
+ assert_no_queries do
183
+ assert_equal 2, roots[0].children.size
184
+ assert_equal 0, roots[1].children.size
185
+ assert_equal 0, roots[2].children.size
186
+ end
187
+ end
188
+
189
+ def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
190
+ root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :children => { :children => :children } }, :order => 'mixins.id')
191
+ assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first }
192
+ end
193
+
194
+ def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
195
+ root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :first_child => { :first_child => :first_child } }, :order => 'mixins.id')
196
+ assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child }
197
+ end
198
+
199
+ def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
200
+ leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC')
201
+ assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
202
+ end
203
+ end
204
+
205
+ class TreeTestWithoutOrder < Test::Unit::TestCase
206
+
207
+ def setup
208
+ setup_db
209
+ @root1 = TreeMixinWithoutOrder.create!
210
+ @root2 = TreeMixinWithoutOrder.create!
211
+ end
212
+
213
+ def teardown
214
+ teardown_db
215
+ end
216
+
217
+ def test_root
218
+ assert [@root1, @root2].include?(TreeMixinWithoutOrder.root)
219
+ end
220
+
221
+ def test_roots
222
+ assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
223
+ end
224
+ end
225
+
226
+ class TestDottedIdTree < Test::Unit::TestCase
227
+
228
+ def setup
229
+ setup_db
230
+ @tree = TreeMixin.create(:name => 'Root')
231
+ @child = @tree.children.create(:name => 'Child')
232
+ @subchild = @child.children.create(:name => 'Subchild')
233
+ @new_root = TreeMixin.create!(:name => 'New Root')
234
+ end
235
+
236
+ def teardown
237
+ teardown_db
238
+ end
239
+
240
+ def test_build_dotted_ids
241
+ assert_equal "#{@tree.id}", @tree.dotted_ids
242
+ assert_equal "#{@tree.id}.#{@child.id}", @child.dotted_ids
243
+ assert_equal "#{@tree.id}.#{@child.id}.#{@subchild.id}", @subchild.dotted_ids
244
+ end
245
+
246
+ def test_ancestor_of
247
+
248
+ assert @tree.ancestor_of?(@child)
249
+ assert @child.ancestor_of?(@subchild)
250
+ assert @tree.ancestor_of?(@subchild)
251
+
252
+ assert !@tree.ancestor_of?(@tree)
253
+ assert !@child.ancestor_of?(@child)
254
+ assert !@subchild.ancestor_of?(@subchild)
255
+
256
+ assert !@child.ancestor_of?(@tree)
257
+ assert !@subchild.ancestor_of?(@tree)
258
+ assert !@subchild.ancestor_of?(@child)
259
+
260
+ end
261
+
262
+ def test_descendant_of
263
+
264
+ assert @child.descendant_of?(@tree)
265
+ assert @subchild.descendant_of?(@child)
266
+ assert @subchild.descendant_of?(@tree)
267
+
268
+ assert !@tree.descendant_of?(@tree)
269
+ assert !@child.descendant_of?(@child)
270
+ assert !@subchild.descendant_of?(@subchild)
271
+
272
+ assert !@tree.descendant_of?(@child)
273
+ assert !@child.descendant_of?(@subchild)
274
+ assert !@tree.descendant_of?(@subchild)
275
+
276
+ end
277
+
278
+
279
+ def test_all_children
280
+
281
+ kids = @tree.all_children
282
+ assert_kind_of Array, kids
283
+ assert kids.size == 2
284
+ assert !kids.include?(@tree)
285
+ assert kids.include?(@child)
286
+ assert kids.include?(@subchild)
287
+
288
+ kids = @child.all_children
289
+ assert_kind_of Array, kids
290
+ assert kids.size == 1
291
+ assert !kids.include?(@child)
292
+ assert kids.include?(@subchild)
293
+
294
+ kids = @subchild.all_children
295
+ assert_kind_of Array, kids
296
+ assert kids.empty?
297
+
298
+ end
299
+
300
+ def test_rebuild
301
+
302
+ @tree.parent_id = @new_root.id
303
+ @tree.save
304
+
305
+ @new_root.reload
306
+ @root = @new_root.children.first
307
+ @child = @root.children.first
308
+ @subchild = @child.children.first
309
+
310
+ assert_equal "#{@new_root.id}", @new_root.dotted_ids
311
+ assert_equal "#{@new_root.id}.#{@tree.id}", @tree.dotted_ids
312
+ assert_equal "#{@new_root.id}.#{@tree.id}.#{@child.id}", @child.dotted_ids
313
+ assert_equal "#{@new_root.id}.#{@tree.id}.#{@child.id}.#{@subchild.id}", @subchild.dotted_ids
314
+ assert @tree.ancestor_of?(@subchild)
315
+ assert @new_root.ancestor_of?(@tree)
316
+
317
+ @subchild.parent = @tree
318
+ @subchild.save
319
+
320
+ assert_equal "#{@new_root.id}", @new_root.dotted_ids
321
+ assert_equal "#{@new_root.id}.#{@tree.id}", @tree.dotted_ids
322
+ assert_equal "#{@new_root.id}.#{@tree.id}.#{@child.id}", @child.dotted_ids
323
+ assert_equal "#{@new_root.id}.#{@tree.id}.#{@subchild.id}", @subchild.dotted_ids
324
+
325
+ @child.parent = nil
326
+ @child.save!
327
+
328
+ assert_equal "#{@new_root.id}", @new_root.dotted_ids
329
+ assert_equal "#{@new_root.id}.#{@tree.id}", @tree.dotted_ids
330
+ assert_equal "#{@child.id}", @child.dotted_ids
331
+ assert_equal "#{@new_root.id}.#{@tree.id}.#{@subchild.id}", @subchild.dotted_ids
332
+
333
+ end
334
+
335
+ def test_ancestors
336
+ assert @tree.ancestors.empty?
337
+ assert_equal [@tree], @child.ancestors
338
+ assert_equal [@child, @tree], @subchild.ancestors
339
+ end
340
+
341
+ def test_root
342
+ assert_equal @tree, @tree.root
343
+ assert_equal @tree, @child.root
344
+ assert_equal @tree, @subchild.root
345
+ end
346
+
347
+ def test_traverse
348
+
349
+ traversed_nodes = []
350
+ TreeMixin.traverse { |node| traversed_nodes << node }
351
+
352
+ assert_equal [@tree, @child, @subchild, @new_root], traversed_nodes
353
+
354
+ end
355
+
356
+ def test_rebuild_dotted_ids
357
+
358
+ TreeMixin.update_all('dotted_ids = NULL')
359
+ assert TreeMixin.find(:all).all? { |n| n.dotted_ids.blank? }
360
+ @subchild.reload
361
+ assert_nil @subchild.dotted_ids
362
+
363
+ TreeMixin.rebuild_dotted_ids!
364
+ assert TreeMixin.find(:all).all? { |n| n.dotted_ids.present? }
365
+ @subchild.reload
366
+ assert_equal "#{@tree.id}.#{@child.id}.#{@subchild.id}", @subchild.dotted_ids
367
+
368
+ end
369
+
370
+ def test_depth
371
+ assert_equal 0, @tree.depth
372
+ assert_equal 1, @child.depth
373
+ assert_equal 2, @subchild.depth
374
+ end
375
+
376
+ end
377
+
data/test/schema.rb ADDED
File without changes
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts-as-tree-with-dotted-ids
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - David Heinemeier Hansson
14
+ - Xavier Defrang
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-11-09 00:00:00 +01:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: activerecord
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 3
33
+ - 0
34
+ - 0
35
+ version: 3.0.0
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ description: ""
39
+ email: tma@freshbit.ch
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - README.rdoc
46
+ files:
47
+ - lib/active_record/acts/tree_with_dotted_ids.rb
48
+ - lib/acts-as-tree-with-dotted-ids.rb
49
+ - README.rdoc
50
+ - test/acts_as_tree_test.rb
51
+ - test/schema.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/tma/acts-as-tree-with-dotted-ids
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.7
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: A drop in replacement for acts_as_tree with super fast ancestors and subtree access
86
+ test_files:
87
+ - test/acts_as_tree_test.rb
88
+ - test/schema.rb