acts-as-tree-with-dotted-ids 1.0.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/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