shuber-acts_as_tree 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/CHANGELOG ADDED
@@ -0,0 +1,14 @@
1
+ 2009-01-15 - Sean Huber (shuber@huberry.com)
2
+ * Remove unused test/fixture files
3
+ * Remove nested active_record/acts/tree file structure
4
+ * Update README
5
+ * Add MIT-LICENSE
6
+ * Update documentation
7
+ * Clean up logic
8
+ * Gemify
9
+
10
+ 2008-08-18 - Sean Huber (shuber@huberry.com)
11
+ * Added is_root? instance method and tests
12
+
13
+ 2008-08-18 - Sean Huber (shuber@huberry.com)
14
+ * The foreign_key for "parent" can't be a reference to the current node or any of its children
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 David Heinemeier Hansson
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.markdown ADDED
@@ -0,0 +1,45 @@
1
+ # acts\_as\_tree #
2
+
3
+ Specify this `acts_as` extension if you want to model a tree structure by providing parent and children associations
4
+
5
+
6
+ ## Installation ##
7
+
8
+ gem install shuber-acts_as_tree --source http://gems.github.com
9
+ OR
10
+ script/plugin install git://github.com/shuber/acts_as_tree.git
11
+
12
+
13
+ ## Usage ##
14
+
15
+ Requires a `:foreign_key` option which defaults to `:parent_id`
16
+
17
+ class Category < ActiveRecord::Base
18
+ # schema
19
+ # id, integer
20
+ # parent_id, integer
21
+ # name, string
22
+
23
+ acts_as_tree
24
+ end
25
+
26
+ Also accepts `:order` and `:counter_cache` (defaults to `false`) options
27
+
28
+
29
+ ## Example ##
30
+
31
+ root
32
+ \_ child1
33
+ \_ subchild1
34
+ \_ subchild2
35
+
36
+ root = Category.create(:name => 'root')
37
+ child1 = root.children.create(:name => 'child1')
38
+ subchild1 = child1.children.create(:name => 'subchild1')
39
+
40
+ root.parent # nil
41
+ child1.parent # root
42
+ root.children # [child1]
43
+ root.children.first.children.first # subchild1
44
+
45
+ Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test acts_as_tree gem/plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for acts_as_tree gem/plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'acts_as_tree'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README.markdown')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_tree'
@@ -0,0 +1,125 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module Tree
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # Returns all children (recursively) of the current node
9
+ #
10
+ # parent.all_children # [child1, child1_child1, child1_child2, child2, child2_child1, child3]
11
+ def all_children
12
+ self_and_all_children - [self]
13
+ end
14
+
15
+ # Returns list of ancestors, starting from parent until root
16
+ #
17
+ # subchild1.ancestors # [child1, root]
18
+ def ancestors
19
+ node, nodes = self, []
20
+ nodes << node = node.parent while node.parent
21
+ nodes
22
+ end
23
+
24
+ # Checks if the current node is a root
25
+ #
26
+ # parent.is_root? # true
27
+ # child.is_root? # false
28
+ def is_root?
29
+ !new_record? && parent.nil?
30
+ end
31
+
32
+ # Returns the root node of the tree
33
+ def root
34
+ node = self
35
+ node = node.parent while node.parent
36
+ node
37
+ end
38
+
39
+ # Returns all siblings of the current node
40
+ #
41
+ # subchild1.siblings # [subchild2]
42
+ def siblings
43
+ self_and_siblings - [self]
44
+ end
45
+
46
+ # Returns all children (recursively) and a reference to the current node
47
+ #
48
+ # parent.self_and_all_children # [parent, child1, child1_child1, child1_child2, child2, child2_child1, child3]
49
+ def self_and_all_children
50
+ children.inject([self]) { |array, child| array += child.self_and_all_children }.flatten
51
+ end
52
+
53
+ # Returns all siblings and a reference to the current node
54
+ #
55
+ # subchild1.self_and_siblings # [subchild1, subchild2]
56
+ def self_and_siblings
57
+ parent ? parent.children : self.class.roots
58
+ end
59
+
60
+ module ClassMethods
61
+ # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
62
+ # association. This requires that you have a foreign key column, which by default is called +parent_id+.
63
+ #
64
+ # class Category < ActiveRecord::Base
65
+ # acts_as_tree :order => :name
66
+ # end
67
+ #
68
+ # Example:
69
+ # root
70
+ # \_ child1
71
+ # \_ subchild1
72
+ # \_ subchild2
73
+ #
74
+ # root = Category.create(:name => 'root')
75
+ # child1 = root.children.create(:name => 'child1')
76
+ # subchild1 = child1.children.create(:name => 'subchild1')
77
+ #
78
+ # root.parent # nil
79
+ # child1.parent # root
80
+ # root.children # [child1]
81
+ # root.children.first.children.first # subchild1
82
+ #
83
+ # In addition to the parent and children associations, the following instance methods are added to the class
84
+ # after calling <tt>acts_as_tree</tt>:
85
+ # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
86
+ # * <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>)
87
+ # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
88
+ # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
89
+ # * <tt>all_children</tt> - Returns all of the children of the parent recursively
90
+ # * <tt>self_and_all_children</tt> - Returns the parent and all of its children recursively
91
+ #
92
+ # Configuration options are:
93
+ #
94
+ # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
95
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
96
+ # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
97
+ def acts_as_tree(options = {})
98
+ options = { :class_name => name, :counter_cache => nil, :foreign_key => :parent_id, :order => nil }.merge(options)
99
+ options[:order] = "#{table_name}.#{options[:order]} ASC" if options[:order].is_a?(Symbol)
100
+
101
+ belongs_to :parent, :class_name => options[:class_name], :foreign_key => options[:foreign_key], :counter_cache => options[:counter_cache]
102
+ has_many :children, :class_name => options[:class_name], :foreign_key => options[:foreign_key], :order => options[:order], :dependent => :destroy
103
+
104
+ named_scope :roots, :conditions => "#{table_name}.#{options[:foreign_key]} IS NULL", :order => options[:order]
105
+
106
+ validate_on_update :ensure_foreign_key_does_not_reference_self_or_all_children
107
+
108
+ define_method 'ensure_foreign_key_does_not_reference_self_or_all_children' do
109
+ if self_and_all_children.detect { |node| node.id == send(options[:foreign_key]) }
110
+ self.errors.add(options[:foreign_key], "can't be a reference to the current node or any of its children")
111
+ false
112
+ end
113
+ end
114
+ end
115
+
116
+ # Returns the first root node
117
+ def root
118
+ roots.find(:first)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree
@@ -0,0 +1,251 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ gem 'activerecord', '>= 2.1.0'
4
+ require 'active_record'
5
+ require File.dirname(__FILE__) + '/../lib/acts_as_tree'
6
+
7
+ class Test::Unit::TestCase
8
+ def assert_queries(num = 1)
9
+ $query_count = 0
10
+ yield
11
+ ensure
12
+ assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
13
+ end
14
+
15
+ def assert_no_queries(&block)
16
+ assert_queries(0, &block)
17
+ end
18
+ end
19
+
20
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
21
+
22
+ # AR keeps printing annoying schema statements
23
+ $stdout = StringIO.new
24
+
25
+ def setup_db
26
+ ActiveRecord::Base.logger
27
+ ActiveRecord::Schema.define(:version => 1) do
28
+ create_table :mixins do |t|
29
+ t.column :type, :string
30
+ t.column :parent_id, :integer
31
+ end
32
+ end
33
+ end
34
+
35
+ def teardown_db
36
+ ActiveRecord::Base.connection.tables.each do |table|
37
+ ActiveRecord::Base.connection.drop_table(table)
38
+ end
39
+ end
40
+
41
+ class Mixin < ActiveRecord::Base
42
+ end
43
+
44
+ class TreeMixin < Mixin
45
+ acts_as_tree :foreign_key => "parent_id", :order => "id"
46
+ end
47
+
48
+ class TreeMixinWithoutOrder < Mixin
49
+ acts_as_tree :foreign_key => "parent_id"
50
+ end
51
+
52
+ class RecursivelyCascadedTreeMixin < Mixin
53
+ acts_as_tree :foreign_key => "parent_id"
54
+ has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
55
+ end
56
+
57
+ class TreeTest < Test::Unit::TestCase
58
+
59
+ def setup
60
+ setup_db
61
+ @root1 = TreeMixin.create!
62
+ @root_child1 = TreeMixin.create! :parent_id => @root1.id
63
+ @child1_child = TreeMixin.create! :parent_id => @root_child1.id
64
+ @root_child2 = TreeMixin.create! :parent_id => @root1.id
65
+ @root2 = TreeMixin.create!
66
+ @root3 = TreeMixin.create!
67
+ end
68
+
69
+ def teardown
70
+ teardown_db
71
+ end
72
+
73
+ def test_children
74
+ assert_equal @root1.children, [@root_child1, @root_child2]
75
+ assert_equal @root_child1.children, [@child1_child]
76
+ assert_equal @child1_child.children, []
77
+ assert_equal @root_child2.children, []
78
+ end
79
+
80
+ def test_parent
81
+ assert_equal @root_child1.parent, @root1
82
+ assert_equal @root_child1.parent, @root_child2.parent
83
+ assert_nil @root1.parent
84
+ end
85
+
86
+ def test_delete
87
+ assert_equal 6, TreeMixin.count
88
+ @root1.destroy
89
+ assert_equal 2, TreeMixin.count
90
+ @root2.destroy
91
+ @root3.destroy
92
+ assert_equal 0, TreeMixin.count
93
+ end
94
+
95
+ def test_insert
96
+ @extra = @root1.children.create
97
+
98
+ assert @extra
99
+
100
+ assert_equal @extra.parent, @root1
101
+
102
+ assert_equal 3, @root1.children.size
103
+ assert @root1.children.include?(@extra)
104
+ assert @root1.children.include?(@root_child1)
105
+ assert @root1.children.include?(@root_child2)
106
+ end
107
+
108
+ def test_ancestors
109
+ assert_equal [], @root1.ancestors
110
+ assert_equal [@root1], @root_child1.ancestors
111
+ assert_equal [@root_child1, @root1], @child1_child.ancestors
112
+ assert_equal [@root1], @root_child2.ancestors
113
+ assert_equal [], @root2.ancestors
114
+ assert_equal [], @root3.ancestors
115
+ end
116
+
117
+ def test_root
118
+ assert_equal @root1, TreeMixin.root
119
+ assert_equal @root1, @root1.root
120
+ assert_equal @root1, @root_child1.root
121
+ assert_equal @root1, @child1_child.root
122
+ assert_equal @root1, @root_child2.root
123
+ assert_equal @root2, @root2.root
124
+ assert_equal @root3, @root3.root
125
+ end
126
+
127
+ def test_roots
128
+ assert_equal [@root1, @root2, @root3], TreeMixin.roots
129
+ end
130
+
131
+ def test_siblings
132
+ assert_equal [@root2, @root3], @root1.siblings
133
+ assert_equal [@root_child2], @root_child1.siblings
134
+ assert_equal [], @child1_child.siblings
135
+ assert_equal [@root_child1], @root_child2.siblings
136
+ assert_equal [@root1, @root3], @root2.siblings
137
+ assert_equal [@root1, @root2], @root3.siblings
138
+ end
139
+
140
+ def test_self_and_siblings
141
+ assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
142
+ assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
143
+ assert_equal [@child1_child], @child1_child.self_and_siblings
144
+ assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings
145
+ assert_equal [@root1, @root2, @root3], @root2.self_and_siblings
146
+ assert_equal [@root1, @root2, @root3], @root3.self_and_siblings
147
+ end
148
+
149
+ def test_all_children
150
+ assert_equal [@root_child1, @child1_child, @root_child2], @root1.all_children
151
+ assert_equal [@child1_child], @root_child1.all_children
152
+ assert_equal [], @child1_child.all_children
153
+ end
154
+
155
+ def test_self_and_all_children
156
+ assert_equal [@root1, @root_child1, @child1_child, @root_child2], @root1.self_and_all_children
157
+ assert_equal [@child1_child], @child1_child.self_and_all_children
158
+ end
159
+
160
+ def test_should_not_reference_self_as_parent_id_on_update
161
+ @root1.parent_id = @root1.id
162
+ assert !@root1.save
163
+ assert @root1.errors.on(:parent_id)
164
+ end
165
+
166
+ def test_should_not_reference_children_as_parent_id_on_update
167
+ @root1.parent_id = @root_child1.id
168
+ assert !@root1.save
169
+ assert @root1.errors.on(:parent_id)
170
+
171
+ @root1.reload
172
+ @root1.parent_id = @child1_child.id
173
+ assert !@root1.save
174
+ assert @root1.errors.on(:parent_id)
175
+ end
176
+
177
+ def test_is_root
178
+ assert @root1.is_root?
179
+ assert !@root_child1.is_root?
180
+ assert !TreeMixin.new.is_root?
181
+ end
182
+ end
183
+
184
+ class TreeTestWithEagerLoading < Test::Unit::TestCase
185
+
186
+ def setup
187
+ teardown_db
188
+ setup_db
189
+ @root1 = TreeMixin.create!
190
+ @root_child1 = TreeMixin.create! :parent_id => @root1.id
191
+ @child1_child = TreeMixin.create! :parent_id => @root_child1.id
192
+ @root_child2 = TreeMixin.create! :parent_id => @root1.id
193
+ @root2 = TreeMixin.create!
194
+ @root3 = TreeMixin.create!
195
+
196
+ @rc1 = RecursivelyCascadedTreeMixin.create!
197
+ @rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
198
+ @rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id
199
+ @rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id
200
+ end
201
+
202
+ def teardown
203
+ teardown_db
204
+ end
205
+
206
+ def test_eager_association_loading
207
+ roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id")
208
+ assert_equal [@root1, @root2, @root3], roots
209
+ assert_no_queries do
210
+ assert_equal 2, roots[0].children.size
211
+ assert_equal 0, roots[1].children.size
212
+ assert_equal 0, roots[2].children.size
213
+ end
214
+ end
215
+
216
+ def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
217
+ root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :children => { :children => :children } }, :order => 'mixins.id')
218
+ assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first }
219
+ end
220
+
221
+ def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
222
+ root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :first_child => { :first_child => :first_child } }, :order => 'mixins.id')
223
+ assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child }
224
+ end
225
+
226
+ def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
227
+ leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC')
228
+ assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
229
+ end
230
+ end
231
+
232
+ class TreeTestWithoutOrder < Test::Unit::TestCase
233
+
234
+ def setup
235
+ setup_db
236
+ @root1 = TreeMixinWithoutOrder.create!
237
+ @root2 = TreeMixinWithoutOrder.create!
238
+ end
239
+
240
+ def teardown
241
+ teardown_db
242
+ end
243
+
244
+ def test_root
245
+ assert [@root1, @root2].include?(TreeMixinWithoutOrder.root)
246
+ end
247
+
248
+ def test_roots
249
+ assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
250
+ end
251
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shuber-acts_as_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Huber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-15 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Allows you to create tree structures with ActiveRecord
17
+ email: shuber@huberry.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - CHANGELOG
26
+ - init.rb
27
+ - lib/acts_as_tree.rb
28
+ - MIT-LICENSE
29
+ - Rakefile
30
+ - README.markdown
31
+ has_rdoc: false
32
+ homepage: http://github.com/shuber/acts_as_tree
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --line-numbers
36
+ - --inline-source
37
+ - --main
38
+ - README.markdown
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: Allows you to create tree structures with ActiveRecord
60
+ test_files:
61
+ - test/acts_as_tree_test.rb