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.
- 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:
|