rd_awesome_nested_set 1.4.4

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