moretea-awesome_nested_set 1.4.3.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,6 @@
1
+ awesome_nested_set.sqlite3.db
2
+ test/debug.log
3
+ rdoc
4
+ coverage
5
+ pkg
6
+ *.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,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 = "moretea-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/collectiveidea/awesome_nested_set"
20
+ s.authors = ["Brandon Keepers", "Daniel Morrison"]
21
+ s.add_dependency "activerecord", ['~> 3.0.0']
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.3.1
@@ -0,0 +1,71 @@
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{moretea-awesome_nested_set}
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-8-31}
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.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
42
+ s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.5}
45
+ s.summary = %q{An awesome nested set implementation for Active Record}
46
+ s.test_files = [
47
+ "test/db/database.yml",
48
+ "test/fixtures/categories.yml",
49
+ "test/fixtures/departments.yml",
50
+ "test/fixtures/notes.yml",
51
+ "test/application.rb",
52
+ "test/awesome_nested_set/helper_test.rb",
53
+ "test/awesome_nested_set_test.rb",
54
+ "test/db/schema.rb",
55
+ "test/fixtures/category.rb",
56
+ "test/test_helper.rb"
57
+ ]
58
+
59
+ if s.respond_to? :specification_version then
60
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
+ s.specification_version = 3
62
+
63
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
64
+ s.add_runtime_dependency(%q<activerecord>, [">= 1.1"])
65
+ else
66
+ s.add_dependency(%q<activerecord>, [">= 1.1"])
67
+ end
68
+ else
69
+ s.add_dependency(%q<activerecord>, [">= 1.1"])
70
+ end
71
+ end
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, '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'
@@ -0,0 +1,601 @@
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
+ count(
122
+ :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
123
+ "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
124
+ :conditions =>
125
+ "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
126
+ "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
127
+ "#{quoted_table_name}.#{quoted_left_column_name} >= " +
128
+ "#{quoted_table_name}.#{quoted_right_column_name} OR " +
129
+ "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
130
+ "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
131
+ "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
132
+ ) == 0
133
+ end
134
+
135
+ def no_duplicates_for_columns?
136
+ scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
137
+ connection.quote_column_name(c)
138
+ end.push(nil).join(", ")
139
+ [quoted_left_column_name, quoted_right_column_name].all? do |column|
140
+ # No duplicates
141
+ first(
142
+ :select => "#{scope_string}#{column}, COUNT(#{column})",
143
+ :group => "#{scope_string}#{column}
144
+ HAVING COUNT(#{column}) > 1").nil?
145
+ end
146
+ end
147
+
148
+ # Wrapper for each_root_valid? that can deal with scope.
149
+ def all_roots_valid?
150
+ if acts_as_nested_set_options[:scope]
151
+ roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
152
+ each_root_valid?(grouped_roots)
153
+ end
154
+ else
155
+ each_root_valid?(roots)
156
+ end
157
+ end
158
+
159
+ def each_root_valid?(roots_to_validate)
160
+ left = right = 0
161
+ roots_to_validate.all? do |root|
162
+ (root.left > left && root.right > right).tap do
163
+ left = root.left
164
+ right = root.right
165
+ end
166
+ end
167
+ end
168
+
169
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
170
+ def rebuild!
171
+ # Don't rebuild a valid tree.
172
+ return true if valid?
173
+
174
+ scope = lambda{|node|}
175
+ if acts_as_nested_set_options[:scope]
176
+ scope = lambda{|node|
177
+ scope_column_names.inject(""){|str, column_name|
178
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
179
+ }
180
+ }
181
+ end
182
+ indices = {}
183
+
184
+ set_left_and_rights = lambda do |node|
185
+ # set left
186
+ node[left_column_name] = indices[scope.call(node)] += 1
187
+ # find
188
+ where("#{quoted_parent_column_name} = ? #{scope.call(node)}", node).
189
+ order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").
190
+ all.each{|n| set_left_and_rights.call(n) }
191
+ # set right
192
+ node[right_column_name] = indices[scope.call(node)] += 1
193
+ node.save!
194
+ end
195
+
196
+ # Find root node(s)
197
+ root_nodes = where(parent_column_name => nil).
198
+ order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").
199
+ all.each do |root_node|
200
+ # setup index for this scope
201
+ indices[scope.call(root_node)] ||= 0
202
+ set_left_and_rights.call(root_node)
203
+ end
204
+ end
205
+
206
+ # Iterates over tree elements and determines the current level in the tree.
207
+ # Only accepts default ordering, odering by an other column than lft
208
+ # does not work. This method is much more efficent than calling level
209
+ # because it doesn't require any additional database queries.
210
+ #
211
+ # Example:
212
+ # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
213
+ #
214
+ def each_with_level(objects)
215
+ path = [nil]
216
+ objects.each do |o|
217
+ if o.parent_id != path.last
218
+ # we are on a new level, did we decent or ascent?
219
+ if path.include?(o.parent_id)
220
+ # remove wrong wrong tailing paths elements
221
+ path.pop while path.last != o.parent_id
222
+ else
223
+ path << o.parent_id
224
+ end
225
+ end
226
+ yield(o, path.length - 1)
227
+ end
228
+ end
229
+
230
+ def before_move(*args, &block)
231
+ set_callback :move, :before, *args, &block
232
+ end
233
+
234
+ def after_move(*args, &block)
235
+ set_callback :move, :after, *args, &block
236
+ end
237
+ end
238
+
239
+ # Mixed into both classes and instances to provide easy access to the column names
240
+ module Columns
241
+ def left_column_name
242
+ acts_as_nested_set_options[:left_column]
243
+ end
244
+
245
+ def right_column_name
246
+ acts_as_nested_set_options[:right_column]
247
+ end
248
+
249
+ def parent_column_name
250
+ acts_as_nested_set_options[:parent_column]
251
+ end
252
+
253
+ def scope_column_names
254
+ Array(acts_as_nested_set_options[:scope])
255
+ end
256
+
257
+ def quoted_left_column_name
258
+ connection.quote_column_name(left_column_name)
259
+ end
260
+
261
+ def quoted_right_column_name
262
+ connection.quote_column_name(right_column_name)
263
+ end
264
+
265
+ def quoted_parent_column_name
266
+ connection.quote_column_name(parent_column_name)
267
+ end
268
+
269
+ def quoted_scope_column_names
270
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
271
+ end
272
+ end
273
+
274
+ # 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.
275
+ #
276
+ # category.self_and_descendants.count
277
+ # category.ancestors.find(:all, :conditions => "name like '%foo%'")
278
+ module InstanceMethods
279
+ # Value of the parent column
280
+ def parent_id
281
+ self[parent_column_name]
282
+ end
283
+
284
+ # Value of the left column
285
+ def left
286
+ self[left_column_name]
287
+ end
288
+
289
+ # Value of the right column
290
+ def right
291
+ self[right_column_name]
292
+ end
293
+
294
+ # Returns true if this is a root node.
295
+ def root?
296
+ parent_id.nil?
297
+ end
298
+
299
+ def leaf?
300
+ !new_record? && right - left == 1
301
+ end
302
+
303
+ # Returns true is this is a child node
304
+ def child?
305
+ !parent_id.nil?
306
+ end
307
+
308
+ # order by left column
309
+ def <=>(x)
310
+ left <=> x.left
311
+ end
312
+
313
+ # Redefine to act like active record
314
+ def ==(comparison_object)
315
+ comparison_object.equal?(self) ||
316
+ (comparison_object.instance_of?(self.class) &&
317
+ comparison_object.id == id &&
318
+ !comparison_object.new_record?)
319
+ end
320
+
321
+ # Returns root
322
+ def root
323
+ self_and_ancestors.first
324
+ end
325
+
326
+ # Returns the array of all parents and self
327
+ def self_and_ancestors
328
+ nested_set_scope.scoped.where(
329
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
330
+ )
331
+ end
332
+
333
+ # Returns an array of all parents
334
+ def ancestors
335
+ without_self self_and_ancestors
336
+ end
337
+
338
+ # Returns the array of all children of the parent, including self
339
+ def self_and_siblings
340
+ nested_set_scope.scoped.where(parent_column_name => parent_id)
341
+ end
342
+
343
+ # Returns the array of all children of the parent, except self
344
+ def siblings
345
+ without_self self_and_siblings
346
+ end
347
+
348
+ # Returns a set of all of its nested children which do not have children
349
+ def leaves
350
+ descendants.scoped.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
351
+ end
352
+
353
+ # Returns the level of this object in the tree
354
+ # root level is 0
355
+ def level
356
+ parent_id.nil? ? 0 : ancestors.count
357
+ end
358
+
359
+ # Returns a set of itself and all of its nested children
360
+ def self_and_descendants
361
+ nested_set_scope.scoped.where(
362
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
363
+ )
364
+ end
365
+
366
+ # Returns a set of all of its children and nested children
367
+ def descendants
368
+ without_self self_and_descendants
369
+ end
370
+
371
+ def is_descendant_of?(other)
372
+ other.left < self.left && self.left < other.right && same_scope?(other)
373
+ end
374
+
375
+ def is_or_is_descendant_of?(other)
376
+ other.left <= self.left && self.left < other.right && same_scope?(other)
377
+ end
378
+
379
+ def is_ancestor_of?(other)
380
+ self.left < other.left && other.left < self.right && same_scope?(other)
381
+ end
382
+
383
+ def is_or_is_ancestor_of?(other)
384
+ self.left <= other.left && other.left < self.right && same_scope?(other)
385
+ end
386
+
387
+ # Check if other model is in the same scope
388
+ def same_scope?(other)
389
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
390
+ self.send(attr) == other.send(attr)
391
+ end
392
+ end
393
+
394
+ # Find the first sibling to the left
395
+ def left_sibling
396
+ siblings.first(:conditions => ["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left],
397
+ :order => "#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC")
398
+ end
399
+
400
+ # Find the first sibling to the right
401
+ def right_sibling
402
+ siblings.first(:conditions => ["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left])
403
+ end
404
+
405
+ # Shorthand method for finding the left sibling and moving to the left of it.
406
+ def move_left
407
+ move_to_left_of left_sibling
408
+ end
409
+
410
+ # Shorthand method for finding the right sibling and moving to the right of it.
411
+ def move_right
412
+ move_to_right_of right_sibling
413
+ end
414
+
415
+ # Move the node to the left of another node (you can pass id only)
416
+ def move_to_left_of(node)
417
+ move_to node, :left
418
+ end
419
+
420
+ # Move the node to the left of another node (you can pass id only)
421
+ def move_to_right_of(node)
422
+ move_to node, :right
423
+ end
424
+
425
+ # Move the node to the child of another node (you can pass id only)
426
+ def move_to_child_of(node)
427
+ move_to node, :child
428
+ end
429
+
430
+ # Move the node to root nodes
431
+ def move_to_root
432
+ move_to nil, :root
433
+ end
434
+
435
+ def move_possible?(target)
436
+ self != target && # Can't target self
437
+ same_scope?(target) && # can't be in different scopes
438
+ # !(left..right).include?(target.left..target.right) # this needs tested more
439
+ # detect impossible move
440
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
441
+ end
442
+
443
+ def to_text
444
+ self_and_descendants.map do |node|
445
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
446
+ end.join("\n")
447
+ end
448
+
449
+ protected
450
+
451
+ def without_self(scope)
452
+ scope.where("#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self)
453
+ end
454
+
455
+ # All nested set queries should use this nested_set_scope, which performs finds on
456
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
457
+ # declaration.
458
+ def nested_set_scope
459
+ options = {:order => quoted_left_column_name}
460
+ scopes = Array(acts_as_nested_set_options[:scope])
461
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
462
+ conditions.merge attr => self[attr]
463
+ end unless scopes.empty?
464
+ self.class.base_class.scoped options
465
+ end
466
+
467
+ def store_new_parent
468
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
469
+ true # force callback to return true
470
+ end
471
+
472
+ def move_to_new_parent
473
+ if @move_to_new_parent_id.nil?
474
+ move_to_root
475
+ elsif @move_to_new_parent_id
476
+ move_to_child_of(@move_to_new_parent_id)
477
+ end
478
+ end
479
+
480
+ # on creation, set automatically lft and rgt to the end of the tree
481
+ def set_default_left_and_right
482
+ maxright = nested_set_scope.maximum(right_column_name) || 0
483
+ # adds the new node to the right of all existing nodes
484
+ self[left_column_name] = maxright + 1
485
+ self[right_column_name] = maxright + 2
486
+ end
487
+
488
+ # Prunes a branch off of the tree, shifting all of the elements on the right
489
+ # back to the left so the counts still work.
490
+ def destroy_descendants
491
+ return if right.nil? || left.nil? || skip_before_destroy
492
+
493
+ self.class.base_class.transaction do
494
+ if acts_as_nested_set_options[:dependent] == :destroy
495
+ descendants.each do |model|
496
+ model.skip_before_destroy = true
497
+ model.destroy
498
+ end
499
+ else
500
+ nested_set_scope.delete_all(
501
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
502
+ left, right]
503
+ )
504
+ end
505
+
506
+ # update lefts and rights for remaining nodes
507
+ diff = right - left + 1
508
+ nested_set_scope.update_all(
509
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
510
+ ["#{quoted_left_column_name} > ?", right]
511
+ )
512
+ nested_set_scope.update_all(
513
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
514
+ ["#{quoted_right_column_name} > ?", right]
515
+ )
516
+
517
+ # Don't allow multiple calls to destroy to corrupt the set
518
+ self.skip_before_destroy = true
519
+ end
520
+ end
521
+
522
+ # reload left, right, and parent
523
+ def reload_nested_set
524
+ reload(:select => "#{quoted_left_column_name}, " +
525
+ "#{quoted_right_column_name}, #{quoted_parent_column_name}")
526
+ end
527
+
528
+ def move_to(target, position)
529
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
530
+
531
+ res = run_callbacks :move do
532
+ transaction do
533
+ if target.is_a? self.class.base_class
534
+ target.reload_nested_set
535
+ elsif position != :root
536
+ # load object if node is not an object
537
+ target = nested_set_scope.find(target)
538
+ end
539
+ self.reload_nested_set
540
+
541
+ unless position == :root || move_possible?(target)
542
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
543
+ end
544
+
545
+ bound = case position
546
+ when :child; target[right_column_name]
547
+ when :left; target[left_column_name]
548
+ when :right; target[right_column_name] + 1
549
+ when :root; 1
550
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
551
+ end
552
+
553
+ if bound > self[right_column_name]
554
+ bound = bound - 1
555
+ other_bound = self[right_column_name] + 1
556
+ else
557
+ other_bound = self[left_column_name] - 1
558
+ end
559
+
560
+ # there would be no change
561
+ return if bound == self[right_column_name] || bound == self[left_column_name]
562
+
563
+ # we have defined the boundaries of two non-overlapping intervals,
564
+ # so sorting puts both the intervals and their boundaries in order
565
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
566
+
567
+ new_parent = case position
568
+ when :child; target.id
569
+ when :root; nil
570
+ else target[parent_column_name]
571
+ end
572
+
573
+ nested_set_scope.update_all([
574
+ "#{quoted_left_column_name} = CASE " +
575
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
576
+ "THEN #{quoted_left_column_name} + :d - :b " +
577
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
578
+ "THEN #{quoted_left_column_name} + :a - :c " +
579
+ "ELSE #{quoted_left_column_name} END, " +
580
+ "#{quoted_right_column_name} = CASE " +
581
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
582
+ "THEN #{quoted_right_column_name} + :d - :b " +
583
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
584
+ "THEN #{quoted_right_column_name} + :a - :c " +
585
+ "ELSE #{quoted_right_column_name} END, " +
586
+ "#{quoted_parent_column_name} = CASE " +
587
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
588
+ "ELSE #{quoted_parent_column_name} END",
589
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
590
+ ])
591
+ end
592
+ target.reload_nested_set if target
593
+ self.reload_nested_set
594
+ self.update_depth if depth?
595
+ end
596
+ end
597
+ end
598
+ end # Base
599
+ end # NestedSet
600
+ end # Acts
601
+ end # CollectiveIdea