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.
- data/MIT-LICENSE +1 -1
- data/README.md +59 -54
- data/Rakefile +2 -2
- data/lib/closure_tree/acts_as_tree.rb +125 -127
- data/lib/closure_tree/version.rb +1 -1
- metadata +7 -8
data/MIT-LICENSE
CHANGED
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
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
47
|
+
# For "all progeny of..." selects:
|
48
|
+
add_index :tag_hierarchies, [:ancestor_id, :descendant_id], :unique => true
|
48
49
|
|
49
|
-
|
50
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
```ruby
|
90
|
+
parent = Tag.create(:name => 'Parent')
|
91
|
+
grandparent.add_child parent
|
92
|
+
```
|
92
93
|
|
93
94
|
Then:
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
|
96
|
+
```ruby
|
97
|
+
puts grandparent.self_and_descendants.collect{ |t| t.name }.join(" > ")
|
98
|
+
"grandparent > parent > child"
|
98
99
|
|
99
|
-
|
100
|
-
|
101
|
-
|
100
|
+
child.ancestry_path
|
101
|
+
["grandparent", "parent", "child"]
|
102
|
+
```
|
102
103
|
|
103
|
-
###
|
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
|
-
|
119
|
-
|
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
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
# Returns true if this node has no parents.
|
71
|
+
def root?
|
72
|
+
parent.nil?
|
73
|
+
end
|
75
74
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
80
|
+
# Returns true if this node has no children.
|
81
|
+
def leaf?
|
82
|
+
children.empty?
|
83
|
+
end
|
85
84
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
85
|
+
# Returns the farthest ancestor, or self if +root?+
|
86
|
+
def root
|
87
|
+
root? ? self : ancestors.last
|
88
|
+
end
|
90
89
|
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
101
|
+
def level
|
102
|
+
ancestors.size
|
103
|
+
end
|
109
104
|
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
116
|
+
def descendants
|
117
|
+
without_self(self_and_descendants)
|
118
|
+
end
|
124
119
|
|
125
|
-
|
126
|
-
|
127
|
-
|
120
|
+
def self_and_siblings
|
121
|
+
self.class.scoped.where(:parent => parent)
|
122
|
+
end
|
128
123
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
children << child_node
|
133
|
-
child_node
|
134
|
-
end
|
124
|
+
def siblings
|
125
|
+
without_self(self_and_siblings)
|
126
|
+
end
|
135
127
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
177
|
-
rebuild! if changes[parent_column_name] || @was_new_record
|
178
|
-
end
|
163
|
+
protected
|
179
164
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
219
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
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
|
data/lib/closure_tree/version.rb
CHANGED
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:
|
4
|
+
hash: 5
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 3
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 3.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:
|
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.
|
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:
|