closure_tree 3.0.0 → 3.0.1

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.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Matthew McEachen
1
+ Copyright (c) 2012 Matthew McEachen
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Closure Tree is a mostly-API-compatible replacement for the
4
4
  acts_as_tree and awesome_nested_set gems, but with much better
5
- mutation performance thanks to the Closure Tree storage algorithm.
5
+ mutation performance thanks to the Closure Tree storage algorithm,
6
+ as well as support for polymorphism within the hierarchy.
6
7
 
7
8
  See [Bill Karwin](http://karwin.blogspot.com/)'s excellent
8
9
  [Models for hierarchical data presentation](http://www.slideshare.net/billkarwin/models-for-hierarchical-data)
@@ -10,7 +11,7 @@ for a description of different tree storage algorithms.
10
11
 
11
12
  ## Setup
12
13
 
13
- Note that closure_tree is being developed for Rails 3.1.x
14
+ Note that closure_tree supports Rails 3. Rails 2, not so much.
14
15
 
15
16
  1. Add this to your Gemfile: ```gem 'closure_tree'```
16
17
 
@@ -22,35 +23,35 @@ Note that closure_tree is being developed for Rails 3.1.x
22
23
 
23
24
  Note that if the column is null, the tag will be considered a root node.
24
25
 
25
- ```ruby
26
- class AddParentIdToTag < ActiveRecord::Migration
27
- def change
28
- add_column :tag, :parent_id, :integer
29
- end
26
+ ```ruby
27
+ class AddParentIdToTag < ActiveRecord::Migration
28
+ def change
29
+ add_column :tag, :parent_id, :integer
30
30
  end
31
- ```
31
+ end
32
+ ```
32
33
 
33
34
  5. Add a database migration to store the hierarchy for your model. By
34
35
  convention the table name will be the model's table name, followed by
35
36
  "_hierarchy". Note that by calling ```acts_as_tree```, a "virtual model" (in this case, ```TagsHierarchy```) will be added automatically, so you don't need to create it.
36
37
 
37
- ```ruby
38
- class CreateTagHierarchies < ActiveRecord::Migration
39
- def change
40
- create_table :tag_hierarchies, :id => false do |t|
41
- t.integer :ancestor_id, :null => false # ID of the parent/grandparent/great-grandparent/... tag
42
- t.integer :descendant_id, :null => false # ID of the target tag
43
- t.integer :generations, :null => false # Number of generations between the ancestor and the descendant. Parent/child = 1, for example.
44
- end
38
+ ```ruby
39
+ class CreateTagHierarchies < ActiveRecord::Migration
40
+ def change
41
+ create_table :tag_hierarchies, :id => false do |t|
42
+ t.integer :ancestor_id, :null => false # ID of the parent/grandparent/great-grandparent/... tag
43
+ t.integer :descendant_id, :null => false # ID of the target tag
44
+ t.integer :generations, :null => false # Number of generations between the ancestor and the descendant. Parent/child = 1, for example.
45
+ end
45
46
 
46
- # For "all progeny of..." selects:
47
- add_index :tag_hierarchies, [:ancestor_id, :descendant_id], :unique => true
47
+ # For "all progeny of..." selects:
48
+ add_index :tag_hierarchies, [:ancestor_id, :descendant_id], :unique => true
48
49
 
49
- # For "all ancestors of..." selects
50
- add_index :tag_hierarchies, [:descendant_id]
51
- end
50
+ # For "all ancestors of..." selects
51
+ add_index :tag_hierarchies, [:descendant_id]
52
52
  end
53
- ```
53
+ end
54
+ ```
54
55
 
55
56
  6. Run ```rake db:migrate```
56
57
 
@@ -66,41 +67,41 @@ Note that closure_tree is being developed for Rails 3.1.x
66
67
 
67
68
  Create a root node:
68
69
 
69
- ```ruby
70
- grandparent = Tag.create(:name => 'Grandparent')
71
- ```
70
+ ```ruby
71
+ grandparent = Tag.create(:name => 'Grandparent')
72
+ ```
72
73
 
73
74
  Child nodes are created by appending to the children collection:
74
75
 
75
- ```ruby
76
- child = parent.children.create(:name => 'Child')
77
- ```
76
+ ```ruby
77
+ child = parent.children.create(:name => 'Child')
78
+ ```
78
79
 
79
80
  You can also append to the children collection:
80
81
 
81
- ```ruby
82
- child = Tag.create(:name => 'Child')
83
- parent.children << child
84
- ```
82
+ ```ruby
83
+ child = Tag.create(:name => 'Child')
84
+ parent.children << child
85
+ ```
85
86
 
86
87
  Or call the "add_child" method:
87
88
 
88
- ```ruby
89
- parent = Tag.create(:name => 'Parent')
90
- grandparent.add_child parent
91
- ```
89
+ ```ruby
90
+ parent = Tag.create(:name => 'Parent')
91
+ grandparent.add_child parent
92
+ ```
92
93
 
93
94
  Then:
94
95
 
95
- ```ruby
96
- puts grandparent.self_and_descendants.collect{ |t| t.name }.join(" > ")
97
- "grandparent > parent > child"
96
+ ```ruby
97
+ puts grandparent.self_and_descendants.collect{ |t| t.name }.join(" > ")
98
+ "grandparent > parent > child"
98
99
 
99
- child.ancestry_path
100
- ["grandparent", "parent", "child"]
101
- ```
100
+ child.ancestry_path
101
+ ["grandparent", "parent", "child"]
102
+ ```
102
103
 
103
- ### <code>find_or_create_by_path</code>
104
+ ### find_or_create_by_path
104
105
 
105
106
  We can do all the node creation and add_child calls from the prior section with one method call:
106
107
 
@@ -115,9 +116,9 @@ provided to ```acts_as_tree```.
115
116
 
116
117
  Note that any other AR fields can be set with the second, optional ```attributes``` argument.
117
118
 
118
- ```ruby
119
- child = Tag.find_or_create_by_path(%w{home chuck Photos"}, {:tag_type => "File"})
120
- ```
119
+ ```ruby
120
+ child = Tag.find_or_create_by_path(%w{home chuck Photos"}, {:tag_type => "File"})
121
+ ```
121
122
  This will pass the attribute hash of ```{:name => "home", :tag_type => "File"}``` to
122
123
  ```Tag.find_or_create_by_name``` if the root directory doesn't exist (and
123
124
  ```{:name => "chuck", :tag_type => "File"}``` if the second-level tag doesn't exist, and so on).
@@ -168,14 +169,14 @@ Polymorphic models are supported:
168
169
  1. Create a db migration that adds a String ```type``` column to your model
169
170
  2. Subclass the model class. You only need to add acts_as_tree to your base class.
170
171
 
171
- ```ruby
172
- class Tag < ActiveRecord::Base
173
- acts_as_tree
174
- end
175
- class WhenTag < Tag ; end
176
- class WhereTag < Tag ; end
177
- class WhatTag < Tag ; end
178
- ```
172
+ ```ruby
173
+ class Tag < ActiveRecord::Base
174
+ acts_as_tree
175
+ end
176
+ class WhenTag < Tag ; end
177
+ class WhereTag < Tag ; end
178
+ class WhatTag < Tag ; end
179
+ ```
179
180
 
180
181
  ## Change log
181
182
 
@@ -192,6 +193,10 @@ Polymorphic models are supported:
192
193
  * ```find_by_path``` and ```find_or_create_by_path``` signatures changed to support constructor attributes
193
194
  * tested against Rails 3.1.3
194
195
 
196
+ ### 3.0.1
197
+
198
+ * Support 3.2.0's fickle deprecation of InstanceMethods (Thanks, [jheiss](https://github.com/mceachen/closure_tree/pull/5))!
199
+
195
200
  ## Thanks to
196
201
 
197
202
  * https://github.com/collectiveidea/awesome_nested_set
data/Rakefile CHANGED
@@ -4,13 +4,13 @@ rescue LoadError
4
4
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
5
  end
6
6
 
7
+ Bundler::GemHelper.install_tasks
8
+
7
9
  require 'yard'
8
10
  YARD::Rake::YardocTask.new do |t|
9
11
  t.files = ['lib/**/*.rb', 'README.md']
10
12
  end
11
13
 
12
- Bundler::GemHelper.install_tasks
13
-
14
14
  require "rspec/core/rake_task"
15
15
  RSpec::Core::RakeTask.new(:spec)
16
16
 
@@ -66,162 +66,160 @@ module ClosureTree
66
66
 
67
67
  module Model
68
68
  extend ActiveSupport::Concern
69
- module InstanceMethods
70
69
 
71
- # Returns true if this node has no parents.
72
- def root?
73
- parent.nil?
74
- end
70
+ # Returns true if this node has no parents.
71
+ def root?
72
+ parent.nil?
73
+ end
75
74
 
76
- # Returns true if this node has a parent, and is not a root.
77
- def child?
78
- !parent.nil?
79
- end
75
+ # Returns true if this node has a parent, and is not a root.
76
+ def child?
77
+ !parent.nil?
78
+ end
80
79
 
81
- # Returns true if this node has no children.
82
- def leaf?
83
- children.empty?
84
- end
80
+ # Returns true if this node has no children.
81
+ def leaf?
82
+ children.empty?
83
+ end
85
84
 
86
- # Returns the farthest ancestor, or self if +root?+
87
- def root
88
- root? ? self : ancestors.last
89
- end
85
+ # Returns the farthest ancestor, or self if +root?+
86
+ def root
87
+ root? ? self : ancestors.last
88
+ end
90
89
 
91
- def leaves
92
- return [self] if leaf?
93
- self.class.leaves.where(<<-SQL
90
+ def leaves
91
+ return [self] if leaf?
92
+ self.class.leaves.where(<<-SQL
94
93
  #{quoted_table_name}.#{self.class.primary_key} IN (
95
- SELECT descendant_id
96
- FROM #{quoted_hierarchy_table_name}
97
- WHERE ancestor_id = #{id})
98
- SQL
99
- )
100
- end
101
-
102
- def level
103
- ancestors.size
104
- end
94
+ SELECT descendant_id
95
+ FROM #{quoted_hierarchy_table_name}
96
+ WHERE ancestor_id = #{id})
97
+ SQL
98
+ )
99
+ end
105
100
 
106
- def ancestors
107
- without_self(self_and_ancestors)
108
- end
101
+ def level
102
+ ancestors.size
103
+ end
109
104
 
110
- # Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
111
- # to the +name_column+.
112
- # (so child.ancestry_path == +%w{grandparent parent child}+
113
- def ancestry_path(to_s_column = name_column)
114
- self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
115
- end
105
+ def ancestors
106
+ without_self(self_and_ancestors)
107
+ end
116
108
 
117
- def descendants
118
- without_self(self_and_descendants)
119
- end
109
+ # Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
110
+ # to the +name_column+.
111
+ # (so child.ancestry_path == +%w{grandparent parent child}+
112
+ def ancestry_path(to_s_column = name_column)
113
+ self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
114
+ end
120
115
 
121
- def self_and_siblings
122
- self.class.scoped.where(:parent => parent)
123
- end
116
+ def descendants
117
+ without_self(self_and_descendants)
118
+ end
124
119
 
125
- def siblings
126
- without_self(self_and_siblings)
127
- end
120
+ def self_and_siblings
121
+ self.class.scoped.where(:parent => parent)
122
+ end
128
123
 
129
- # Alias for appending to the children collection.
130
- # You can also add directly to the children collection, if you'd prefer.
131
- def add_child(child_node)
132
- children << child_node
133
- child_node
134
- end
124
+ def siblings
125
+ without_self(self_and_siblings)
126
+ end
135
127
 
136
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
137
- def find_by_path(path)
138
- path = [path] unless path.is_a? Enumerable
139
- node = self
140
- while (!path.empty? && node)
141
- node = node.children.send("find_by_#{name_column}", path.shift)
142
- end
143
- node
144
- end
128
+ # Alias for appending to the children collection.
129
+ # You can also add directly to the children collection, if you'd prefer.
130
+ def add_child(child_node)
131
+ children << child_node
132
+ child_node
133
+ end
145
134
 
146
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
147
- def find_or_create_by_path(path, attributes = {})
148
- path = [path] unless path.is_a? Enumerable
149
- node = self
150
- attrs = {}
151
- attrs[:type] = self.type if ct_subclass? && ct_has_type?
152
- path.each do |name|
153
- attrs[name_sym] = name
154
- child = node.children.where(attrs).first
155
- unless child
156
- child = self.class.new(attributes.merge attrs)
157
- node.children << child
158
- end
159
- node = child
160
- end
161
- node
135
+ # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
136
+ def find_by_path(path)
137
+ path = [path] unless path.is_a? Enumerable
138
+ node = self
139
+ while (!path.empty? && node)
140
+ node = node.children.send("find_by_#{name_column}", path.shift)
162
141
  end
142
+ node
143
+ end
163
144
 
164
- protected
165
-
166
- def acts_as_tree_before_save
167
- @was_new_record = new_record?
168
- if changes[parent_column_name] &&
169
- parent.present? &&
170
- parent.self_and_ancestors.include?(self)
171
- # TODO: raise Ouroboros or Philip J. Fry error:
172
- raise ActiveRecord::ActiveRecordError "You cannot add an ancestor as a descendant"
145
+ # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
146
+ def find_or_create_by_path(path, attributes = {})
147
+ path = [path] unless path.is_a? Enumerable
148
+ node = self
149
+ attrs = {}
150
+ attrs[:type] = self.type if ct_subclass? && ct_has_type?
151
+ path.each do |name|
152
+ attrs[name_sym] = name
153
+ child = node.children.where(attrs).first
154
+ unless child
155
+ child = self.class.new(attributes.merge attrs)
156
+ node.children << child
173
157
  end
158
+ node = child
174
159
  end
160
+ node
161
+ end
175
162
 
176
- def acts_as_tree_after_save
177
- rebuild! if changes[parent_column_name] || @was_new_record
178
- end
163
+ protected
179
164
 
180
- def rebuild!
181
- delete_hierarchy_references unless @was_new_record
182
- hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
183
- unless root?
184
- connection.execute <<-SQL
185
- INSERT INTO #{quoted_hierarchy_table_name}
186
- (ancestor_id, descendant_id, generations)
187
- SELECT x.ancestor_id, #{id}, x.generations + 1
188
- FROM #{quoted_hierarchy_table_name} x
189
- WHERE x.descendant_id = #{self._parent_id}
190
- SQL
191
- end
192
- children.each { |c| c.rebuild! }
165
+ def acts_as_tree_before_save
166
+ @was_new_record = new_record?
167
+ if changes[parent_column_name] &&
168
+ parent.present? &&
169
+ parent.self_and_ancestors.include?(self)
170
+ # TODO: raise Ouroboros or Philip J. Fry error:
171
+ raise ActiveRecord::ActiveRecordError "You cannot add an ancestor as a descendant"
193
172
  end
173
+ end
194
174
 
195
- def acts_as_tree_before_destroy
196
- delete_hierarchy_references
197
- if closure_tree_options[:dependent] == :nullify
198
- children.each { |c| c.rebuild! }
199
- end
200
- end
175
+ def acts_as_tree_after_save
176
+ rebuild! if changes[parent_column_name] || @was_new_record
177
+ end
201
178
 
202
- def delete_hierarchy_references
203
- # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
204
- # It shouldn't affect performance of postgresql.
205
- # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
179
+ def rebuild!
180
+ delete_hierarchy_references unless @was_new_record
181
+ hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
182
+ unless root?
206
183
  connection.execute <<-SQL
207
- DELETE FROM #{quoted_hierarchy_table_name}
208
- WHERE descendant_id IN (
209
- SELECT DISTINCT descendant_id
210
- FROM ( SELECT descendant_id
211
- FROM #{quoted_hierarchy_table_name}
212
- WHERE ancestor_id = #{id}
213
- ) AS x )
214
- OR descendant_id = #{id}
184
+ INSERT INTO #{quoted_hierarchy_table_name}
185
+ (ancestor_id, descendant_id, generations)
186
+ SELECT x.ancestor_id, #{id}, x.generations + 1
187
+ FROM #{quoted_hierarchy_table_name} x
188
+ WHERE x.descendant_id = #{self._parent_id}
215
189
  SQL
216
190
  end
191
+ children.each { |c| c.rebuild! }
192
+ end
217
193
 
218
- def without_self(scope)
219
- scope.where(["#{quoted_table_name}.#{self.class.primary_key} != ?", self])
194
+ def acts_as_tree_before_destroy
195
+ delete_hierarchy_references
196
+ if closure_tree_options[:dependent] == :nullify
197
+ children.each { |c| c.rebuild! }
220
198
  end
199
+ end
221
200
 
222
- def _parent_id
223
- send(parent_column_name)
224
- end
201
+ def delete_hierarchy_references
202
+ # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
203
+ # It shouldn't affect performance of postgresql.
204
+ # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
205
+ connection.execute <<-SQL
206
+ DELETE FROM #{quoted_hierarchy_table_name}
207
+ WHERE descendant_id IN (
208
+ SELECT DISTINCT descendant_id
209
+ FROM ( SELECT descendant_id
210
+ FROM #{quoted_hierarchy_table_name}
211
+ WHERE ancestor_id = #{id}
212
+ ) AS x )
213
+ OR descendant_id = #{id}
214
+ SQL
215
+ end
216
+
217
+ def without_self(scope)
218
+ scope.where(["#{quoted_table_name}.#{self.class.primary_key} != ?", self])
219
+ end
220
+
221
+ def _parent_id
222
+ send(parent_column_name)
225
223
  end
226
224
 
227
225
  module ClassMethods
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.0.0" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.0.1" unless defined?(::ClosureTree::VERSION)
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 3
8
8
  - 0
9
- - 0
10
- version: 3.0.0
9
+ - 1
10
+ version: 3.0.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matthew McEachen
@@ -15,11 +15,11 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-11-27 00:00:00 -08:00
19
- default_executable:
18
+ date: 2012-01-30 00:00:00 Z
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
21
  prerelease: false
22
+ type: :runtime
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  none: false
25
25
  requirements:
@@ -31,7 +31,6 @@ dependencies:
31
31
  - 0
32
32
  - 0
33
33
  version: 3.0.0
34
- type: :runtime
35
34
  name: activerecord
36
35
  version_requirements: *id001
37
36
  description: " A mostly-API-compatible replacement for the acts_as_tree and awesome_nested_set gems,\n but with much better mutation performance thanks to the Closure Tree storage algorithm\n"
@@ -59,7 +58,6 @@ files:
59
58
  - spec/support/models.rb
60
59
  - spec/tag_spec.rb
61
60
  - spec/user_spec.rb
62
- has_rdoc: true
63
61
  homepage: http://matthew.mceachen.us/closure_tree
64
62
  licenses: []
65
63
 
@@ -89,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
87
  requirements: []
90
88
 
91
89
  rubyforge_project:
92
- rubygems_version: 1.6.2
90
+ rubygems_version: 1.8.15
93
91
  signing_key:
94
92
  specification_version: 3
95
93
  summary: Hierarchies for ActiveRecord models using a Closure Tree storage algorithm
@@ -102,3 +100,4 @@ test_files:
102
100
  - spec/support/models.rb
103
101
  - spec/tag_spec.rb
104
102
  - spec/user_spec.rb
103
+ has_rdoc: