awesome_nested_set_jrmurad 1.4.3

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