nested_set 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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