nested_set 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1,25 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ # PROJECT::SPECIFIC
22
+ awesome_nested_set.sqlite3.db
23
+ test/debug.log
24
+ rdoc
25
+ *.sw?
@@ -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.
@@ -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
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'rake'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "nested_set"
9
+ gem.summary = "An awesome nested set implementation for Active Record"
10
+ gem.description = gem.summary
11
+ gem.email = "info@collectiveidea.com"
12
+ gem.homepage = "http://github.com/collectiveidea/awesome_nested_set"
13
+ gem.authors = ["Brandon Keepers", "Daniel Morrison"]
14
+ gem.add_dependency "activerecord", ['>= 3.0.0.rc']
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "nested_set #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.5.0
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "rails", "init")
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module CollectiveIdea
3
+ module Acts
4
+ module NestedSet
5
+ autoload :Base, 'nested_set/base'
6
+ autoload :Depth, 'nested_set/depth'
7
+ autoload :Descendants, 'nested_set/descendants'
8
+ autoload :Helper, 'nested_set/helper'
9
+ end
10
+ end
11
+ end
12
+
13
+ require 'nested_set/railtie'
@@ -0,0 +1,599 @@
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
+ conditions = Array(acts_as_nested_set_options[:scope]).inject({}) do |conditions, attr|
462
+ conditions.merge attr => self[attr]
463
+ end
464
+
465
+ self.class.base_class.order(q_left).where(conditions)
466
+ end
467
+
468
+ def store_new_parent
469
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
470
+ true # force callback to return true
471
+ end
472
+
473
+ def move_to_new_parent
474
+ if @move_to_new_parent_id.nil?
475
+ move_to_root
476
+ elsif @move_to_new_parent_id
477
+ move_to_child_of(@move_to_new_parent_id)
478
+ end
479
+ end
480
+
481
+ # on creation, set automatically lft and rgt to the end of the tree
482
+ def set_default_left_and_right
483
+ maxright = nested_set_scope.maximum(right_column_name) || 0
484
+ # adds the new node to the right of all existing nodes
485
+ self[left_column_name] = maxright + 1
486
+ self[right_column_name] = maxright + 2
487
+ end
488
+
489
+ # Prunes a branch off of the tree, shifting all of the elements on the right
490
+ # back to the left so the counts still work.
491
+ def destroy_descendants
492
+ return if right.nil? || left.nil? || skip_before_destroy
493
+
494
+ self.class.base_class.transaction do
495
+ if acts_as_nested_set_options[:dependent] == :destroy
496
+ descendants.each do |model|
497
+ model.skip_before_destroy = true
498
+ model.destroy
499
+ end
500
+ else
501
+ nested_set_scope.delete_all(["#{q_left} > ? AND #{q_right} < ?", left, right])
502
+ end
503
+
504
+ # update lefts and rights for remaining nodes
505
+ diff = right - left + 1
506
+ nested_set_scope.update_all(
507
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
508
+ ["#{quoted_left_column_name} > ?", right]
509
+ )
510
+ nested_set_scope.update_all(
511
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
512
+ ["#{quoted_right_column_name} > ?", right]
513
+ )
514
+
515
+ # Don't allow multiple calls to destroy to corrupt the set
516
+ self.skip_before_destroy = true
517
+ end
518
+ end
519
+
520
+ # reload left, right, and parent
521
+ def reload_nested_set
522
+ reload(:select => "#{quoted_left_column_name}, " +
523
+ "#{quoted_right_column_name}, #{quoted_parent_column_name}")
524
+ end
525
+
526
+ def move_to(target, position)
527
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
528
+
529
+ res = run_callbacks :move do
530
+ transaction do
531
+ if target.is_a? self.class.base_class
532
+ target.reload_nested_set
533
+ elsif position != :root
534
+ # load object if node is not an object
535
+ target = nested_set_scope.find(target)
536
+ end
537
+ self.reload_nested_set
538
+
539
+ unless position == :root || move_possible?(target)
540
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
541
+ end
542
+
543
+ bound = case position
544
+ when :child; target[right_column_name]
545
+ when :left; target[left_column_name]
546
+ when :right; target[right_column_name] + 1
547
+ when :root; 1
548
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
549
+ end
550
+
551
+ if bound > self[right_column_name]
552
+ bound = bound - 1
553
+ other_bound = self[right_column_name] + 1
554
+ else
555
+ other_bound = self[left_column_name] - 1
556
+ end
557
+
558
+ # there would be no change
559
+ return if bound == self[right_column_name] || bound == self[left_column_name]
560
+
561
+ # we have defined the boundaries of two non-overlapping intervals,
562
+ # so sorting puts both the intervals and their boundaries in order
563
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
564
+
565
+ new_parent = case position
566
+ when :child; target.id
567
+ when :root; nil
568
+ else target[parent_column_name]
569
+ end
570
+
571
+ nested_set_scope.update_all([
572
+ "#{quoted_left_column_name} = CASE " +
573
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
574
+ "THEN #{quoted_left_column_name} + :d - :b " +
575
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
576
+ "THEN #{quoted_left_column_name} + :a - :c " +
577
+ "ELSE #{quoted_left_column_name} END, " +
578
+ "#{quoted_right_column_name} = CASE " +
579
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
580
+ "THEN #{quoted_right_column_name} + :d - :b " +
581
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
582
+ "THEN #{quoted_right_column_name} + :a - :c " +
583
+ "ELSE #{quoted_right_column_name} END, " +
584
+ "#{quoted_parent_column_name} = CASE " +
585
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
586
+ "ELSE #{quoted_parent_column_name} END",
587
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
588
+ ])
589
+ end
590
+ target.reload_nested_set if target
591
+ self.reload_nested_set
592
+ self.update_depth if depth?
593
+ end
594
+ end
595
+ end
596
+ end # Base
597
+ end # NestedSet
598
+ end # Acts
599
+ end # CollectiveIdea