closure_tree 3.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: