be9-awesome_nested_set 1.4.3

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/.autotest ADDED
@@ -0,0 +1,13 @@
1
+ Autotest.add_hook :initialize do |at|
2
+ at.clear_mappings
3
+
4
+ at.add_mapping %r%^lib/(.*)\.rb$% do |_, m|
5
+ at.files_matching %r%^test/#{m[1]}_test.rb$%
6
+ end
7
+
8
+ at.add_mapping(%r%^test/.*\.rb$%) {|filename, _| filename }
9
+
10
+ at.add_mapping %r%^test/fixtures/(.*)s.yml% do |_, _|
11
+ at.files_matching %r%^test/.*\.rb$%
12
+ end
13
+ end
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gemspec
2
+ awesome_nested_set.sqlite3.db
3
+ test/debug.log
4
+ rdoc
5
+ coverage
6
+ pkg
7
+ *.sw?
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 [name of plugin creator]
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.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ = AwesomeNestedSet
2
+
3
+ Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer. It supports Rails 3.0.rc and later.
4
+
5
+ See, it's Rails 3 only.
6
+
7
+ == What makes this so awesome?
8
+
9
+ This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
10
+
11
+ == Installation
12
+
13
+ The plugin is available as a gem:
14
+
15
+ gem 'be9-awesome_nested_set'
16
+
17
+ Or install as a plugin:
18
+
19
+ rails plugin install git://github.com/be9/awesome_nested_set.git
20
+
21
+ == Usage
22
+
23
+ To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
24
+
25
+ class CreateCategories < ActiveRecord::Migration
26
+ def self.up
27
+ create_table :categories do |t|
28
+ t.string :name
29
+ t.integer :parent_id
30
+ t.integer :lft
31
+ t.integer :rgt
32
+
33
+ # Uncomment it to store item level
34
+ # t.integer :depth
35
+ end
36
+ end
37
+
38
+ def self.down
39
+ drop_table :categories
40
+ end
41
+ end
42
+
43
+ Enable the nested set functionality by declaring acts_as_nested_set on your model
44
+
45
+ class Category < ActiveRecord::Base
46
+ acts_as_nested_set
47
+ end
48
+
49
+ Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::Base::SingletonMethods for more info.
50
+
51
+ == Conversion from other trees
52
+
53
+ Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run
54
+
55
+ Category.rebuild!
56
+
57
+ Your tree be converted to a valid nested set. Awesome!
58
+
59
+ == View Helper
60
+
61
+ The view helper is called #nested_set_options.
62
+
63
+ Example usage:
64
+
65
+ <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
66
+
67
+ <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
68
+
69
+ See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
70
+
71
+ == References
72
+
73
+ You can learn more about nested sets at:
74
+
75
+ http://www.dbmsmag.com/9603d06.html
76
+ http://threebit.net/tutorials/nestedset/tutorial1.html
77
+ http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html
78
+ http://opensource.symetrie.com/trac/better_nested_set/
79
+
80
+ == How to contribute
81
+
82
+ If you find what you might think is a bug:
83
+
84
+ 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
85
+ http://github.com/collectiveidea/awesome_nested_set/issues/
86
+ 2. If you don't see anything, create an issue with information on how to reproduce it.
87
+
88
+ If you want to contribute an enhancement or a fix:
89
+
90
+ 1. Fork the project on github.
91
+ http://github.com/collectiveidea/awesome_nested_set/
92
+ 2. Make your changes with tests.
93
+ 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
94
+ 4. Send a pull request.
95
+
96
+ Copyright ©2010 Collective Idea, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ begin
4
+ require 'jeweler'
5
+ rescue LoadError
6
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
7
+ exit 1
8
+ end
9
+ require 'rake/testtask'
10
+ require 'rake/rdoctask'
11
+ require 'rcov/rcovtask'
12
+ #require "load_multi_rails_rake_tasks"
13
+
14
+ Jeweler::Tasks.new do |s|
15
+ s.name = "be9-awesome_nested_set"
16
+ s.summary = "An awesome nested set implementation for Active Record"
17
+ s.description = s.summary
18
+ s.email = "info@collectiveidea.com"
19
+ s.homepage = "http://github.com/collectiveidea/awesome_nested_set"
20
+ s.authors = ["Brandon Keepers", "Daniel Morrison"]
21
+ s.add_dependency "activerecord", ['>= 3.0.0.rc']
22
+ s.has_rdoc = true
23
+ s.extra_rdoc_files = [ "README.rdoc"]
24
+ s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
25
+ s.test_files = Dir['test/**/*.{yml,rb}']
26
+ end
27
+ Jeweler::GemcutterTasks.new
28
+
29
+ desc 'Default: run unit tests.'
30
+ task :default => :test
31
+
32
+ desc 'Test the awesome_nested_set plugin.'
33
+ Rake::TestTask.new(:test) do |t|
34
+ t.libs += ['lib', 'test']
35
+ t.pattern = 'test/**/*_test.rb'
36
+ t.verbose = true
37
+ end
38
+
39
+ desc 'Generate documentation for the awesome_nested_set plugin.'
40
+ Rake::RDocTask.new(:rdoc) do |rdoc|
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = 'AwesomeNestedSet'
43
+ rdoc.options << '--line-numbers' << '--inline-source'
44
+ rdoc.rdoc_files.include('README.rdoc')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
47
+
48
+ namespace :test do
49
+ desc "just rcov minus html output"
50
+ Rcov::RcovTask.new(:coverage) do |t|
51
+ t.libs << 'test'
52
+ t.test_files = FileList['test/**/*_test.rb']
53
+ t.output_dir = 'coverage'
54
+ t.verbose = true
55
+ t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage)
56
+ end
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.4.3
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require 'awesome_nested_set'
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module CollectiveIdea
3
+ module Acts
4
+ module NestedSet
5
+ autoload :Base, 'awesome_nested_set/base'
6
+ autoload :Depth, 'awesome_nested_set/depth'
7
+ autoload :Descendants, 'awesome_nested_set/descendants'
8
+ autoload :Helper, 'awesome_nested_set/helper'
9
+ end
10
+ end
11
+ end
12
+
13
+ require 'awesome_nested_set/railtie'
@@ -0,0 +1,604 @@
1
+ # encoding: utf-8
2
+ module CollectiveIdea #:nodoc:
3
+ module Acts #:nodoc:
4
+ module NestedSet #:nodoc:
5
+ module Base
6
+ def self.included(base)
7
+ base.extend(SingletonMethods)
8
+ end
9
+
10
+ # This acts provides Nested Set functionality. Nested Set is a smart way to implement
11
+ # an _ordered_ tree, with the added feature that you can select the children and all of their
12
+ # descendants with a single query. The drawback is that insertion or move need some complex
13
+ # sql queries. But everything is done here by this module!
14
+ #
15
+ # Nested sets are appropriate each time you want either an orderd tree (menus,
16
+ # commercial categories) or an efficient way of querying big trees (threaded posts).
17
+ #
18
+ # == API
19
+ #
20
+ # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
21
+ # by another easier.
22
+ #
23
+ # item.children.create(:name => "child1")
24
+ #
25
+ module SingletonMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
29
+ # * +:left_column+ - column name for left boundry data, default "lft"
30
+ # * +:right_column+ - column name for right boundry data, default "rgt"
31
+ # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
32
+ # (if it hasn't been already) and use that as the foreign key restriction. You
33
+ # can also pass an array to scope by multiple attributes.
34
+ # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
35
+ # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
36
+ # child objects are destroyed alongside this object by calling their destroy
37
+ # method. If set to :delete_all (default), all the child objects are deleted
38
+ # without calling their destroy method.
39
+ #
40
+ # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
41
+ # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
42
+ # to acts_as_nested_set models
43
+ def acts_as_nested_set(options = {})
44
+ options = {
45
+ :parent_column => 'parent_id',
46
+ :left_column => 'lft',
47
+ :right_column => 'rgt',
48
+ :dependent => :delete_all, # or :destroy
49
+ }.merge(options)
50
+
51
+ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
52
+ options[:scope] = "#{options[:scope]}_id".intern
53
+ end
54
+
55
+ write_inheritable_attribute :acts_as_nested_set_options, options
56
+ class_inheritable_reader :acts_as_nested_set_options
57
+
58
+ unless self.is_a?(ClassMethods)
59
+ include Comparable
60
+ include Columns
61
+ include InstanceMethods
62
+
63
+ include Depth
64
+ include Descendants
65
+
66
+ extend Columns
67
+ extend ClassMethods
68
+
69
+ belongs_to :parent, :class_name => self.base_class.to_s,
70
+ :foreign_key => parent_column_name
71
+ has_many :children, :class_name => self.base_class.to_s,
72
+ :foreign_key => parent_column_name, :order => quoted_left_column_name
73
+
74
+ attr_accessor :skip_before_destroy
75
+
76
+ # no bulk assignment
77
+ if accessible_attributes.blank?
78
+ attr_protected left_column_name.intern, right_column_name.intern
79
+ end
80
+
81
+ before_create :set_default_left_and_right
82
+ before_save :store_new_parent
83
+ after_save :move_to_new_parent
84
+ before_destroy :destroy_descendants
85
+
86
+ # no assignment to structure fields
87
+ [left_column_name, right_column_name].each do |column|
88
+ module_eval <<-"end_eval", __FILE__, __LINE__
89
+ def #{column}=(x)
90
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
91
+ end
92
+ end_eval
93
+ end
94
+
95
+ scope :roots, lambda {
96
+ where(parent_column_name => nil).order(quoted_left_column_name)
97
+ }
98
+ scope :leaves, lambda {
99
+ where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").
100
+ order(quoted_left_column_name)
101
+ }
102
+ scope :with_depth, proc {|level| where(:depth => level).order("lft") }
103
+
104
+ define_callbacks :move, :terminator => "result == false"
105
+ end
106
+ end
107
+ end
108
+
109
+ module ClassMethods
110
+
111
+ # Returns the first root
112
+ def root
113
+ roots.first
114
+ end
115
+
116
+ def valid?
117
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
118
+ end
119
+
120
+ def left_and_rights_valid?
121
+ joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
122
+ "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
123
+ where(
124
+ "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
125
+ "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
126
+ "#{quoted_table_name}.#{quoted_left_column_name} >= " +
127
+ "#{quoted_table_name}.#{quoted_right_column_name} OR " +
128
+ "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
129
+ "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
130
+ "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
131
+ ).exists? == false
132
+ end
133
+
134
+ def no_duplicates_for_columns?
135
+ scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
136
+ connection.quote_column_name(c)
137
+ end.push(nil).join(", ")
138
+ [quoted_left_column_name, quoted_right_column_name].all? do |column|
139
+ # No duplicates
140
+ first(
141
+ :select => "#{scope_string}#{column}, COUNT(#{column})",
142
+ :group => "#{scope_string}#{column}
143
+ HAVING COUNT(#{column}) > 1").nil?
144
+ end
145
+ end
146
+
147
+ # Wrapper for each_root_valid? that can deal with scope.
148
+ def all_roots_valid?
149
+ if acts_as_nested_set_options[:scope]
150
+ roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
151
+ each_root_valid?(grouped_roots)
152
+ end
153
+ else
154
+ each_root_valid?(roots)
155
+ end
156
+ end
157
+
158
+ def each_root_valid?(roots_to_validate)
159
+ left = right = 0
160
+ roots_to_validate.all? do |root|
161
+ (root.left > left && root.right > right).tap do
162
+ left = root.left
163
+ right = root.right
164
+ end
165
+ end
166
+ end
167
+
168
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
169
+ def rebuild!
170
+ # Don't rebuild a valid tree.
171
+ return true if valid?
172
+
173
+ scope = lambda{|node|}
174
+ if acts_as_nested_set_options[:scope]
175
+ scope = lambda{|node|
176
+ scope_column_names.inject(""){|str, column_name|
177
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
178
+ }
179
+ }
180
+ end
181
+ indices = {}
182
+
183
+ set_left_and_rights = lambda do |node|
184
+ # set left
185
+ node[left_column_name] = indices[scope.call(node)] += 1
186
+ # find
187
+ where("#{quoted_parent_column_name} = ? #{scope.call(node)}", node).
188
+ order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").
189
+ all.each{|n| set_left_and_rights.call(n) }
190
+ # set right
191
+ node[right_column_name] = indices[scope.call(node)] += 1
192
+ node.save!
193
+ end
194
+
195
+ # Find root node(s)
196
+ root_nodes = where(parent_column_name => nil).
197
+ order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").
198
+ all.each do |root_node|
199
+ # setup index for this scope
200
+ indices[scope.call(root_node)] ||= 0
201
+ set_left_and_rights.call(root_node)
202
+ end
203
+ end
204
+
205
+ # Iterates over tree elements and determines the current level in the tree.
206
+ # Only accepts default ordering, odering by an other column than lft
207
+ # does not work. This method is much more efficent than calling level
208
+ # because it doesn't require any additional database queries.
209
+ #
210
+ # Example:
211
+ # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
212
+ #
213
+ def each_with_level(objects)
214
+ path = [nil]
215
+ objects.each do |o|
216
+ if o.parent_id != path.last
217
+ # we are on a new level, did we decent or ascent?
218
+ if path.include?(o.parent_id)
219
+ # remove wrong wrong tailing paths elements
220
+ path.pop while path.last != o.parent_id
221
+ else
222
+ path << o.parent_id
223
+ end
224
+ end
225
+ yield(o, path.length - 1)
226
+ end
227
+ end
228
+
229
+ def before_move(*args, &block)
230
+ set_callback :move, :before, *args, &block
231
+ end
232
+
233
+ def after_move(*args, &block)
234
+ set_callback :move, :after, *args, &block
235
+ end
236
+ end
237
+
238
+ # Mixed into both classes and instances to provide easy access to the column names
239
+ module Columns
240
+ def left_column_name
241
+ acts_as_nested_set_options[:left_column]
242
+ end
243
+
244
+ def right_column_name
245
+ acts_as_nested_set_options[:right_column]
246
+ end
247
+
248
+ def parent_column_name
249
+ acts_as_nested_set_options[:parent_column]
250
+ end
251
+
252
+ def scope_column_names
253
+ Array(acts_as_nested_set_options[:scope])
254
+ end
255
+
256
+ def quoted_left_column_name
257
+ connection.quote_column_name(left_column_name)
258
+ end
259
+
260
+ def quoted_right_column_name
261
+ connection.quote_column_name(right_column_name)
262
+ end
263
+
264
+ def quoted_parent_column_name
265
+ connection.quote_column_name(parent_column_name)
266
+ end
267
+
268
+ def quoted_scope_column_names
269
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
270
+ end
271
+ end
272
+
273
+ # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
274
+ #
275
+ # category.self_and_descendants.count
276
+ # category.ancestors.find(:all, :conditions => "name like '%foo%'")
277
+ module InstanceMethods
278
+ # Value of the parent column
279
+ def parent_id
280
+ self[parent_column_name]
281
+ end
282
+
283
+ # Value of the left column
284
+ def left
285
+ self[left_column_name]
286
+ end
287
+
288
+ # Value of the right column
289
+ def right
290
+ self[right_column_name]
291
+ end
292
+
293
+ # Returns true if this is a root node.
294
+ def root?
295
+ parent_id.nil?
296
+ end
297
+
298
+ def leaf?
299
+ !new_record? && right - left == 1
300
+ end
301
+
302
+ # Returns true is this is a child node
303
+ def child?
304
+ !parent_id.nil?
305
+ end
306
+
307
+ # order by left column
308
+ def <=>(x)
309
+ left <=> x.left
310
+ end
311
+
312
+ # Redefine to act like active record
313
+ def ==(comparison_object)
314
+ comparison_object.equal?(self) ||
315
+ (comparison_object.instance_of?(self.class) &&
316
+ comparison_object.id == id &&
317
+ !comparison_object.new_record?)
318
+ end
319
+
320
+ # Returns root
321
+ def root
322
+ self_and_ancestors.first
323
+ end
324
+
325
+ # Returns the array of all parents and self
326
+ def self_and_ancestors
327
+ nested_set_scope.scoped.where("#{q_left} <= ? AND #{q_right} >= ?", left, right)
328
+ end
329
+
330
+ # Returns an array of all parents
331
+ def ancestors
332
+ without_self self_and_ancestors
333
+ end
334
+
335
+ # Returns the array of all children of the parent, including self
336
+ def self_and_siblings
337
+ nested_set_scope.scoped.where(parent_column_name => parent_id)
338
+ end
339
+
340
+ # Returns the array of all children of the parent, except self
341
+ def siblings
342
+ without_self self_and_siblings
343
+ end
344
+
345
+ # Returns a set of all of its nested children which do not have children
346
+ def leaves
347
+ descendants.scoped.where("#{q_right} - #{q_left} = 1")
348
+ end
349
+
350
+ # Returns the level of this object in the tree
351
+ # root level is 0
352
+ def level
353
+ parent_id.nil? ? 0 : ancestors.count
354
+ end
355
+
356
+ # Returns a set of itself and all of its nested children
357
+ def self_and_descendants
358
+ nested_set_scope.scoped.where("#{q_left} >= ? AND #{q_right} <= ?", left, right)
359
+ end
360
+
361
+ # Returns a set of all of its children and nested children
362
+ def descendants
363
+ without_self self_and_descendants
364
+ end
365
+
366
+ def is_descendant_of?(other)
367
+ other.left < self.left && self.left < other.right && same_scope?(other)
368
+ end
369
+
370
+ def is_or_is_descendant_of?(other)
371
+ other.left <= self.left && self.left < other.right && same_scope?(other)
372
+ end
373
+
374
+ def is_ancestor_of?(other)
375
+ self.left < other.left && other.left < self.right && same_scope?(other)
376
+ end
377
+
378
+ def is_or_is_ancestor_of?(other)
379
+ self.left <= other.left && other.left < self.right && same_scope?(other)
380
+ end
381
+
382
+ # Check if other model is in the same scope
383
+ def same_scope?(other)
384
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
385
+ self.send(attr) == other.send(attr)
386
+ end
387
+ end
388
+
389
+ # Find the first sibling to the left
390
+ def left_sibling
391
+ siblings.where("#{q_left} < ?", left).reverse_order.first
392
+ end
393
+
394
+ # Find the first sibling to the right
395
+ def right_sibling
396
+ siblings.where("#{q_left} > ?", left).first
397
+ end
398
+
399
+ # Shorthand method for finding the left sibling and moving to the left of it.
400
+ def move_left
401
+ move_to_left_of left_sibling
402
+ end
403
+
404
+ # Shorthand method for finding the right sibling and moving to the right of it.
405
+ def move_right
406
+ move_to_right_of right_sibling
407
+ end
408
+
409
+ # Move the node to the left of another node (you can pass id only)
410
+ def move_to_left_of(node)
411
+ move_to node, :left
412
+ end
413
+
414
+ # Move the node to the left of another node (you can pass id only)
415
+ def move_to_right_of(node)
416
+ move_to node, :right
417
+ end
418
+
419
+ # Move the node to the child of another node (you can pass id only)
420
+ def move_to_child_of(node)
421
+ move_to node, :child
422
+ end
423
+
424
+ # Move the node to root nodes
425
+ def move_to_root
426
+ move_to nil, :root
427
+ end
428
+
429
+ def move_possible?(target)
430
+ self != target && # Can't target self
431
+ same_scope?(target) && # can't be in different scopes
432
+ # !(left..right).include?(target.left..target.right) # this needs tested more
433
+ # detect impossible move
434
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
435
+ end
436
+
437
+ def to_text
438
+ self_and_descendants.map do |node|
439
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
440
+ end.join("\n")
441
+ end
442
+
443
+ protected
444
+
445
+ def q_left
446
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name}"
447
+ end
448
+
449
+ def q_right
450
+ "#{self.class.quoted_table_name}.#{quoted_right_column_name}"
451
+ end
452
+
453
+ def without_self(scope)
454
+ scope.where("#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self)
455
+ end
456
+
457
+ # All nested set queries should use this nested_set_scope, which performs finds on
458
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
459
+ # declaration.
460
+ def nested_set_scope
461
+ scope = self.class.base_class.order(q_left)
462
+ conditions = Array(acts_as_nested_set_options[:scope]).inject({}) do |conditions, attr|
463
+ conditions.merge attr => self[attr]
464
+ end
465
+
466
+ if conditions.empty?
467
+ scope
468
+ else
469
+ scope.where(conditions)
470
+ end
471
+ end
472
+
473
+ def store_new_parent
474
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
475
+ true # force callback to return true
476
+ end
477
+
478
+ def move_to_new_parent
479
+ if @move_to_new_parent_id.nil?
480
+ move_to_root
481
+ elsif @move_to_new_parent_id
482
+ move_to_child_of(@move_to_new_parent_id)
483
+ end
484
+ end
485
+
486
+ # on creation, set automatically lft and rgt to the end of the tree
487
+ def set_default_left_and_right
488
+ maxright = nested_set_scope.maximum(right_column_name) || 0
489
+ # adds the new node to the right of all existing nodes
490
+ self[left_column_name] = maxright + 1
491
+ self[right_column_name] = maxright + 2
492
+ end
493
+
494
+ # Prunes a branch off of the tree, shifting all of the elements on the right
495
+ # back to the left so the counts still work.
496
+ def destroy_descendants
497
+ return if right.nil? || left.nil? || skip_before_destroy
498
+
499
+ self.class.base_class.transaction do
500
+ if acts_as_nested_set_options[:dependent] == :destroy
501
+ descendants.each do |model|
502
+ model.skip_before_destroy = true
503
+ model.destroy
504
+ end
505
+ else
506
+ nested_set_scope.delete_all(["#{q_left} > ? AND #{q_right} < ?", left, right])
507
+ end
508
+
509
+ # update lefts and rights for remaining nodes
510
+ diff = right - left + 1
511
+ nested_set_scope.update_all(
512
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
513
+ ["#{quoted_left_column_name} > ?", right]
514
+ )
515
+ nested_set_scope.update_all(
516
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
517
+ ["#{quoted_right_column_name} > ?", right]
518
+ )
519
+
520
+ # Don't allow multiple calls to destroy to corrupt the set
521
+ self.skip_before_destroy = true
522
+ end
523
+ end
524
+
525
+ # reload left, right, and parent
526
+ def reload_nested_set
527
+ reload(:select => "#{quoted_left_column_name}, " +
528
+ "#{quoted_right_column_name}, #{quoted_parent_column_name}")
529
+ end
530
+
531
+ def move_to(target, position)
532
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
533
+
534
+ res = run_callbacks :move do
535
+ transaction do
536
+ if target.is_a? self.class.base_class
537
+ target.reload_nested_set
538
+ elsif position != :root
539
+ # load object if node is not an object
540
+ target = nested_set_scope.find(target)
541
+ end
542
+ self.reload_nested_set
543
+
544
+ unless position == :root || move_possible?(target)
545
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
546
+ end
547
+
548
+ bound = case position
549
+ when :child; target[right_column_name]
550
+ when :left; target[left_column_name]
551
+ when :right; target[right_column_name] + 1
552
+ when :root; 1
553
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
554
+ end
555
+
556
+ if bound > self[right_column_name]
557
+ bound = bound - 1
558
+ other_bound = self[right_column_name] + 1
559
+ else
560
+ other_bound = self[left_column_name] - 1
561
+ end
562
+
563
+ # there would be no change
564
+ return if bound == self[right_column_name] || bound == self[left_column_name]
565
+
566
+ # we have defined the boundaries of two non-overlapping intervals,
567
+ # so sorting puts both the intervals and their boundaries in order
568
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
569
+
570
+ new_parent = case position
571
+ when :child; target.id
572
+ when :root; nil
573
+ else target[parent_column_name]
574
+ end
575
+
576
+ nested_set_scope.update_all([
577
+ "#{quoted_left_column_name} = CASE " +
578
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
579
+ "THEN #{quoted_left_column_name} + :d - :b " +
580
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
581
+ "THEN #{quoted_left_column_name} + :a - :c " +
582
+ "ELSE #{quoted_left_column_name} END, " +
583
+ "#{quoted_right_column_name} = CASE " +
584
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
585
+ "THEN #{quoted_right_column_name} + :d - :b " +
586
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
587
+ "THEN #{quoted_right_column_name} + :a - :c " +
588
+ "ELSE #{quoted_right_column_name} END, " +
589
+ "#{quoted_parent_column_name} = CASE " +
590
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
591
+ "ELSE #{quoted_parent_column_name} END",
592
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
593
+ ])
594
+ end
595
+ target.reload_nested_set if target
596
+ self.reload_nested_set
597
+ self.update_depth if depth?
598
+ end
599
+ end
600
+ end
601
+ end # Base
602
+ end # NestedSet
603
+ end # Acts
604
+ end # CollectiveIdea