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.
- data/.autotest +13 -0
- data/.gitignore +6 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +87 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/awesome_nested_set_jrmurad.gemspec +71 -0
- data/init.rb +12 -0
- data/lib/awesome_nested_set.rb +614 -0
- data/lib/awesome_nested_set/helper.rb +32 -0
- data/test/application.rb +1 -0
- data/test/awesome_nested_set/helper_test.rb +41 -0
- data/test/awesome_nested_set_test.rb +801 -0
- data/test/db/database.yml +18 -0
- data/test/db/schema.rb +30 -0
- data/test/fixtures/categories.yml +34 -0
- data/test/fixtures/category.rb +15 -0
- data/test/fixtures/departments.yml +3 -0
- data/test/fixtures/notes.yml +38 -0
- data/test/test_helper.rb +29 -0
- metadata +109 -0
data/.autotest
ADDED
@@ -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
|
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|