chrislloyd-eb_nested_set 0.3.2

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/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,65 @@
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
+ ## Installation
6
+
7
+ Stable:
8
+
9
+ [sudo] gem install eb_nested_set
10
+
11
+ Edge:
12
+
13
+ [sudo] gem install jnicklas-eb_nested_set --source http://gems.github.com
14
+
15
+ From source:
16
+
17
+ git clone git://github.com/jnicklas/even_better_nested_set.git
18
+ cd even_better_nested_set
19
+ rake install
20
+
21
+ If you're running Rails, just add it to your environment.rb file
22
+
23
+ config.gem 'eb_nested_set'
24
+
25
+ You can also install it as a Rails plugin.
26
+
27
+ script/plugin install git://github.com/jnicklas/even_better_nested_set.git
28
+
29
+ ## Declaring nested sets
30
+
31
+ This is how you declare a nested set:
32
+
33
+ class Directory < ActiveRecord::Base
34
+
35
+ acts_as_nested_set
36
+
37
+ end
38
+
39
+ The directories table should have the columns 'parent_id', 'left' and 'right'.
40
+
41
+ Now just set the parent to wherever you want your node to be located and EvenBetterNestedSet will do the rest for you.
42
+
43
+ d = Directory.new
44
+
45
+ d.children.create!(:name => 'blah')
46
+ d.children.create!(:name => 'gurr')
47
+ d.children.create!(:name => 'doh')
48
+
49
+ d.bounds #=> 1..8
50
+ d.children[1].bounds #=> 4..5
51
+ d.children[1].name #=> 'gurr'
52
+ d.children[1].parent #=> d
53
+
54
+ c = Directory.create!(:name => 'test', :parent => d.directory[1]
55
+
56
+ ## Finding with nested sets
57
+
58
+ 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:
59
+
60
+ d = Directory.find(42)
61
+ d.cache_nested_set
62
+
63
+ or more conveniently:
64
+
65
+ 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)