awesome_nested_set 1.4.1

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