closure_tree 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -105,12 +105,22 @@ Then:
105
105
  We can do all the node creation and add_child calls from the prior section with one method call:
106
106
 
107
107
  ```ruby
108
- child = Tag.find_or_create_by_path("grandparent", "parent", "child")
108
+ child = Tag.find_or_create_by_path(["grandparent", "parent", "child"])
109
109
  ```
110
110
 
111
- You can ```find``` as well as ```find_or_create``` by "ancestry paths". Ancestry paths may be built using any column in your model. The default column is ```name```, which can be changed with the :name_column option provided to ```acts_as_tree```.
111
+ You can ```find``` as well as ```find_or_create``` by "ancestry paths".
112
+ Ancestry paths may be built using any column in your model. The default
113
+ column is ```name```, which can be changed with the :name_column option
114
+ provided to ```acts_as_tree```.
112
115
 
113
- Note that the other columns will be null if nodes are created, other than auto-generated columns like ID and created_at timestamp. Only the specified column will receive the path element value.
116
+ Note that any other AR fields can be set with the second, optional ```attributes``` argument.
117
+
118
+ ```ruby
119
+ child = Tag.find_or_create_by_path(%w{home chuck Photos"}, {:tag_type => "File"})
120
+ ```
121
+ This will pass the attribute hash of ```{:name => "home", :tag_type => "File"}``` to
122
+ ```Tag.find_or_create_by_name``` if the root directory doesn't exist (and
123
+ ```{:name => "chuck", :tag_type => "File"}``` if the second-level tag doesn't exist, and so on).
114
124
 
115
125
  ### Available options
116
126
  <a id="options" />
@@ -129,35 +139,58 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
129
139
 
130
140
  ### Class methods
131
141
 
132
- * ``` Tag.root``` returns an arbitrary root node
133
- * ``` Tag.roots``` returns all root nodes
134
- * ``` Tag.leaves``` returns all leaf nodes
142
+ * ```Tag.root``` returns an arbitrary root node
143
+ * ```Tag.roots``` returns all root nodes
144
+ * ```Tag.leaves``` returns all leaf nodes
135
145
 
136
146
  ### Instance methods
137
147
 
