chrislloyd-eb_nested_set 0.3.2

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,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)