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 +78 -0
- data/lib/active_record/acts/tree_with_dotted_ids.rb +218 -0
- data/lib/acts-as-tree-with-dotted-ids.rb +2 -0
- data/test/acts_as_tree_test.rb +377 -0
- data/test/schema.rb +0 -0
- metadata +88 -0
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,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
|