eb_nested_set 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Jonas Nicklas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # EvenBetterNestedSet
2
+
3
+ A nested set is a datastruture in a database, sort of like a tree, but unlike a tree it allows you to find all descendants of a node with a single query. Loading a deeply nested structure with nested sets is therefore a lot more efficient than using a tree. So what's the disadvantage? Nested sets are a lot harder to maintain, since inserting and moving records requires management and it is easy to corrupt the dataset. Enter: EvenBetterNestedSet. Amount of micromanaging you need to do: 0. EvenBetterNestedSet does it all for you.
4
+
5
+ ## Declaring nested sets
6
+
7
+ This is how you declare a nested set:
8
+
9
+ class Directory < ActiveRecord::Base
10
+
11
+ acts_as_nested_set
12
+
13
+ end
14
+
15
+ The directories table should have the columns 'parent_id', 'left' and 'right'.
16
+
17
+ Now just set the parent to wherever you want your node to be located and EvenBetterNestedSet will do the rest for you.
18
+
19
+ d = Directory.new
20
+
21
+ d.children.create!(:name => 'blah')
22
+ d.children.create!(:name => 'gurr')
23
+ d.children.create!(:name => 'doh')
24
+
25
+ d.bounds #=> 1..8
26
+ d.children[1].bounds #=> 4..5
27
+ d.children[1].name #=> 'gurr'
28
+ d.children[1].parent #=> d
29
+
30
+ c = Directory.create!(:name => 'test', :parent => d.directory[1]
31
+
32
+ ## Finding with nested sets
33
+
34
+ EvenBetterNestedSet will not automatically cache children for you, because it assumes that this is not always the preferred behaviour. If you want to cache children to a nested set, just do:
35
+
36
+ d = Directory.find(42)
37
+ d.cache_nested_set
38
+
39
+ or more conveniently:
40
+
41
+ d = Directory.find_with_nested_set(42)
data/Rakefile ADDED
@@ -0,0 +1,87 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+
7
+ GEM = "eb_nested_set"
8
+ GEM_VERSION = "0.3.3"
9
+ AUTHOR = "Jonas Nicklas"
10
+ EMAIL = "jonas.nicklas@gmail.com"
11
+ HOMEPAGE = "http://github.com/jnicklas/even_better_nested_set/tree/master"
12
+ SUMMARY = "A cool acts_as_nested_set alternative"
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.name = GEM
16
+ s.version = GEM_VERSION
17
+ s.platform = Gem::Platform::RUBY
18
+ s.has_rdoc = true
19
+ s.extra_rdoc_files = ['README.md', 'LICENSE']
20
+ s.summary = SUMMARY
21
+ s.description = s.summary
22
+ s.author = AUTHOR
23
+ s.email = EMAIL
24
+ s.homepage = HOMEPAGE
25
+ s.require_path = 'lib'
26
+ s.autorequire = GEM
27
+ s.files = %w(LICENSE README.md Rakefile init.rb) + Dir.glob("{lib,spec}/**/*")
28
+ end
29
+
30
+ Rake::GemPackageTask.new(spec) do |pkg|
31
+ pkg.gem_spec = spec
32
+ end
33
+
34
+ desc "install the plugin locally"
35
+ task :install => [:package] do
36
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION} --no-update-sources}
37
+ end
38
+
39
+ desc "create a gemspec file"
40
+ task :make_spec do
41
+ File.open("#{GEM}.gemspec", "w") do |file|
42
+ file.puts spec.to_ruby
43
+ end
44
+ end
45
+
46
+ namespace :jruby do
47
+
48
+ desc "Run :package and install the resulting .gem with jruby"
49
+ task :install => :package do
50
+ sh %{#{SUDO} jruby -S gem install pkg/#{GEM}-#{GEM_VERSION}.gem --no-rdoc --no-ri}
51
+ end
52
+
53
+ end
54
+
55
+ spec_files = FileList['spec/*_spec.rb']
56
+
57
+ desc 'Default: run unit tests.'
58
+ task :default => 'spec'
59
+
60
+ task :specs => :spec
61
+ desc "Run all examples"
62
+ Spec::Rake::SpecTask.new('spec') do |t|
63
+ t.spec_opts = ['--color']
64
+ t.spec_files = spec_files
65
+ end
66
+
67
+ namespace :spec do
68
+ desc "Run all examples with RCov"
69
+ Spec::Rake::SpecTask.new('rcov') do |t|
70
+ t.spec_files = spec_files
71
+ t.rcov = true
72
+ t.rcov_dir = "doc/coverage"
73
+ t.rcov_opts = ['--exclude', 'spec,rspec-*,rcov-*,gems']
74
+ t.spec_opts = ['--color']
75
+ end
76
+
77
+ desc "Generate an html report"
78
+ Spec::Rake::SpecTask.new('report') do |t|
79
+ t.spec_files = spec_files
80
+ t.rcov = true
81
+ t.rcov_dir = "doc/coverage"
82
+ t.rcov_opts = ['--exclude', 'spec']
83
+ t.spec_opts = ['--color', "--format", "html:doc/reports/specs.html"]
84
+ t.fail_on_error = false
85
+ end
86
+
87
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'lib', 'even_better_nested_set')
@@ -0,0 +1,349 @@
1
+ module EvenBetterNestedSet
2
+
3
+ def self.included(base)
4
+ super
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ class NestedSetError < StandardError; end
9
+ class IllegalAssignmentError < NestedSetError; end
10
+
11
+ module NestedSet
12
+
13
+ def self.included(base)
14
+ super
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ attr_accessor :nested_set_options
21
+
22
+ def find_last_root
23
+ find(:first, :order => "#{nested_set_column(:right)} DESC", :conditions => { :parent_id => nil })
24
+ end
25
+
26
+ def find_boundaries(id)
27
+ query = "SELECT #{nested_set_column(:left)}, #{nested_set_column(:right)}" +
28
+ "FROM #{quote_db_property(table_name)}" +
29
+ "WHERE #{quote_db_property(primary_key)} = #{id}"
30
+ connection.select_rows(query).first
31
+ end
32
+
33
+ def nested_set
34
+ sort_nodes_to_nested_set(find(:all, :order => "#{nested_set_column(:left)} ASC"))
35
+ end
36
+
37
+ def find_with_nested_set(*args)
38
+ result = find(*args)
39
+ if result.respond_to?(:cache_nested_set)
40
+ result.cache_nested_set
41
+ elsif result.respond_to?(:each)
42
+ result.each do |node|
43
+ node.cache_nested_set
44
+ end
45
+ end
46
+ result
47
+ end
48
+
49
+ def sort_nodes_to_nested_set(nodes)
50
+ roots = []
51
+ hashmap = {}
52
+ for node in nodes.sort_by { |n| n.left }
53
+ # if the parent is not in the hashmap, parent will be nil, therefore node will be a root node
54
+ # in that case
55
+ parent = node.parent_id ? hashmap[node.parent_id] : nil
56
+
57
+ # make sure this is called at least once on every node, so leaves know that they have *no* children
58
+ node.cache_children()
59
+
60
+ if parent
61
+ node.cache_parent(parent)
62
+ parent.cache_children(node)
63
+ else
64
+ roots << node
65
+ end
66
+
67
+ hashmap[node.id] = node
68
+ end
69
+ return roots
70
+ end
71
+
72
+ def nested_set_column(name)
73
+ quote_db_property(nested_set_options[name])
74
+ end
75
+
76
+ # Recalculates the left and right values for the entire tree
77
+ def recalculate_nested_set
78
+ transaction do
79
+ left = 1
80
+ roots.each do |root|
81
+ left = root.recalculate_nested_set(left)
82
+ end
83
+ end
84
+ end
85
+
86
+ def quote_db_property(property)
87
+ "`#{property}`".gsub('.','`.`')
88
+ end
89
+
90
+ end
91
+
92
+ def root?
93
+ not parent_id?
94
+ end
95
+
96
+ def descendant_of?(node)
97
+ node.left < self.left && self.right < node.right
98
+ end
99
+
100
+ def root
101
+ transaction do
102
+ reload_boundaries
103
+ @root ||= base_class.roots.find(:first, :conditions => ["#{nested_set_column(:left)} <= ? AND #{nested_set_column(:right)} >= ?", left, right])
104
+ end
105
+ end
106
+
107
+ alias_method :patriarch, :root
108
+
109
+ def ancestors(force_reload=false)
110
+ @ancestors = nil if force_reload
111
+ @ancestors ||= base_class.find(
112
+ :all,:conditions => ["#{nested_set_column(:left)} < ? AND #{nested_set_column(:right)} > ?", left, right],
113
+ :order => "#{nested_set_column(:left)} DESC"
114
+ )
115
+ end
116
+
117
+ def lineage(force_reload=false)
118
+ [self, *ancestors(force_reload)]
119
+ end
120
+
121
+ def kin
122
+ patriarch.family
123
+ end
124
+
125
+ def descendants
126
+ base_class.descendants(self)
127
+ end
128
+
129
+ def cache_nested_set
130
+ @cached_children || base_class.sort_nodes_to_nested_set(family)
131
+ end
132
+
133
+ def family
134
+ [self, *descendants]
135
+ end
136
+
137
+ def family_ids(force_reload=false)
138
+ return @family_ids unless @family_ids.nil? or force_reload
139
+
140
+ transaction do
141
+ reload_boundaries
142
+ query = "SELECT id FROM #{self.class.quote_db_property(base_class.table_name)} " +
143
+ "WHERE #{nested_set_column(:left)} >= #{left} AND #{nested_set_column(:right)} <= #{right} " +
144
+ "ORDER BY #{nested_set_column(:left)}"
145
+ @family_ids = base_class.connection.select_values(query).map(&:to_i)
146
+ end
147
+ end
148
+
149
+ def generation
150
+ root? ? base_class.roots : parent.children
151
+ end
152
+
153
+ def siblings
154
+ generation - [self]
155
+ end
156
+
157
+ def level
158
+ if root?
159
+ 0
160
+ elsif @ancestors
161
+ @ancestors.size
162
+ else
163
+ base_class.count :conditions => ["#{nested_set_column(:left)} < ? AND #{nested_set_column(:right)} > ?", left, right]
164
+ end
165
+ end
166
+
167
+ def bounds
168
+ left..right
169
+ end
170
+
171
+ def children
172
+ @cached_children || uncached_children
173
+ end
174
+
175
+ def children?
176
+ children.empty?
177
+ end
178
+
179
+ def cache_parent(parent) #:nodoc:
180
+ self.parent = parent
181
+ end
182
+
183
+ def cache_children(*nodes) #:nodoc:
184
+ @cached_children ||= []
185
+ @cached_children.push(*nodes)
186
+ end
187
+
188
+ def left
189
+ read_attribute(self.class.nested_set_options[:left])
190
+ end
191
+
192
+ def right
193
+ read_attribute(self.class.nested_set_options[:right])
194
+ end
195
+
196
+ def recalculate_nested_set(left)
197
+ child_left = left + 1
198
+ children.each do |child|
199
+ child_left = child.recalculate_nested_set(child_left)
200
+ end
201
+ set_boundaries(left, child_left)
202
+ save_without_validation!
203
+
204
+ right + 1
205
+ end
206
+
207
+ protected
208
+
209
+ def illegal_nesting
210
+ if parent_id? and family_ids.include?(parent_id)
211
+ errors.add(:parent_id, 'cannot move node to its own descendant')
212
+ end
213
+ end
214
+
215
+ def remove_node
216
+ base_class.delete_all ["#{nested_set_column(:left)} > ? AND #{nested_set_column(:right)} < ?", left, right] # TODO: Figure out what to do with children's destroy callbacks
217
+
218
+ shift!(-node_width, right)
219
+ end
220
+
221
+ def append_node
222
+ boundary = 1
223
+
224
+ if parent_id?
225
+ transaction do
226
+ boundary = parent(true).right
227
+ shift! 2, boundary
228
+ end
229
+ elsif last_root = base_class.find_last_root
230
+ boundary = last_root.right + 1
231
+ end
232
+
233
+ set_boundaries(boundary, boundary + 1)
234
+ end
235
+
236
+ def move_node
237
+ if parent_id_changed?
238
+ transaction do
239
+ reload_boundaries
240
+
241
+ if parent_id.blank? # moved to root
242
+ shift_difference = base_class.find_last_root.right - left + 1
243
+ else # moved to non-root
244
+ new_parent = base_class.find_by_id(parent_id)
245
+
246
+ # open up a space
247
+ boundary = new_parent.right
248
+ shift! node_width, boundary
249
+
250
+ reload_boundaries
251
+
252
+ shift_difference = (new_parent.right - left)
253
+ end
254
+ # move itself and children into place
255
+ shift! shift_difference, left, right
256
+
257
+ # close up the space that was left behind after move
258
+ shift! -node_width, left
259
+
260
+ reload_boundaries
261
+ end
262
+ end
263
+ end
264
+
265
+ def shift!(positions, left_boundary, right_boundary=nil)
266
+ if right_boundary
267
+ base_class.update_all "#{nested_set_column(:left)} = (#{nested_set_column(:left)} + #{positions})", ["#{nested_set_column(:left)} >= ? AND #{nested_set_column(:left)} <= ?", left_boundary, right_boundary]
268
+ base_class.update_all "#{nested_set_column(:right)} = (#{nested_set_column(:right)} + #{positions})", ["#{nested_set_column(:right)} >= ? AND #{nested_set_column(:right)} <= ?", left_boundary, right_boundary]
269
+ else
270
+ base_class.update_all "#{nested_set_column(:left)} = (#{nested_set_column(:left)} + #{positions})", ["#{nested_set_column(:left)} >= ?", left_boundary]
271
+ base_class.update_all "#{nested_set_column(:right)} = (#{nested_set_column(:right)} + #{positions})", ["#{nested_set_column(:right)} >= ?", left_boundary]
272
+ end
273
+ end
274
+
275
+ def node_width
276
+ right - left + 1
277
+ end
278
+
279
+ def set_boundaries(left, right)
280
+ write_attribute(self.class.nested_set_options[:left], left)
281
+ write_attribute(self.class.nested_set_options[:right], right)
282
+ end
283
+
284
+ def reload_boundaries
285
+ set_boundaries(*base_class.find_boundaries(id))
286
+ end
287
+
288
+ def base_class
289
+ self.class.base_class
290
+ end
291
+
292
+ def validate_parent_is_within_scope
293
+ if self.class.nested_set_options[:scope] && parent_id
294
+ parent.reload # Make sure we are testing the record corresponding to the parent_id
295
+ if self.send(self.class.nested_set_options[:scope]) != parent.send(self.class.nested_set_options[:scope])
296
+ errors.add(:parent_id, "cannot be a record with a different #{self.class.nested_set_options[:scope]} to this record")
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ module ClassMethods
303
+
304
+ def acts_as_nested_set(options = {})
305
+ options = { :left => :left, :right => :right }.merge!(options)
306
+ options[:scope] = "#{options[:scope]}_id" if options[:scope]
307
+
308
+ include NestedSet
309
+
310
+ self.nested_set_options = options
311
+
312
+ class_eval <<-RUBY, __FILE__, __LINE__+1
313
+ def #{options[:left]}=(left)
314
+ raise EvenBetterNestedSet::IllegalAssignmentError, "#{options[:left]} is an internal attribute used by EvenBetterNestedSet, do not assign it directly as is may corrupt the data in your database"
315
+ end
316
+
317
+ def #{options[:right]}=(right)
318
+ raise EvenBetterNestedSet::IllegalAssignmentError, "#{options[:right]} is an internal attribute used by EvenBetterNestedSet, do not assign it directly as is may corrupt the data in your database"
319
+ end
320
+ RUBY
321
+
322
+ named_scope :roots, :conditions => { :parent_id => nil }, :order => "#{nested_set_column(:left)} asc"
323
+
324
+ has_many :uncached_children, :class_name => self.name, :foreign_key => :parent_id, :order => "#{nested_set_column(:left)} asc"
325
+ protected :uncached_children, :uncached_children=
326
+
327
+ belongs_to :parent, :class_name => self.name, :foreign_key => :parent_id
328
+
329
+ named_scope :descendants, lambda { |node|
330
+ left, right = find_boundaries(node.id)
331
+ { :conditions => ["#{nested_set_column(:left)} > ? and #{nested_set_column(:right)} < ?", left, right],
332
+ :order => "#{nested_set_column(:left)} asc" }
333
+ }
334
+
335
+ before_create :append_node
336
+ before_update :move_node
337
+ before_destroy :reload
338
+ after_destroy :remove_node
339
+ validate_on_update :illegal_nesting
340
+ validate :validate_parent_is_within_scope
341
+
342
+ delegate :nested_set_column, :to => "self.class"
343
+ end
344
+
345
+ end
346
+
347
+ end
348
+
349
+ ActiveRecord::Base.send(:include, EvenBetterNestedSet) if defined?(ActiveRecord)
Binary file
@@ -0,0 +1,54 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/nested_set_behavior'
3
+
4
+ class Directory < ActiveRecord::Base
5
+ acts_as_nested_set :left => :lft, :right => :rgt
6
+
7
+ validates_presence_of :name
8
+ end
9
+
10
+ describe Directory do
11
+
12
+ def invalid_attributes(options = {})
13
+ return { }.merge(options)
14
+ end
15
+
16
+ def valid_attributes(options = {})
17
+ $directory_no = $directory_no ? $directory_no + 1 : 0
18
+ return { :name => "directory#{$directory_no}" }.merge(options)
19
+ end
20
+
21
+ before do
22
+ @model = Directory
23
+ @instance = @model.new
24
+ end
25
+
26
+ it_should_behave_like "all nested set models"
27
+
28
+ it "should throw an error when attempting to assign lft directly" do
29
+ lambda {
30
+ @instance.lft = 42
31
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
32
+ @instance.lft.should_not == 42
33
+ end
34
+
35
+ it "should throw an error when attempting to assign rgt directly" do
36
+ lambda {
37
+ @instance.rgt = 42
38
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
39
+ @instance.rgt.should_not == 42
40
+ end
41
+
42
+ it "should throw an error when mass assigning to lft" do
43
+ lambda {
44
+ @model.new(valid_attributes(:lft => 1))
45
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
46
+ end
47
+
48
+ it "should throw an error when mass assigning to rgt" do
49
+ lambda {
50
+ @model.new(valid_attributes(:rgt => 1))
51
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
52
+ end
53
+
54
+ end
@@ -0,0 +1,74 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/nested_set_behavior'
3
+
4
+ class Employee < ActiveRecord::Base
5
+ acts_as_nested_set :scope => :company
6
+
7
+ validates_presence_of :name
8
+ end
9
+
10
+ describe Employee, "with nested sets for two different companies" do
11
+ before do
12
+ # Company 1...
13
+ Employee.with_options :company_id => 1 do |c1|
14
+ @c1_1 = c1.create!(:name => "Company 1 - 1")
15
+ @c1_2 = c1.create!(:name => "Company 1 - 2")
16
+
17
+ @c1_11 = c1.create!(:name => "Company 1 - 11", :parent => @c1_1)
18
+ @c1_12 = c1.create!(:name => "Company 1 - 12", :parent => @c1_1)
19
+
20
+ @c1_111 = c1.create!(:name => "Company 1 - 111", :parent => @c1_11)
21
+ end
22
+
23
+ # Company 2...
24
+ Employee.with_options :company_id => 2 do |c2|
25
+ @c2_1 = c2.create!(:name => "Company 2 - 1")
26
+ @c2_11 = c2.create!(:name => "Company 2 - 11", :parent => @c2_1)
27
+ end
28
+ end
29
+
30
+ after do
31
+ Employee.delete_all
32
+ end
33
+
34
+ it "should not allow a new employee in one company to be a child of an employee in the other company, when parent is assigned to" do
35
+ @employee = Employee.create(:company_id => 1, :parent => @c2_11)
36
+ @employee.errors[:parent_id].should_not be_nil
37
+ end
38
+
39
+ it "should not allow a new employee in one company to be a child of an employee in the other company, when parent_id is assigned to" do
40
+ @employee = Employee.create(:company_id => 1, :parent_id => @c2_11.id)
41
+ @employee.errors[:parent_id].should_not be_nil
42
+ end
43
+
44
+ it "should not allow an existing employee in one company to become a child of an employee in the other company, when parent is assigned to" do
45
+ @c1_11.parent = @c2_11
46
+ @c1_11.save
47
+ @c1_11.errors[:parent_id].should_not be_nil
48
+ end
49
+
50
+ it "should not allow an existing employee in one company to become a child of an employee in the other company, when parent_id is assigned to" do
51
+ @c1_11.parent_id = @c2_11.id
52
+ @c1_11.save
53
+ @c1_11.errors[:parent_id].should_not be_nil
54
+ end
55
+
56
+ it "should keep the tree for company 1 and for company 2 entirely disjoint" do
57
+ c1_tree = (@c1_1.family + @c1_2.family).flatten
58
+ c2_tree = @c2_1.family
59
+
60
+ (c1_tree & c2_tree).should be_empty
61
+ end
62
+
63
+ it "should return the correct descendants when retrieving via a database query" do
64
+ @c1_1.descendants.should == [@c1_11, @c1_111, @c1_12]
65
+ @c1_2.descendants.should == []
66
+ @c2_1.descendants.should == [@c2_11]
67
+ end
68
+
69
+ it "should return the correct levels when retrieving via a database query" do
70
+ @c1_1.family.map { |d| d.level }.should == [0, 1, 2, 1]
71
+ @c1_2.family.map { |d| d.level }.should == [0]
72
+ @c2_1.family.map { |d| d.level }.should == [0, 1]
73
+ end
74
+ end
@@ -0,0 +1,586 @@
1
+ describe "all nested set models", :shared => true do
2
+
3
+ describe @model, 'model with acts_as_nested_set' do
4
+
5
+ before do
6
+ @instance = @model.new(valid_attributes)
7
+ end
8
+
9
+ it "should change the parent_id in the database when a parent is assigned" do
10
+ without_changing_the_database do
11
+ @parent = @model.create!(valid_attributes)
12
+
13
+ @instance.parent = @parent
14
+ @instance.save!
15
+ @instance = @model.find(@instance.id)
16
+
17
+ @instance.parent_id.should == @parent.id
18
+ end
19
+ end
20
+
21
+ it "should change the parent_id in the database when a parent_id is assigned" do
22
+ without_changing_the_database do
23
+ @parent = @model.create!(valid_attributes)
24
+
25
+ @instance.parent_id = @parent.id
26
+ @instance.save!
27
+ @instance = @model.find(@instance.id)
28
+
29
+ @instance.parent_id.should == @parent.id
30
+ end
31
+ end
32
+
33
+ describe '#bounds' do
34
+
35
+ it "should return a range, from left to right" do
36
+ without_changing_the_database do
37
+ @instance.save!
38
+ @instance.left.should == 1
39
+ @instance.right.should == 2
40
+ @instance.bounds.should == (1..2)
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+ describe @model, "with many descendants" do
49
+ before do
50
+ @r1 = @model.create!(valid_attributes)
51
+ @r2 = @model.create!(valid_attributes)
52
+ @r3 = @model.create!(valid_attributes)
53
+
54
+ @r1c1 = @model.create!(valid_attributes(:parent => @r1))
55
+ @r1c2 = @model.create!(valid_attributes(:parent => @r1))
56
+ @r1c3 = @model.create!(valid_attributes(:parent => @r1))
57
+ @r2c1 = @model.create!(valid_attributes(:parent => @r2))
58
+
59
+ @r1c1s1 = @model.create!(valid_attributes(:parent => @r1c1))
60
+ @r1c2s1 = @model.create!(valid_attributes(:parent => @r1c2))
61
+ @r1c2s2 = @model.create!(valid_attributes(:parent => @r1c2))
62
+ @r1c2s3 = @model.create!(valid_attributes(:parent => @r1c2))
63
+
64
+ @r1c2s2m1 = @model.create!(valid_attributes(:parent => @r1c2s2))
65
+ end
66
+
67
+ after do
68
+ @model.delete_all
69
+ end
70
+
71
+ it "should find all root nodes" do
72
+ @model.roots.all.should == [@r1, @r2, @r3]
73
+ end
74
+
75
+ it "should find a root nodes" do
76
+ @model.roots.first.should == @r1
77
+ end
78
+
79
+ it "should maintain the integrity of the tree if a node is deleted" do
80
+ @r1c2.destroy
81
+
82
+ @r1.reload
83
+ @r1c3.reload
84
+
85
+ @r1.bounds.should == (1..8)
86
+ @r1c3.bounds.should == (6..7)
87
+ end
88
+
89
+ it "should maintain the integrity of the tree if a node is moved" do
90
+ @r1c2.parent_id = @r2.id
91
+ @r1c2.save!
92
+
93
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
94
+
95
+ @r1.bounds.should == (1..8)
96
+ @r1c3.bounds.should == (6..7)
97
+ @r2.bounds.should == (9..22)
98
+ @r1c2.bounds.should == (12..21)
99
+ @r1c2s1.bounds.should == (13..14)
100
+ end
101
+
102
+ it "should change the parent, left and right in the database when a node is moved" do
103
+
104
+ @r1c2.parent_id = @r2.id
105
+
106
+ @r1c2.save!
107
+ @r1c2 = @model.find(@r1c2.id)
108
+
109
+ @r1c2.bounds.should == (12..21)
110
+ @r1c2.parent_id.should == @r2.id
111
+ end
112
+
113
+ it "should maintain the integrity of the tree if a node is moved to a root position" do
114
+ @r1c2.parent_id = nil
115
+ @r1c2.save!
116
+
117
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
118
+
119
+ @r1.bounds.should == (1..8)
120
+ @r1c3.bounds.should == (6..7)
121
+ @r1c2.bounds.should == (15..24)
122
+ @r1c2s1.bounds.should == (16..17)
123
+ end
124
+
125
+ it "should maintain the integrity of the tree if a node is moved to a root position by assigning a blank string (mass assignment)" do
126
+ @r1c2.parent_id = ""
127
+ @r1c2.save!
128
+
129
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
130
+
131
+ @r1.bounds.should == (1..8)
132
+ @r1c3.bounds.should == (6..7)
133
+ @r1c2.bounds.should == (15..24)
134
+ @r1c2s1.bounds.should == (16..17)
135
+ end
136
+
137
+ it "should maintain the integrity of the tree if a root is to a non-root position" do
138
+ @r1c2.reload
139
+ @r2.parent_id = @r1c2.id
140
+ @r2.save!
141
+
142
+ reload_models(@r1, @r2, @r2c1, @r1c3, @r3, @r1c2)
143
+
144
+ @r1.bounds.should == (1..22)
145
+ @r1c2.bounds.should == (6..19)
146
+ @r1c3.bounds.should == (20..21)
147
+ @r3.bounds.should == (23..24)
148
+ @r2.bounds.should == (15..18)
149
+ @r2c1.bounds.should == (16..17)
150
+ end
151
+
152
+ it "should maintain the integrity of the tree if a node is moved through the parent association" do
153
+ @r1c2.parent = @r2
154
+ @r1c2.save!
155
+
156
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
157
+
158
+ @r1.bounds.should == (1..8)
159
+ @r1c3.bounds.should == (6..7)
160
+ @r2.bounds.should == (9..22)
161
+ @r1c2.bounds.should == (12..21)
162
+ @r1c2s1.bounds.should == (13..14)
163
+ end
164
+
165
+ it "should maintain the integrity of the tree if a node is moved to a root position through the parent association" do
166
+ @r1c2.parent = nil
167
+ @r1c2.save!
168
+
169
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
170
+
171
+ @r1.bounds.should == (1..8)
172
+ @r1c3.bounds.should == (6..7)
173
+ @r1c2.bounds.should == (15..24)
174
+ @r1c2s1.bounds.should == (16..17)
175
+ end
176
+
177
+ it "should maintain the integrity of the tree if a root is to a non-root position through the parent association" do
178
+ @r1c2.reload
179
+ @r2.parent = @r1c2
180
+ @r2.save!
181
+
182
+ reload_models(@r1, @r2, @r2c1, @r1c3, @r3, @r1c2)
183
+
184
+ @r1.bounds.should == (1..22)
185
+ @r1c2.bounds.should == (6..19)
186
+ @r1c3.bounds.should == (20..21)
187
+ @r3.bounds.should == (23..24)
188
+ @r2.bounds.should == (15..18)
189
+ @r2c1.bounds.should == (16..17)
190
+ end
191
+
192
+ it "should be invalid if parent is a descendant" do
193
+ @r2.parent = @r2c1
194
+ @r2.should_not be_valid
195
+ end
196
+
197
+ it "should be invalid if parent is self" do
198
+ @r2.parent = @r2
199
+ @r2.should_not be_valid
200
+ end
201
+
202
+ describe ".nested_set" do
203
+ it "should find all nodes as a nested set and cache that data" do
204
+ roots = @model.nested_set
205
+
206
+ @model.delete_all
207
+
208
+ roots[0].should == @r1
209
+ roots[0].children[0].should == @r1c1
210
+ roots[0].children[0].children[0].should == @r1c1s1
211
+ roots[0].children[1].should == @r1c2
212
+ roots[0].children[1].children[0].should == @r1c2s1
213
+ roots[0].children[1].children[1].should == @r1c2s2
214
+ roots[0].children[1].children[1].children[0].should == @r1c2s2m1
215
+ roots[0].children[1].children[2].should == @r1c2s3
216
+ roots[0].children[2].should == @r1c3
217
+ roots[1].should == @r2
218
+ roots[1].children[0].should == @r2c1
219
+ roots[2].should == @r3
220
+
221
+ roots[1].children[0].parent.should == @r2
222
+ end
223
+ end
224
+
225
+ describe ".find_with_nested_set" do
226
+ it "should find a single node and cache it's descendants" do
227
+ node = @model.find_with_nested_set(@r1c2.id)
228
+
229
+ @model.delete_all
230
+
231
+ node.should == @r1c2
232
+ node.children[0].should == @r1c2s1
233
+ node.children[1].should == @r1c2s2
234
+ node.children[1].children[0].should == @r1c2s2m1
235
+ node.children[2].should == @r1c2s3
236
+ end
237
+
238
+ it "should allow find with conditions" do
239
+ node = @model.find_with_nested_set(:first, :conditions => { :id => @r1c2.id })
240
+
241
+ @model.delete_all
242
+
243
+ node.should == @r1c2
244
+ node.children[0].should == @r1c2s1
245
+ node.children[1].should == @r1c2s2
246
+ node.children[1].children[0].should == @r1c2s2m1
247
+ node.children[2].should == @r1c2s3
248
+ end
249
+
250
+ it "should allow find all with conditions" do
251
+ nodes = @model.find_with_nested_set(:all, :conditions => { :parent_id => @r1.id })
252
+
253
+ @model.delete_all
254
+
255
+ nodes[0].should == @r1c1
256
+ nodes[0].children[0].should == @r1c1s1
257
+ nodes[1].should == @r1c2
258
+ nodes[1].children[0].should == @r1c2s1
259
+ nodes[1].children[1].should == @r1c2s2
260
+ nodes[1].children[1].children[0].should == @r1c2s2m1
261
+ nodes[1].children[2].should == @r1c2s3
262
+ nodes[2].should == @r1c3
263
+ end
264
+ end
265
+
266
+ describe ".sort_nodes_to_nested_set" do
267
+ it "should accept a list of nodes and sort them to a nested set" do
268
+ roots = @model.sort_nodes_to_nested_set(@model.find(:all))
269
+ roots[0].should == @r1
270
+ roots[0].children[0].should == @r1c1
271
+ roots[0].children[0].children[0].should == @r1c1s1
272
+ roots[0].children[1].should == @r1c2
273
+ roots[0].children[1].children[0].should == @r1c2s1
274
+ roots[0].children[1].children[1].should == @r1c2s2
275
+ roots[0].children[1].children[1].children[0].should == @r1c2s2m1
276
+ roots[0].children[1].children[2].should == @r1c2s3
277
+ roots[0].children[2].should == @r1c3
278
+ roots[1].should == @r2
279
+ roots[1].children[0].should == @r2c1
280
+ roots[2].should == @r3
281
+ end
282
+ end
283
+
284
+ describe ".recalculate_nested_set" do
285
+ def values
286
+ @model.find(:all, :order => :id).map { |node| [node.left, node.right] }
287
+ end
288
+
289
+ before do
290
+ @model.find(:all, :order => :id).each do |i|
291
+ i.send(:set_boundaries, rand(1000), rand(1000))
292
+ i.save_without_validation!
293
+ end
294
+ end
295
+
296
+ it "should correctly restore the left and right values for a messed up nested set" do
297
+ @model.recalculate_nested_set
298
+ [@r1, @r2, @r3].each(&:reload)
299
+
300
+ expected = [
301
+ [@r1c1, @r1c2, @r1c3, @r1c1s1, @r1c2s1, @r1c2s2, @r1c2s3, @r1c2s2m1],
302
+ [@r2c1],
303
+ [],
304
+ [@r1c1s1],
305
+ [@r1c2s1, @r1c2s2, @r1c2s3, @r1c2s2m1],
306
+ [],
307
+ [],
308
+ [],
309
+ [],
310
+ [@r1c2s2m1],
311
+ [],
312
+ []
313
+ ]
314
+
315
+ [@r1, @r2, @r3, @r1c1, @r1c2, @r1c3, @r2c1, @r1c1s1, @r1c2s1, @r1c2s2, @r1c2s3, @r1c2s2m1].each_with_index do |node, i|
316
+ node.descendants.find(:all, :order => :id).should == expected[i]
317
+ end
318
+ end
319
+
320
+ it "should leave all records valid after running" do
321
+ @model.recalculate_nested_set
322
+ @model.find(:all).each do |node|
323
+ node.should be_valid
324
+ end
325
+ end
326
+ end
327
+
328
+ describe "#cache_nested_set" do
329
+ it "should cache all descendant nodes so that calls to #children or #parent don't hit the database" do
330
+ @r1c2.cache_nested_set
331
+
332
+ @model.delete_all
333
+
334
+ @r1c2.children[0].should == @r1c2s1
335
+ @r1c2.children[1].should == @r1c2s2
336
+ @r1c2.children[1].children[0].should == @r1c2s2m1
337
+ @r1c2.children[2].should == @r1c2s3
338
+
339
+ @r1c2.children[1].children[0].parent.should == @r1c2s2
340
+ end
341
+ end
342
+
343
+ describe "#parent" do
344
+ it "should find the parent node" do
345
+ @r1c1.parent.should == @r1
346
+ @r1c2s2.parent.should == @r1c2
347
+ @r1c2s2m1.parent.should == @r1c2s2
348
+ end
349
+ end
350
+
351
+ describe "#children" do
352
+ it "should find all nodes that are direct descendants of this one" do
353
+ @r1.children.should == [@r1c1, @r1c2, @r1c3]
354
+ @r1c2s2.children.should == [@r1c2s2m1]
355
+ end
356
+
357
+ it "should allow creation of children" do
358
+ child = @r1c2.children.create!(valid_attributes)
359
+
360
+ child.parent_id.should == @r1c2.id
361
+ child.bounds.should == (15..16)
362
+ end
363
+
364
+ it "should allow addition of children" do
365
+ @r2.children << @r1c2
366
+
367
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
368
+
369
+ @r1.bounds.should == (1..8)
370
+ @r1c3.bounds.should == (6..7)
371
+ @r2.bounds.should == (9..22)
372
+ @r1c2.bounds.should == (12..21)
373
+ @r1c2s1.bounds.should == (13..14)
374
+ end
375
+ end
376
+
377
+ describe "#patriarch" do
378
+ it "should find the root node that this node descended from" do
379
+ @r1c1.patriarch.should == @r1
380
+ @r1c2s2.patriarch.should == @r1
381
+ @r1c2s2m1.patriarch.should == @r1
382
+ @r2c1.patriarch.should == @r2
383
+ @r1.patriarch.should == @r1
384
+ end
385
+ end
386
+
387
+ describe "#root" do
388
+ it "should find the root node that this node descended from" do
389
+ @r1c1.root.should == @r1
390
+ @r1c2s2.root.should == @r1
391
+ @r1c2s2m1.root.should == @r1
392
+ @r2c1.root.should == @r2
393
+ @r1.root.should == @r1
394
+ end
395
+ end
396
+
397
+ describe "#root?" do
398
+ it "should be true if node doesn't have a parent" do
399
+ @r1.should be_root
400
+ @model.roots.should include(@r1)
401
+ @r1.parent.should be_nil
402
+ end
403
+ end
404
+
405
+ describe "#descendant_of(other_node)" do
406
+ it "should be true if other_node is an ancestor of node" do
407
+ reload_models @r1, @r1c2s2
408
+
409
+ @r1c2s2.should be_descendant_of(@r1)
410
+ @r1c2s2.ancestors.should include(@r1)
411
+ @r1.descendants.should include(@r1c2s2)
412
+ end
413
+ end
414
+
415
+ describe "#generation" do
416
+ it "should find all nodes in the same generation as this one for a root node" do
417
+ @r1.generation.should == [@r1, @r2, @r3]
418
+ end
419
+
420
+ it "should find all nodes in the same generation as this one" do
421
+ @r1c1.generation.should == [@r1c1, @r1c2, @r1c3]
422
+ end
423
+ end
424
+
425
+ describe "#siblings" do
426
+ it "should find all sibling nodes for a root node" do
427
+ @r1.siblings.should == [@r2, @r3]
428
+ end
429
+
430
+ it "should find all sibling nodes for a child node" do
431
+ @r1c1.siblings.should == [@r1c2, @r1c3]
432
+ end
433
+ end
434
+
435
+ describe "#descendants" do
436
+ it "should find all descendants of this node" do
437
+ @r1.descendants.should == [@r1c1, @r1c1s1, @r1c2, @r1c2s1, @r1c2s2, @r1c2s2m1, @r1c2s3, @r1c3]
438
+ end
439
+ end
440
+
441
+ describe "#family" do
442
+ it "should combine self and descendants" do
443
+ @r1.family.should == [@r1, @r1c1, @r1c1s1, @r1c2, @r1c2s1, @r1c2s2, @r1c2s2m1, @r1c2s3, @r1c3]
444
+ end
445
+ end
446
+
447
+ describe "#family_ids" do
448
+ it "should find all ids of the node's nested set" do
449
+ @r1c1.family_ids.should == [@r1c1.id, @r1c1s1.id]
450
+ @r1c2.family_ids.should == [@r1c2.id, @r1c2s1.id, @r1c2s2.id, @r1c2s2m1.id, @r1c2s3.id]
451
+ end
452
+ end
453
+
454
+ describe "#ancestors" do
455
+ it "should return a node's parent and its parent's parents" do
456
+ @r1c2s2m1.ancestors.should == [@r1c2s2, @r1c2, @r1]
457
+ end
458
+ end
459
+
460
+ describe "#lineage" do
461
+ it "should return a node, it's parent and its parent's parents" do
462
+ @r1c2s2m1.lineage.should == [@r1c2s2m1, @r1c2s2, @r1c2, @r1]
463
+ end
464
+ end
465
+
466
+ describe "#level" do
467
+ it "should give the depth from the node to its root" do
468
+ @r1.level.should == 0
469
+ @r1c2.level.should == 1
470
+ @r1c2s2.level.should == 2
471
+ @r1c2s2m1.level.should == 3
472
+ end
473
+ end
474
+
475
+ describe "#kin" do
476
+ it "should find the patriarch and all its descendants" do
477
+ @r1c2s2.kin.should == [@r1, @r1c1, @r1c1s1, @r1c2, @r1c2s1, @r1c2s2, @r1c2s2m1, @r1c2s3, @r1c3]
478
+ end
479
+ end
480
+
481
+ end
482
+
483
+ describe @model, "with acts_as_nested_set" do
484
+
485
+ it "should add a new root node if the parent is not set" do
486
+ without_changing_the_database do
487
+ @instance = @model.create!(valid_attributes)
488
+ @instance.parent_id.should be_nil
489
+
490
+ @instance.bounds.should == (1..2)
491
+ end
492
+ end
493
+
494
+ it "should add a new root node if the parent is not set and there already are some root nodes" do
495
+ without_changing_the_database do
496
+ @model.create!(valid_attributes)
497
+ @model.create!(valid_attributes)
498
+ @instance = @model.create!(valid_attributes)
499
+ @instance.reload
500
+
501
+ @instance.parent_id.should be_nil
502
+ @instance.bounds.should == (5..6)
503
+ end
504
+ end
505
+
506
+ it "should append a child node to a parent" do
507
+ without_changing_the_database do
508
+ @parent = @model.create!(valid_attributes)
509
+ @parent.bounds.should == (1..2)
510
+
511
+ @instance = @model.create!(valid_attributes(:parent => @parent))
512
+
513
+ @parent.reload
514
+
515
+ @instance.parent.should == @parent
516
+
517
+ @instance.bounds.should == (2..3)
518
+ @parent.bounds.should == (1..4)
519
+ end
520
+ end
521
+
522
+ it "should rollback changes if the save is not successfull for some reason" do
523
+ without_changing_the_database do
524
+ @parent = @model.create!(valid_attributes)
525
+ @parent.bounds.should == (1..2)
526
+
527
+ @instance = @model.create(invalid_attributes(:parent => @parent))
528
+ @instance.should be_a_new_record
529
+
530
+ @parent.reload
531
+
532
+ @parent.bounds.should == (1..2)
533
+ end
534
+ end
535
+
536
+ it "should append a child node to a parent and shift other nodes out of the way" do
537
+ without_changing_the_database do
538
+ @root1 = @model.create!(valid_attributes)
539
+ @root2 = @model.create!(valid_attributes)
540
+
541
+ @root1.bounds.should == (1..2)
542
+ @root2.bounds.should == (3..4)
543
+
544
+ @child1 = @model.create!(valid_attributes(:parent => @root1))
545
+ reload_models(@root1, @root2)
546
+
547
+ @root1.bounds.should == (1..4)
548
+ @root2.bounds.should == (5..6)
549
+ @child1.bounds.should == (2..3)
550
+
551
+ @child2 = @model.create!(valid_attributes(:parent => @root1))
552
+ reload_models(@root1, @root2, @child1)
553
+
554
+ @root1.bounds.should == (1..6)
555
+ @root2.bounds.should == (7..8)
556
+ @child1.bounds.should == (2..3)
557
+ @child2.bounds.should == (4..5)
558
+
559
+ @subchild1 = @model.create!(valid_attributes(:parent => @child2))
560
+ reload_models(@root1, @root2, @child1, @child2)
561
+
562
+ @root1.bounds.should == (1..8)
563
+ @root2.bounds.should == (9..10)
564
+ @child1.bounds.should == (2..3)
565
+ @child2.bounds.should == (4..7)
566
+ @subchild1.bounds.should == (5..6)
567
+
568
+ @subchild2 = @model.create!(valid_attributes(:parent => @child1))
569
+ reload_models(@root1, @root2, @child1, @child2, @subchild1)
570
+
571
+ @root1.bounds.should == (1..10)
572
+ @root2.bounds.should == (11..12)
573
+ @child1.bounds.should == (2..5)
574
+ @child2.bounds.should == (6..9)
575
+ @subchild1.bounds.should == (7..8)
576
+ @subchild2.bounds.should == (3..4)
577
+ end
578
+ end
579
+
580
+ end
581
+
582
+ def reload_models(*attrs)
583
+ attrs.each {|m| m.reload }
584
+ end
585
+
586
+ end
@@ -0,0 +1,83 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ # require 'ruby-debug' # Slows down the tests massively
7
+
8
+ require 'eb_nested_set'
9
+
10
+ require 'spec'
11
+
12
+ # change this if sqlite is unavailable
13
+ dbconfig = case ENV["DB"]
14
+ when "postgresql"
15
+ {
16
+ :adapter => 'postgresql',
17
+ :database => 'even_better_nested_set_test',
18
+ :host => '127.0.0.1'
19
+ }
20
+ when "mysql"
21
+ {
22
+ :adapter => 'mysql',
23
+ :database => 'even_better_nested_set_test',
24
+ :host => '127.0.0.1'
25
+ }
26
+ else
27
+ {
28
+ :adapter => 'sqlite3',
29
+ :database => File.join(File.dirname(__FILE__), 'db', 'test.sqlite3')
30
+ }
31
+ end
32
+
33
+ ActiveRecord::Base.establish_connection(dbconfig)
34
+ ActiveRecord::Migration.verbose = false
35
+
36
+ def show_model_variables_for(context, model)
37
+ context.instance_variables.sort.each do |i|
38
+ m = eval(i)
39
+ if m.is_a?(model)
40
+ m.reload
41
+ puts "#{i.ljust(8)}\t#{m.left}\t#{m.right}\t#{m.name}"
42
+ end
43
+ end
44
+ end
45
+
46
+ #ActiveRecord::Base.logger = Logger.new(STDOUT)
47
+
48
+
49
+ class TestMigration < ActiveRecord::Migration
50
+ def self.up
51
+ create_table :directories, :force => true do |t|
52
+ t.column :lft, :integer
53
+ t.column :rgt, :integer
54
+ t.column :parent_id, :integer
55
+ t.column :name, :string
56
+ end
57
+
58
+ create_table :employees, :force => true do |t|
59
+ t.column :left, :integer
60
+ t.column :right, :integer
61
+ t.column :parent_id, :integer
62
+ t.column :name, :string
63
+ t.column :company_id, :integer
64
+ end
65
+ end
66
+
67
+ def self.down
68
+ drop_table :directories
69
+ drop_table :employees
70
+ rescue
71
+ nil
72
+ end
73
+ end
74
+
75
+ def without_changing_the_database
76
+ ActiveRecord::Base.transaction do
77
+ yield
78
+ raise ActiveRecord::Rollback
79
+ end
80
+ end
81
+
82
+ TestMigration.down
83
+ TestMigration.up
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eb_nested_set
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.3
5
+ platform: ruby
6
+ authors:
7
+ - Jonas Nicklas
8
+ autorequire: eb_nested_set
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-15 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A cool acts_as_nested_set alternative
17
+ email: jonas.nicklas@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.md
24
+ - LICENSE
25
+ files:
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - init.rb
30
+ - lib/eb_nested_set.rb
31
+ - spec/db
32
+ - spec/db/test.sqlite3
33
+ - spec/directory_spec.rb
34
+ - spec/employee_spec.rb
35
+ - spec/nested_set_behavior.rb
36
+ - spec/spec_helper.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/jnicklas/even_better_nested_set/tree/master
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.3.1
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: A cool acts_as_nested_set alternative
63
+ test_files: []
64
+