closure_tree 2.0.0 → 3.0.0

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