138
- * ``` tag.root``` returns the root for this node
139
- * ``` tag.root?``` returns true if this is a root node
140
- * ``` tag.child?``` returns true if this is a child node. It has a parent.
141
- * ``` tag.leaf?``` returns true if this is a leaf node. It has no children.
142
- * ``` tag.leaves``` returns an array of all the nodes in self_and_descendants that are leaves.
143
- * ``` tag.level``` returns the level, or "generation", for this node in the tree. A root node == 0.
144
- * ``` tag.parent``` returns the node's immediate parent. Root nodes will return nil.
145
- * ``` tag.children``` returns an array of immediate children (just those nodes whose parent is the current node).
146
- * ``` tag.ancestors``` returns an array of [ parent, grandparent, great grandparent, ... ]. Note that the size of this array will always equal ```tag.level```.
147
- * ``` tag.self_and_ancestors``` returns an array of self, parent, grandparent, great grandparent, etc.
148
- * ``` tag.siblings``` returns an array of brothers and sisters (all at that level), excluding self.
149
- * ``` tag.self_and_siblings``` returns an array of brothers and sisters (all at that level), including self.
150
- * ``` tag.descendants``` returns an array of all children, childrens' children, etc., excluding self.
151
- * ``` tag.self_and_descendants``` returns an array of all children, childrens' children, etc., including self.
152
- * ``` tag.destroy``` will destroy a node and do <em>something</em> to its children, which is determined by the ```:dependent``` option passed to ```acts_as_tree```.
153
-
154
- ## Changelog
155
-
156
- ### 2.0.0.beta1
148
+ * ```tag.root``` returns the root for this node
149
+ * ```tag.root?``` returns true if this is a root node
150
+ * ```tag.child?``` returns true if this is a child node. It has a parent.
151
+ * ```tag.leaf?``` returns true if this is a leaf node. It has no children.
152
+ * ```tag.leaves``` returns an array of all the nodes in self_and_descendants that are leaves.
153
+ * ```tag.level``` returns the level, or "generation", for this node in the tree. A root node == 0.
154
+ * ```tag.parent``` returns the node's immediate parent. Root nodes will return nil.
155
+ * ```tag.children``` returns an array of immediate children (just those nodes whose parent is the current node).
156
+ * ```tag.ancestors``` returns an array of [ parent, grandparent, great grandparent, ... ]. Note that the size of this array will always equal ```tag.level```.
157
+ * ```tag.self_and_ancestors``` returns an array of self, parent, grandparent, great grandparent, etc.
158
+ * ```tag.siblings``` returns an array of brothers and sisters (all at that level), excluding self.
159
+ * ```tag.self_and_siblings``` returns an array of brothers and sisters (all at that level), including self.
160
+ * ```tag.descendants``` returns an array of all children, childrens' children, etc., excluding self.
161
+ * ```tag.self_and_descendants``` returns an array of all children, childrens' children, etc., including self.
162
+ * ```tag.destroy``` will destroy a node and do <em>something</em> to its children, which is determined by the ```:dependent``` option passed to ```acts_as_tree```.
163
+
164
+ ## Polymorphic hierarchies
165
+
166
+ Polymorphic models are supported:
167
+
168
+ 1. Create a db migration that adds a String ```type``` column to your model
169
+ 2. Subclass the model class. You only need to add acts_as_tree to your base class.
170
+
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
+ ```
179
+
180
+ ## Change log
181
+
182
+ ### 2.0.0
157
183
 
158
184
  * Had to increment the major version, as rebuild! will need to be called by prior consumers to support the new ```leaves``` class and instance methods.
159
185
  * Tag deletion is supported now along with ```:dependent => :destroy``` and ```:dependent => :delete_all```
160
186
  * Switched from default rails plugin directory structure to rspec
187
+ * Support for running specs under different database engines: ```export DB ; for DB in sqlite3 mysql postgresql ; do rake ; done```
188
+
189
+ ### 3.0.0
190
+
191
+ * Support for polymorphic trees
192
+ * ```find_by_path``` and ```find_or_create_by_path``` signatures changed to support constructor attributes
193
+ * tested against Rails 3.1.3
161
194
 
162
195
  ## Thanks to
163
196
 
@@ -10,6 +10,8 @@ module ClosureTree
10
10
  :name_column => 'name'
11
11
  }.merge(options)
12
12
 
13
+ raise IllegalArgumentException, "name_column can't be 'path'" if closure_tree_options[:name_column] == 'path'
14
+
13
15
  include ClosureTree::Columns
14
16
  extend ClosureTree::Columns
15
17
 
@@ -19,8 +21,8 @@ module ClosureTree
19
21
  self.hierarchy_class = Object.const_set hierarchy_class_name, Class.new(ActiveRecord::Base)
20
22
 
21
23
  self.hierarchy_class.class_eval <<-RUBY
22
- belongs_to :ancestor, :class_name => "#{base_class.to_s}"
23
- belongs_to :descendant, :class_name => "#{base_class.to_s}"
24
+ belongs_to :ancestor, :class_name => "#{ct_class.to_s}"
25
+ belongs_to :descendant, :class_name => "#{ct_class.to_s}"
24
26
  RUBY
25
27
 
26
28
  include ClosureTree::Model
@@ -30,23 +32,23 @@ module ClosureTree
30
32
  after_save :acts_as_tree_after_save
31
33
 
32
34
  belongs_to :parent,
33
- :class_name => base_class.to_s,
35
+ :class_name => ct_class.to_s,
34
36
  :foreign_key => parent_column_name
35
37
 
36
38
  has_many :children,
37
- :class_name => base_class.to_s,
39
+ :class_name => ct_class.to_s,
38
40
  :foreign_key => parent_column_name,
39
41
  :dependent => closure_tree_options[:dependent]
40
42
 
41
43
  has_and_belongs_to_many :self_and_ancestors,
42
- :class_name => base_class.to_s,
44
+ :class_name => ct_class.to_s,
43
45
  :join_table => hierarchy_table_name,
44
46
  :foreign_key => "descendant_id",
45
47
  :association_foreign_key => "ancestor_id",
46
48
  :order => "generations asc"
47
49
 
48
50
  has_and_belongs_to_many :self_and_descendants,
49
- :class_name => base_class.to_s,
51
+ :class_name => ct_class.to_s,
50
52
  :join_table => hierarchy_table_name,
51
53
  :foreign_key => "ancestor_id",
52
54
  :association_foreign_key => "descendant_id",
@@ -124,21 +126,39 @@ module ClosureTree
124
126
  without_self(self_and_siblings)
125
127
  end
126
128
 
127
- # alias for appending to the children collect
129
+ # Alias for appending to the children collection.
130
+ # You can also add directly to the children collection, if you'd prefer.
128
131
  def add_child(child_node)
129
132
  children << child_node
130
133
  child_node
131
134
  end
132
135
 
133
136
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
134
- # If the first argument is a symbol, it will be used as the column to search by
135
- def find_by_path(*path)
136
- foc_by_path("find", *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
137
144
  end
138
145
 
139
146
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
140
- def find_or_create_by_path(*path)
141
- foc_by_path("find_or_create", *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
142
162
  end
143
163
 
144
164
  protected
@@ -195,16 +215,6 @@ module ClosureTree
195
215
  SQL
196
216
  end
197
217
 
198
- def foc_by_path(method_prefix, *path)
199
- path = path.flatten
200
- return self if path.empty?
201
- node = self
202
- while (!path.empty? && node)
203
- node = node.children.send("#{method_prefix}_by_#{name_column}", path.shift)
204
- end
205
- node
206
- end
207
-
208
218
  def without_self(scope)
209
219
  scope.where(["#{quoted_table_name}.#{self.class.primary_key} != ?", self])
210
220
  end
@@ -230,17 +240,21 @@ module ClosureTree
230
240
  end
231
241
 
232
242
  # Find the node whose +ancestry_path+ is +path+
233
- def find_by_path(*path)
234
- path = path.flatten
235
- r = roots.send("find_by_#{name_column}", path.shift)
236
- r.nil? ? nil : r.find_by_path(*path)
243
+ def find_by_path(path)
244
+ root = roots.send("find_by_#{name_column}", path.shift)
245
+ root.try(:find_by_path, path)
237
246
  end
238
247
 
239
248
  # Find or create nodes such that the +ancestry_path+ is +path+
240
- def find_or_create_by_path(*path)
241
- path = path.flatten
242
- root = roots.send("find_or_create_by_#{name_column}", path.shift)
243
- root.find_or_create_by_path(*path)
249
+ def find_or_create_by_path(path, attributes = {})
250
+ name = path.shift
251
+ # shenanigans because find_or_create can't infer we want the same class as this:
252
+ # Note that roots will already be constrained to this subclass (in the case of polymorphism):
253
+ root = roots.send("find_by_#{name_column}", name)
254
+ if root.nil?
255
+ root = create!(attributes.merge(name_sym => name))
256
+ end
257
+ root.find_or_create_by_path(path, attributes)
244
258
  end
245
259
  end
246
260
  end
@@ -289,6 +303,18 @@ module ClosureTree
289
303
  (self.is_a?(Class) ? self : self.class)
290
304
  end
291
305
 
306
+ def ct_subclass?
307
+ ct_class != ct_class.base_class
308
+ end
309
+
310
+ def ct_attribute_names
311
+ @ct_attr_names ||= ct_class.new.attributes.keys - ct_class.protected_attributes.to_a
312
+ end
313
+
314
+ def ct_has_type?
315
+ ct_attribute_names.include? 'type'
316
+ end
317
+
292
318
  def ct_table_name
293
319
  ct_class.table_name
294
320
  end
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "2.0.0" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.0.0" unless defined?(::ClosureTree::VERSION)
3
3
  end
data/spec/db/schema.rb CHANGED
@@ -37,4 +37,20 @@ ActiveRecord::Schema.define(:version => 0) do
37
37
 
38
38
  add_index :referral_hierarchies, [:ancestor_id, :descendant_id], :unique => true
39
39
  add_index :referral_hierarchies, [:descendant_id]
40
+
41
+ create_table "labels", :force => true do |t|
42
+ t.string "name"
43
+ t.string "type"
44
+ t.integer "parent_id"
45
+ end
46
+
47
+ create_table "label_hierarchies", :id => false, :force => true do |t|
48
+ t.integer "ancestor_id", :null => false
49
+ t.integer "descendant_id", :null => false
50
+ t.integer "generations", :null => false
51
+ end
52
+
53
+ add_index :label_hierarchies, [:ancestor_id, :descendant_id], :unique => true
54
+ add_index :label_hierarchies, [:descendant_id]
55
+
40
56
  end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ def nuke_db
4
+ Label.delete_all
5
+ LabelHierarchy.delete_all
6
+ end
7
+
8
+ describe Label do
9
+ context "Base Label class" do
10
+ it "should find or create by path" do
11
+ # class method:
12
+ c = Label.find_or_create_by_path(%w{grandparent parent child})
13
+ c.ancestry_path.should == %w{grandparent parent child}
14
+ c.name.should == "child"
15
+ c.parent.name.should == "parent"
16
+ end
17
+ end
18
+ context "DateLabel" do
19
+
20
+ it "should find or create by path" do
21
+ date = DateLabel.find_or_create_by_path(%w{2011 November 23})
22
+ date.ancestry_path.should == %w{2011 November 23}
23
+ date.parent
24
+ date.self_and_ancestors.each { |ea| ea.class.should == DateLabel }
25
+ date.name.should == "23"
26
+ date.parent.name.should == "November"
27
+ end
28
+ end
29
+
30
+ context "DirectoryLabel" do
31
+ it "should find or create by path" do
32
+ dir = DirectoryLabel.find_or_create_by_path(%w{grandparent parent child})
33
+ dir.ancestry_path.should == %w{grandparent parent child}
34
+ dir.name.should == "child"
35
+ dir.parent.name.should == "parent"
36
+ dir.parent.parent.name.should == "grandparent"
37
+ dir.root.name.should == "grandparent"
38
+ dir.id.should_not == Label.find_or_create_by_path(%w{grandparent parent child})
39
+ dir.self_and_ancestors.each { |ea| ea.class.should == DirectoryLabel }
40
+ end
41
+ end
42
+
43
+ context "Mixed class tree" do
44
+ it "should support mixed type ancestors" do
45
+ [Label, DateLabel, DirectoryLabel, EventLabel].permutation do |classes|
46
+ nuke_db
47
+ classes.each{|c|c.all.should(be_empty, "class #{c} wasn't cleaned out") }
48
+ names = ('A'..'Z').to_a.first(classes.size)
49
+ instances = classes.collect { |clazz| clazz.new(:name => names.shift) }
50
+ a = instances.first
51
+ a.save!
52
+ a.name.should == "A"
53
+ instances[1..-1].each_with_index do |ea, idx|
54
+ instances[idx].children << ea
55
+ end
56
+ roots = classes.first.roots
57
+ i = instances.shift
58
+ roots.should =~ [i]
59
+ while (!instances.empty?) do
60
+ child = instances.shift
61
+ i.children.should =~ [child]
62
+ i = child
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,12 +1,12 @@
1
1
  class Tag < ActiveRecord::Base
2
2
  acts_as_tree :dependent => :destroy
3
- before_destroy :create_tag
3
+ before_destroy :add_destroyed_tag
4
4
 
5
5
  def to_s
6
6
  name
7
7
  end
8
8
 
9
- def create_tag
9
+ def add_destroyed_tag
10
10
  # Proof for the tests that the destroy rather than the delete method was called:
11
11
  DestroyedTag.create(:name => name)
12
12
  end
@@ -24,3 +24,19 @@ class User < ActiveRecord::Base
24
24
  email
25
25
  end
26
26
  end
27
+
28
+ class Label < ActiveRecord::Base
29
+ acts_as_tree
30
+ def to_s
31
+ "#{self.class}: #{name}"
32
+ end
33
+ end
34
+
35
+ class EventLabel < Label
36
+ end
37
+
38
+ class DateLabel < Label
39
+ end
40
+
41
+ class DirectoryLabel < Label
42
+ end
data/spec/tag_spec.rb CHANGED
@@ -2,14 +2,14 @@ require 'spec_helper'
2
2
 
3
3
  describe "empty db" do
4
4
 
5
- def nuke
5
+ def nuke_db
6
6
  Tag.delete_all
7
7
  TagHierarchy.delete_all
8
8
  DestroyedTag.delete_all
9
9
  end
10
10
 
11
11
  before :each do
12
- nuke
12
+ nuke_db
13
13
  end
14
14
 
15
15
  context "empty db" do
@@ -63,7 +63,7 @@ describe "empty db" do
63
63
  Tag.all.should be_empty
64
64
  Tag.roots.should be_empty
65
65
  Tag.leaves.should be_empty
66
- DestroyedTag.all.collect{|t|t.name}.should =~ %w{root mid leaf}
66
+ DestroyedTag.all.collect { |t| t.name }.should =~ %w{root mid leaf}
67
67
  end
68
68
  end
69
69
 
@@ -200,10 +200,10 @@ describe Tag do
200
200
  it "should cascade delete all children" do
201
201
  b2 = tags(:b2)
202
202
  entities = b2.self_and_descendants.to_a
203
- names = b2.self_and_descendants.collect{|t|t.name}
203
+ names = b2.self_and_descendants.collect { |t| t.name }
204
204
  b2.destroy
205
- entities.each{|e| Tag.find_by_id(e.id).should be_nil }
206
- DestroyedTag.all.collect{|t|t.name}.should =~ names
205
+ entities.each { |e| Tag.find_by_id(e.id).should be_nil }
206
+ DestroyedTag.all.collect { |t| t.name }.should =~ names
207
207
  end
208
208
  end
209
209
 
@@ -257,9 +257,19 @@ describe Tag do
257
257
  tags(:parent).find_by_path(%w{child larvae}).should be_nil
258
258
  end
259
259
 
260
+ it "should return nil for missing nodes" do
261
+ Tag.find_by_path(%w{missing}).should be_nil
262
+ Tag.find_by_path(%w{grandparent missing}).should be_nil
263
+ Tag.find_by_path(%w{grandparent parent missing}).should be_nil
264
+ Tag.find_by_path(%w{grandparent parent missing child}).should be_nil
265
+ end
266
+
260
267
  it "should find or create by path" do
261
268
  # class method:
262
- Tag.find_or_create_by_path(%w{grandparent parent child}).should == tags(:child)
269
+ grandparent = Tag.find_or_create_by_path(%w{grandparent})
270
+ grandparent.should == tags(:grandparent)
271
+ child = Tag.find_or_create_by_path(%w{grandparent parent child})
272
+ child.should == tags(:child)
263
273
  Tag.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
264
274
  a = Tag.find_or_create_by_path(%w{a})
265
275
  a.ancestry_path.should == %w{a}
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: 15
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
- - 2
7
+ - 3
8
8
  - 0
9
9
  - 0
10
- version: 2.0.0
10
+ version: 3.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matthew McEachen
@@ -15,10 +15,12 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-11-24 00:00:00 Z
18
+ date: 2011-11-27 00:00:00 -08:00
19
+ default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
21
- version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
22
24
  none: false
23
25
  requirements:
24
26
  - - ">="
@@ -29,10 +31,9 @@ dependencies:
29
31
  - 0
30
32
  - 0
31
33
  version: 3.0.0
32
- requirement: *id001
33
34
  type: :runtime
34
- prerelease: false
35
35
  name: activerecord
36
+ version_requirements: *id001
36
37
  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"
37
38
  email:
38
39
  - matthew-github@mceachen.org
@@ -53,10 +54,12 @@ files:
53
54
  - spec/db/database.yml
54
55
  - spec/db/schema.rb
55
56
  - spec/fixtures/tags.yml
57
+ - spec/label_spec.rb
56
58
  - spec/spec_helper.rb
57
59
  - spec/support/models.rb
58
60
  - spec/tag_spec.rb
59
61
  - spec/user_spec.rb
62
+ has_rdoc: true
60
63
  homepage: http://matthew.mceachen.us/closure_tree
61
64
  licenses: []
62
65
 
@@ -86,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
89
  requirements: []
87
90
 
88
91
  rubyforge_project:
89
- rubygems_version: 1.8.10
92
+ rubygems_version: 1.6.2
90
93
  signing_key:
91
94
  specification_version: 3
92
95
  summary: Hierarchies for ActiveRecord models using a Closure Tree storage algorithm
@@ -94,8 +97,8 @@ test_files:
94
97
  - spec/db/database.yml
95
98
  - spec/db/schema.rb
96
99
  - spec/fixtures/tags.yml
100
+ - spec/label_spec.rb
97
101
  - spec/spec_helper.rb
98
102
  - spec/support/models.rb
99
103
  - spec/tag_spec.rb
100
104
  - spec/user_spec.rb
101
- has_rdoc: