nbrew-better_nested_set 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +224 -0
- data/Rakefile.rb +28 -0
- data/lib/better_nested_set.rb +1129 -0
- data/lib/better_nested_set_helper.rb +127 -0
- data/rails/init.rb +14 -0
- data/test/RUNNING_UNIT_TESTS +1 -0
- data/test/abstract_unit.rb +25 -0
- data/test/acts_as_nested_set_test.rb +1368 -0
- data/test/database.yml +15 -0
- data/test/fixtures/mixin.rb +33 -0
- data/test/fixtures/mixins.yml +66 -0
- data/test/mysql.rb +2 -0
- data/test/postgresql.rb +2 -0
- data/test/schema.rb +12 -0
- data/test/sqlite3.rb +2 -0
- metadata +90 -0
data/README
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
= Better nested set
|
2
|
+
|
3
|
+
This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the
|
4
|
+
object-relational mapping layer of the framework Ruby on Rails. The original
|
5
|
+
nested set in Rails lacks many important features, such as moving branches within a tree.
|
6
|
+
|
7
|
+
= Installation
|
8
|
+
|
9
|
+
script/plugin install svn://rubyforge.org/var/svn/betternestedset/trunk
|
10
|
+
|
11
|
+
== Details
|
12
|
+
|
13
|
+
A nested set is a smart way to implement an _ordered_ tree that allows for
|
14
|
+
fast, non-recursive queries. For example, you can fetch all descendants of
|
15
|
+
a node in a single query, no matter how deep the tree. The drawback is that
|
16
|
+
insertions/moves/deletes require complex SQL, but that is handled behind
|
17
|
+
the curtains by this plugin!
|
18
|
+
|
19
|
+
Nested sets are appropriate for ordered trees
|
20
|
+
(e.g. menus, commercial categories) and big trees that must be queried
|
21
|
+
efficiently (e.g. threaded posts).
|
22
|
+
|
23
|
+
See http://www.dbmsmag.com/9603d06.html for nested sets theory, and a tutorial here:
|
24
|
+
http://threebit.net/tutorials/nestedset/tutorial1.html
|
25
|
+
|
26
|
+
== Small nested set theory reminder
|
27
|
+
|
28
|
+
An easy way to visualize how a nested set works is to think of a parent entity surrounding all
|
29
|
+
of its children, and its parent surrounding it, etc. So this tree:
|
30
|
+
root
|
31
|
+
|_ Child 1
|
32
|
+
|_ Child 1.1
|
33
|
+
|_ Child 1.2
|
34
|
+
|_ Child 2
|
35
|
+
|_ Child 2.1
|
36
|
+
|_ Child 2.2
|
37
|
+
|
38
|
+
Could be visualized like this:
|
39
|
+
___________________________________________________________________
|
40
|
+
| Root |
|
41
|
+
| ____________________________ ____________________________ |
|
42
|
+
| | Child 1 | | Child 2 | |
|
43
|
+
| | __________ _________ | | __________ _________ | |
|
44
|
+
| | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
|
45
|
+
1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
|
46
|
+
| |___________________________| |___________________________| |
|
47
|
+
|___________________________________________________________________|
|
48
|
+
|
49
|
+
The numbers represent the left and right boundaries. The table then might
|
50
|
+
look like this:
|
51
|
+
id | parent_id | lft | rgt | data
|
52
|
+
1 | | 1 | 14 | root
|
53
|
+
2 | 1 | 2 | 7 | Child 1
|
54
|
+
3 | 2 | 3 | 4 | Child 1.1
|
55
|
+
4 | 2 | 5 | 6 | Child 1.2
|
56
|
+
5 | 1 | 8 | 13 | Child 2
|
57
|
+
6 | 5 | 9 | 10 | Child 2.1
|
58
|
+
7 | 5 | 11 | 12 | Child 2.2
|
59
|
+
|
60
|
+
To get all children of an entry +parent+, you
|
61
|
+
SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt
|
62
|
+
|
63
|
+
To get the number of children, it's
|
64
|
+
(right - left - 1)/2
|
65
|
+
|
66
|
+
To get a node and all its ancestors going back to the root, you
|
67
|
+
SELECT * WHERE node.lft IS BETWEEN lft AND rgt
|
68
|
+
|
69
|
+
As you can see, queries that would be recursive and prohibitively slow on ordinary trees are suddenly quite fast. Nifty, isn't it? There are instance methods for each of the above, plus many others.
|
70
|
+
|
71
|
+
|
72
|
+
= API
|
73
|
+
Method names are mostly the same as in acts_as_tree, to make replacment from one
|
74
|
+
by another easier, except for object creation:
|
75
|
+
|
76
|
+
in acts_as_tree:
|
77
|
+
|
78
|
+
my_item.children.create(:name => "child1")
|
79
|
+
|
80
|
+
in acts_as_nested_set:
|
81
|
+
|
82
|
+
# adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right) + 1
|
83
|
+
child = MyClass.create(:name => "child1")
|
84
|
+
# now move the item to its desired location
|
85
|
+
child.move_to_child_of my_item
|
86
|
+
|
87
|
+
You can use:
|
88
|
+
* <tt>move_to_child_of</tt>
|
89
|
+
* <tt>move_to_right_of</tt>
|
90
|
+
* <tt>move_to_left_of</tt>
|
91
|
+
and pass them an id or an object.
|
92
|
+
|
93
|
+
Other instance methods added by this plugin include:
|
94
|
+
* <tt>root</tt> - root item of the tree (the one that has a nil parent)
|
95
|
+
* <tt>roots</tt> - root items, in case of multiple roots (the ones that have a nil parent)
|
96
|
+
* <tt>level</tt> - number indicating the level, a root being level 0
|
97
|
+
* <tt>ancestors</tt> - array of all parents, with root as first item
|
98
|
+
* <tt>self_and_ancestors</tt> - array of all parents and self
|
99
|
+
* <tt>siblings</tt> - array of all siblings (items sharing the same parent)
|
100
|
+
* <tt>self_and_siblings</tt> - array of itself and all siblings
|
101
|
+
* <tt>children_count</tt> - count of all direct children
|
102
|
+
* <tt>children</tt> - array of all immediate children
|
103
|
+
* <tt>all_children</tt> - array of all children and nested children
|
104
|
+
* <tt>all_children_count</tt> - count of all nested children
|
105
|
+
* <tt>full_set</tt> - array of itself and all children and nested children
|
106
|
+
* <tt>leaves</tt> - array of the children of this node who do not have children
|
107
|
+
* <tt>leaves_count</tt> - the number of leaves
|
108
|
+
* <tt>check_subtree</tt> - check the left/right indexes of this node and all descendants
|
109
|
+
* <tt>check_full_tree</tt> - check the whole tree this node belongs to
|
110
|
+
* <tt>renumber_full_tree</tt> - recreate the left/right indexes for the whole tree
|
111
|
+
|
112
|
+
These should not be of interest, unless you want to write schema-independent SQL:
|
113
|
+
* <tt>left_col_name</tt> - name of the left column passed on the declaration line
|
114
|
+
* <tt>right_col_name</tt> - name of the right column passed on the declaration line
|
115
|
+
* <tt>parent_col_name</tt> - name of the parent column passed on the declaration line
|
116
|
+
|
117
|
+
Please see the generated RDoc files in doc/ for the full API (run 'rake rdoc' if they need to be created).
|
118
|
+
|
119
|
+
== Concurrency and callbacks
|
120
|
+
|
121
|
+
ActiveRecord does not yet provide a way to treat columns as read-only, which causes problems for
|
122
|
+
nested sets and other things (http://dev.rubyonrails.org/ticket/6896). As a workaround, we have overridden
|
123
|
+
ActiveRecord::Base#update to prevent it from writing to the left/right columns. This protects the left/right
|
124
|
+
values from corruption under concurrent usage, but it breaks the update-related callbacks (before_update and friends).
|
125
|
+
If you need the callbacks and aren't worried about concurrency, you can comment out the update method and the two
|
126
|
+
methods below it (all at the very bottom of better_nested_set.rb).
|
127
|
+
|
128
|
+
If this situation bugs you as much as it does us, leave a comment on the above ticket asking the core team to
|
129
|
+
please apply the patch soon.
|
130
|
+
|
131
|
+
|
132
|
+
== Scopes and roots
|
133
|
+
|
134
|
+
Scope separates trees from each other, and roots are nodes without a parent. The complication is that a tree can
|
135
|
+
have multiple ("virtual") roots.
|
136
|
+
|
137
|
+
Virtual roots?! In some situations, such as a menu, the root of the tree is ignored, and becomes a nuisance to the programmer.
|
138
|
+
In that case it makes sense to remove the root, turning each of its children into a 'virtual root'. These virtual roots
|
139
|
+
are still members of the same tree, sharing a single continuous left/right index.
|
140
|
+
|
141
|
+
Here's an example that demonstrates scopes, roots and virtual roots:
|
142
|
+
class Set < ActiveRecord::Base
|
143
|
+
acts_as_nested_set :scope => :tree_id
|
144
|
+
end
|
145
|
+
|
146
|
+
# This will create two trees, each with a single (real) root.
|
147
|
+
a = Set.create(:tree_id => 1)
|
148
|
+
b = Set.create(:tree_id => 2)
|
149
|
+
|
150
|
+
# This will add a second root to tree #2, so it will have two (virtual) roots.
|
151
|
+
# New objects are by default created as virtual roots at the right side of the tree.
|
152
|
+
c = Set.create(:tree_id => 2) # c.lft is 3, c.rgt is 4 -- the lft/rgt values are contiguous between the two roots
|
153
|
+
|
154
|
+
# When we move c to be a child of b, tree #2 will have a single (real) root again.
|
155
|
+
c.move_to_child_of(b)
|
156
|
+
|
157
|
+
# The table would now look like this:
|
158
|
+
id | parent_id | tree_id | lft | rgt | data
|
159
|
+
1 | NULL | 1 | 1 | 2 | a
|
160
|
+
2 | NULL | 2 | 1 | 4 | b
|
161
|
+
3 | 2 | 2 | 2 | 3 | c
|
162
|
+
|
163
|
+
== Recommendations
|
164
|
+
|
165
|
+
Don't name your left and right columns 'left' and 'right', since most databases reserve these words.
|
166
|
+
Use something like 'lft' and 'rgt' instead.
|
167
|
+
|
168
|
+
If you have a choice between multiple separate trees or one large tree with multiple roots, separate trees will
|
169
|
+
offer better performance when altering tree structure (inserts/moves/deletes).
|
170
|
+
|
171
|
+
= Where to find better_nested_set
|
172
|
+
|
173
|
+
This plugin is provided by Jean-Christophe Michel from Symétrie, and the home page is:
|
174
|
+
|
175
|
+
http://opensource.symetrie.com/trac/better_nested_set/
|
176
|
+
|
177
|
+
= What databases?
|
178
|
+
|
179
|
+
The code has so far been tested on MySQL 5, SQLite3 and PostgreSQL 8, but is thought to work on others.
|
180
|
+
Databases featuring transactions will help protect the left/right indexes from corruption during concurrent usage.
|
181
|
+
|
182
|
+
= Compatibility
|
183
|
+
|
184
|
+
Future versions of this code will break compatibility with the original acts_as_nested_set, but this version
|
185
|
+
is intended to be (almost completely) compatible. Differences include:
|
186
|
+
* New records automatically have their left/right values set to place them at the far right of the tree.
|
187
|
+
* Very minor changes to the deprecated method #root?.
|
188
|
+
|
189
|
+
|
190
|
+
= Running the unit tests
|
191
|
+
|
192
|
+
1) Set up a test database as specified in database.yml. Example for MySQL:
|
193
|
+
|
194
|
+
create database acts_as_nested_set_plugin_test;
|
195
|
+
grant all on acts_as_nested_set_plugin_test.* to 'rails'@'localhost' identified by '';
|
196
|
+
|
197
|
+
2) The tests must be run with the plugin installed in a Rails project, so do that if you haven't already.
|
198
|
+
|
199
|
+
3) Run 'rake test_mysql' (or test_sqlite3 or test_postgresql) in plugins/betternestedset. The default rake task attempts to use
|
200
|
+
all three adapters.
|
201
|
+
|
202
|
+
= License
|
203
|
+
|
204
|
+
Copyright (c) 2006 Jean-Christophe Michel, Symétrie
|
205
|
+
|
206
|
+
The MIT License
|
207
|
+
|
208
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
209
|
+
of this software and associated documentation files (the "Software"), to deal
|
210
|
+
in the Software without restriction, including without limitation the rights
|
211
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
212
|
+
copies of the Software, and to permit persons to whom the Software is
|
213
|
+
furnished to do so, subject to the following conditions:
|
214
|
+
|
215
|
+
The above copyright notice and this permission notice shall be included in
|
216
|
+
all copies or substantial portions of the Software.
|
217
|
+
|
218
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
219
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
220
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
221
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
222
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
223
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
224
|
+
THE SOFTWARE
|
data/Rakefile.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Run tests on all database adapters. See README.'
|
6
|
+
task :default => [:test_mysql, :test_sqlite3, :test_postgresql]
|
7
|
+
|
8
|
+
for adapter in %w(mysql postgresql sqlite3)
|
9
|
+
Rake::TestTask.new("test_#{adapter}") { |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = "test/#{adapter}.rb"
|
12
|
+
t.verbose = true
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
PKG_RDOC_OPTS = ['--main=README',
|
17
|
+
'--line-numbers',
|
18
|
+
'--charset=utf-8',
|
19
|
+
'--promiscuous']
|
20
|
+
|
21
|
+
desc 'Generate documentation'
|
22
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
23
|
+
rdoc.rdoc_dir = 'doc'
|
24
|
+
rdoc.title = 'BetterNestedSet.'
|
25
|
+
rdoc.options = PKG_RDOC_OPTS
|
26
|
+
rdoc.rdoc_files.include('README', 'lib/*.rb')
|
27
|
+
end
|
28
|
+
task :doc => :rdoc
|
@@ -0,0 +1,1129 @@
|
|
1
|
+
module SymetrieCom
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
# This module provides an enhanced acts_as_nested_set mixin for ActiveRecord.
|
9
|
+
# Please see the README for background information, examples, and tips on usage.
|
10
|
+
module ClassMethods
|
11
|
+
# Configuration options are:
|
12
|
+
# * +dependent+ - behaviour for cascading destroy operations (default: :delete_all)
|
13
|
+
# * +parent_column+ - Column name for the parent/child foreign key (default: +parent_id+).
|
14
|
+
# * +left_column+ - Column name for the left index (default: +lft+).
|
15
|
+
# * +right_column+ - Column name for the right index (default: +rgt+). NOTE:
|
16
|
+
# Don't use +left+ and +right+, since these are reserved database words.
|
17
|
+
# * +scope+ - Restricts what is to be considered a tree. Given a symbol, it'll attach "_id"
|
18
|
+
# (if it isn't there already) and use that as the foreign key restriction. It's also possible
|
19
|
+
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
|
20
|
+
# Example: <tt>acts_as_nested_set :scope => 'tree_id = #{tree_id} AND completed = 0'</tt>
|
21
|
+
# * +text_column+ - Column name for the title field (optional). Used as default in the
|
22
|
+
# {your-class}_options_for_select helper method. If empty, will use the first string field
|
23
|
+
# of your model class.
|
24
|
+
def acts_as_nested_set(options = {})
|
25
|
+
|
26
|
+
extend(SingletonMethods) unless respond_to?(:find_in_nestedset)
|
27
|
+
|
28
|
+
options[:scope] = "#{options[:scope]}_id".intern if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
29
|
+
|
30
|
+
write_inheritable_attribute(:acts_as_nested_set_options,
|
31
|
+
{ :parent_column => (options[:parent_column] || 'parent_id'),
|
32
|
+
:left_column => (options[:left_column] || 'lft'),
|
33
|
+
:right_column => (options[:right_column] || 'rgt'),
|
34
|
+
:scope => (options[:scope] || '1 = 1'),
|
35
|
+
:text_column => (options[:text_column] || columns.collect{|c| (c.type == :string) ? c.name : nil }.compact.first),
|
36
|
+
:class => self, # for single-table inheritance
|
37
|
+
:dependent => (options[:dependent] || :delete_all) # accepts :delete_all and :destroy
|
38
|
+
} )
|
39
|
+
|
40
|
+
class_inheritable_reader :acts_as_nested_set_options
|
41
|
+
|
42
|
+
base_set_class.class_inheritable_accessor :acts_as_nested_set_scope_enabled
|
43
|
+
base_set_class.acts_as_nested_set_scope_enabled = true
|
44
|
+
|
45
|
+
if acts_as_nested_set_options[:scope].is_a?(Symbol)
|
46
|
+
scope_condition_method = %(
|
47
|
+
def scope_condition
|
48
|
+
if #{acts_as_nested_set_options[:scope].to_s}.nil?
|
49
|
+
self.class.use_scope_condition? ? "#{table_name}.#{acts_as_nested_set_options[:scope].to_s} IS NULL" : "(1 = 1)"
|
50
|
+
else
|
51
|
+
self.class.use_scope_condition? ? "#{table_name}.#{acts_as_nested_set_options[:scope].to_s} = \#{#{acts_as_nested_set_options[:scope].to_s}}" : "(1 = 1)"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
)
|
55
|
+
else
|
56
|
+
scope_condition_method = "def scope_condition(); self.class.use_scope_condition? ? \"#{acts_as_nested_set_options[:scope]}\" : \"(1 = 1)\"; end"
|
57
|
+
end
|
58
|
+
|
59
|
+
# skip recursive destroy calls
|
60
|
+
attr_accessor :skip_before_destroy
|
61
|
+
|
62
|
+
# no bulk assignment
|
63
|
+
attr_protected acts_as_nested_set_options[:left_column].intern,
|
64
|
+
acts_as_nested_set_options[:right_column].intern,
|
65
|
+
acts_as_nested_set_options[:parent_column].intern
|
66
|
+
# no assignment to structure fields
|
67
|
+
class_eval <<-EOV
|
68
|
+
before_create :set_left_right
|
69
|
+
before_destroy :destroy_descendants
|
70
|
+
include SymetrieCom::Acts::NestedSet::InstanceMethods
|
71
|
+
|
72
|
+
def #{acts_as_nested_set_options[:left_column]}=(x)
|
73
|
+
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:left_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
74
|
+
end
|
75
|
+
def #{acts_as_nested_set_options[:right_column]}=(x)
|
76
|
+
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:right_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
77
|
+
end
|
78
|
+
def #{acts_as_nested_set_options[:parent_column]}=(x)
|
79
|
+
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:parent_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
80
|
+
end
|
81
|
+
#{scope_condition_method}
|
82
|
+
EOV
|
83
|
+
end
|
84
|
+
|
85
|
+
module SingletonMethods
|
86
|
+
|
87
|
+
# Most query methods are wrapped in with_scope to provide further filtering
|
88
|
+
# find_in_nested_set(what, outer_scope, inner_scope)
|
89
|
+
# inner scope is user supplied, while outer_scope is the normal query
|
90
|
+
# this way the user can override most scope attributes, except :conditions
|
91
|
+
# which is merged; use :reverse => true to sort result in reverse direction
|
92
|
+
def find_in_nested_set(*args)
|
93
|
+
what, outer_scope, inner_scope = case args.length
|
94
|
+
when 3 then [args[0], args[1], args[2]]
|
95
|
+
when 2 then [args[0], nil, args[1]]
|
96
|
+
when 1 then [args[0], nil, nil]
|
97
|
+
else [:all, nil, nil]
|
98
|
+
end
|
99
|
+
if inner_scope && outer_scope && inner_scope.delete(:reverse) && outer_scope[:order] == "#{prefixed_left_col_name}"
|
100
|
+
outer_scope[:order] = "#{prefixed_right_col_name} DESC"
|
101
|
+
end
|
102
|
+
acts_as_nested_set_options[:class].with_scope(:find => (outer_scope || {})) do
|
103
|
+
acts_as_nested_set_options[:class].find(what, inner_scope || {})
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Count wrapped in with_scope
|
108
|
+
def count_in_nested_set(*args)
|
109
|
+
outer_scope, inner_scope = case args.length
|
110
|
+
when 2 then [args[0], args[1]]
|
111
|
+
when 1 then [nil, args[0]]
|
112
|
+
else [nil, nil]
|
113
|
+
end
|
114
|
+
acts_as_nested_set_options[:class].with_scope(:find => (outer_scope || {})) do
|
115
|
+
acts_as_nested_set_options[:class].count(inner_scope || {})
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Loop through set using block
|
120
|
+
# pass :nested => false when result is not fully parent-child relational
|
121
|
+
# for example with filtered result sets
|
122
|
+
# Set options[:sort_on] to the name of a column you want to sort on (optional).
|
123
|
+
def recurse_result_set(result, options = {}, &block)
|
124
|
+
return result unless block_given?
|
125
|
+
inner_recursion = options.delete(:inner_recursion)
|
126
|
+
result_set = inner_recursion ? result : result.dup
|
127
|
+
|
128
|
+
parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
|
129
|
+
options[:level] ||= 0
|
130
|
+
options[:nested] = true unless options.key?(:nested)
|
131
|
+
|
132
|
+
siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
|
133
|
+
siblings.sort! {|a,b| a.send(options[:sort_on]) <=> b.send(options[:sort_on])} if options[:sort_on]
|
134
|
+
siblings.each do |sibling|
|
135
|
+
result_set.delete(sibling)
|
136
|
+
block.call(sibling, options[:level])
|
137
|
+
opts = { :parent_id => sibling.id, :level => options[:level] + 1, :inner_recursion => true, :sort_on => options[:sort_on]}
|
138
|
+
recurse_result_set(result_set, opts, &block) if options[:nested]
|
139
|
+
end
|
140
|
+
result_set.each { |orphan| block.call(orphan, options[:level]) } unless inner_recursion
|
141
|
+
end
|
142
|
+
|
143
|
+
# Loop and create a nested array of hashes (with children property)
|
144
|
+
# pass :nested => false when result is not fully parent-child relational
|
145
|
+
# for example with filtered result sets
|
146
|
+
def result_to_array(result, options = {}, &block)
|
147
|
+
array = []
|
148
|
+
inner_recursion = options.delete(:inner_recursion)
|
149
|
+
result_set = inner_recursion ? result : result.dup
|
150
|
+
|
151
|
+
parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
|
152
|
+
level = options[:level] || 0
|
153
|
+
options[:children] ||= 'children'
|
154
|
+
options[:methods] ||= []
|
155
|
+
options[:nested] = true unless options.key?(:nested)
|
156
|
+
options[:symbolize_keys] = true unless options.key?(:symbolize_keys)
|
157
|
+
|
158
|
+
if options[:only].blank? && options[:except].blank?
|
159
|
+
options[:except] = [:left_column, :right_column, :parent_column].inject([]) do |ex, opt|
|
160
|
+
column = acts_as_nested_set_options[opt].to_sym
|
161
|
+
ex << column unless ex.include?(column)
|
162
|
+
ex
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
|
167
|
+
siblings.each do |sibling|
|
168
|
+
result_set.delete(sibling)
|
169
|
+
node = block_given? ? block.call(sibling, level) : sibling.attributes(:only => options[:only], :except => options[:except])
|
170
|
+
options[:methods].inject(node) { |enum, m| enum[m.to_s] = sibling.send(m) if sibling.respond_to?(m); enum }
|
171
|
+
if options[:nested]
|
172
|
+
opts = options.merge(:parent_id => sibling.id, :level => level + 1, :inner_recursion => true)
|
173
|
+
childnodes = result_to_array(result_set, opts, &block)
|
174
|
+
node[ options[:children] ] = childnodes if !childnodes.empty? && node.respond_to?(:[]=)
|
175
|
+
end
|
176
|
+
array << (options[:symbolize_keys] && node.respond_to?(:symbolize_keys) ? node.symbolize_keys : node)
|
177
|
+
end
|
178
|
+
unless inner_recursion
|
179
|
+
result_set.each do |orphan|
|
180
|
+
node = (block_given? ? block.call(orphan, level) : orphan.attributes(:only => options[:only], :except => options[:except]))
|
181
|
+
options[:methods].inject(node) { |enum, m| enum[m.to_s] = orphan.send(m) if orphan.respond_to?(m); enum }
|
182
|
+
array << (options[:symbolize_keys] && node.respond_to?(:symbolize_keys) ? node.symbolize_keys : node)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
array
|
186
|
+
end
|
187
|
+
|
188
|
+
# Loop and create an xml structure. The following options are available
|
189
|
+
# :root sets the root tag, :children sets the siblings tag
|
190
|
+
# :record sets the node item tag, if given
|
191
|
+
# see also: result_to_array and ActiveRecord::XmlSerialization
|
192
|
+
def result_to_xml(result, options = {}, &block)
|
193
|
+
inner_recursion = options.delete(:inner_recursion)
|
194
|
+
result_set = inner_recursion ? result : result.dup
|
195
|
+
|
196
|
+
parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
|
197
|
+
options[:nested] = true unless options.key?(:nested)
|
198
|
+
|
199
|
+
options[:except] ||= []
|
200
|
+
[:left_column, :right_column, :parent_column].each do |opt|
|
201
|
+
column = acts_as_nested_set_options[opt].intern
|
202
|
+
options[:except] << column unless options[:except].include?(column)
|
203
|
+
end
|
204
|
+
|
205
|
+
options[:indent] ||= 2
|
206
|
+
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
207
|
+
options[:builder].instruct! unless options.delete(:skip_instruct)
|
208
|
+
|
209
|
+
record = options.delete(:record)
|
210
|
+
root = options.delete(:root) || :nodes
|
211
|
+
children = options.delete(:children) || :children
|
212
|
+
|
213
|
+
attrs = {}
|
214
|
+
attrs[:xmlns] = options[:namespace] if options[:namespace]
|
215
|
+
|
216
|
+
siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
|
217
|
+
options[:builder].tag!(root, attrs) do
|
218
|
+
siblings.each do |sibling|
|
219
|
+
result_set.delete(sibling) if options[:nested]
|
220
|
+
procs = options[:procs] ? options[:procs].dup : []
|
221
|
+
procs << Proc.new { |opts| block.call(opts, sibling) } if block_given?
|
222
|
+
if options[:nested]
|
223
|
+
proc = Proc.new do |opts|
|
224
|
+
proc_opts = opts.merge(:parent_id => sibling.id, :root => children, :record => record, :inner_recursion => true)
|
225
|
+
proc_opts[:procs] ||= options[:procs] if options[:procs]
|
226
|
+
proc_opts[:methods] ||= options[:methods] if options[:methods]
|
227
|
+
sibling.class.result_to_xml(result_set, proc_opts, &block)
|
228
|
+
end
|
229
|
+
procs << proc
|
230
|
+
end
|
231
|
+
opts = options.merge(:procs => procs, :skip_instruct => true, :root => record)
|
232
|
+
sibling.to_xml(opts)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
options[:builder].target!
|
236
|
+
end
|
237
|
+
|
238
|
+
# Loop and create a nested xml representation of nodes with attributes
|
239
|
+
# pass :nested => false when result is not fully parent-child relational
|
240
|
+
# for example with filtered result sets
|
241
|
+
def result_to_attributes_xml(result, options = {}, &block)
|
242
|
+
inner_recursion = options.delete(:inner_recursion)
|
243
|
+
result_set = inner_recursion ? result : result.dup
|
244
|
+
|
245
|
+
parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
|
246
|
+
level = options[:level] || 0
|
247
|
+
options[:methods] ||= []
|
248
|
+
options[:nested] = true unless options.key?(:nested)
|
249
|
+
options[:dasherize] = true unless options.key?(:dasherize)
|
250
|
+
|
251
|
+
if options[:only].blank? && options[:except].blank?
|
252
|
+
options[:except] = [:left_column, :right_column, :parent_column].inject([]) do |ex, opt|
|
253
|
+
column = acts_as_nested_set_options[opt].to_sym
|
254
|
+
ex << column unless ex.include?(column)
|
255
|
+
ex
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
options[:indent] ||= 2
|
260
|
+
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
261
|
+
options[:builder].instruct! unless options.delete(:skip_instruct)
|
262
|
+
|
263
|
+
parent_attrs = {}
|
264
|
+
parent_attrs[:xmlns] = options[:namespace] if options[:namespace]
|
265
|
+
|
266
|
+
siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
|
267
|
+
siblings.each do |sibling|
|
268
|
+
result_set.delete(sibling)
|
269
|
+
node_tag = (options[:record] || sibling[sibling.class.inheritance_column] || 'node').underscore
|
270
|
+
node_tag = node_tag.dasherize unless options[:dasherize]
|
271
|
+
attrs = block_given? ? block.call(sibling, level) : sibling.attributes(:only => options[:only], :except => options[:except])
|
272
|
+
options[:methods].inject(attrs) { |enum, m| enum[m.to_s] = sibling.send(m) if sibling.respond_to?(m); enum }
|
273
|
+
if options[:nested] && sibling.children?
|
274
|
+
opts = options.merge(:parent_id => sibling.id, :level => level + 1, :inner_recursion => true, :skip_instruct => true)
|
275
|
+
options[:builder].tag!(node_tag, attrs) { result_to_attributes_xml(result_set, opts, &block) }
|
276
|
+
else
|
277
|
+
options[:builder].tag!(node_tag, attrs)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
unless inner_recursion
|
281
|
+
result_set.each do |orphan|
|
282
|
+
node_tag = (options[:record] || orphan[orphan.class.inheritance_column] || 'node').underscore
|
283
|
+
node_tag = node_tag.dasherize unless options[:dasherize]
|
284
|
+
attrs = block_given? ? block.call(orphan, level) : orphan.attributes(:only => options[:only], :except => options[:except])
|
285
|
+
options[:methods].inject(attrs) { |enum, m| enum[m.to_s] = orphan.send(m) if orphan.respond_to?(m); enum }
|
286
|
+
options[:builder].tag!(node_tag, attrs)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
options[:builder].target!
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns the single root for the class (or just the first root, if there are several).
|
293
|
+
# Deprecation note: the original acts_as_nested_set allowed roots to have parent_id = 0,
|
294
|
+
# so we currently do the same. This silliness will not be tolerated in future versions, however.
|
295
|
+
def root(scope = {})
|
296
|
+
find_in_nested_set(:first, { :conditions => "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)" }, scope)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Returns the roots and/or virtual roots of all trees. See the explanation of virtual roots in the README.
|
300
|
+
def roots(scope = {})
|
301
|
+
find_in_nested_set(:all, { :conditions => "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Checks the left/right indexes of all records,
|
305
|
+
# returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
|
306
|
+
def check_all
|
307
|
+
total = 0
|
308
|
+
transaction do
|
309
|
+
# if there are virtual roots, only call check_full_tree on the first, because it will check other virtual roots in that tree.
|
310
|
+
total = roots.inject(0) {|sum, r| sum + (r[r.left_col_name] == 1 ? r.check_full_tree : 0 )}
|
311
|
+
raise ActiveRecord::ActiveRecordError, "Scope problems or nodes without a valid root" unless acts_as_nested_set_options[:class].count == total
|
312
|
+
end
|
313
|
+
return total
|
314
|
+
end
|
315
|
+
|
316
|
+
# Re-calculate the left/right values of all nodes. Can be used to convert ordinary trees into nested sets.
|
317
|
+
def renumber_all
|
318
|
+
scopes = []
|
319
|
+
# only call it once for each scope_condition (if the scope conditions are messed up, this will obviously cause problems)
|
320
|
+
roots.each do |r|
|
321
|
+
r.renumber_full_tree unless scopes.include?(r.scope_condition)
|
322
|
+
scopes << r.scope_condition
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Returns an SQL fragment that matches _items_ *and* all of their descendants, for use in a WHERE clause.
|
327
|
+
# You can pass it a single object, a single ID, or an array of objects and/or IDs.
|
328
|
+
# # if a.lft = 2, a.rgt = 7, b.lft = 12 and b.rgt = 13
|
329
|
+
# Set.sql_for([a,b]) # returns "((lft BETWEEN 2 AND 7) OR (lft BETWEEN 12 AND 13))"
|
330
|
+
# Returns "1 != 1" if passed no items. If you need to exclude items, just use "NOT (#{sql_for(items)})".
|
331
|
+
# Note that if you have multiple trees, it is up to you to apply your scope condition.
|
332
|
+
def sql_for(items)
|
333
|
+
items = [items] unless items.is_a?(Array)
|
334
|
+
# get objects for IDs
|
335
|
+
items.collect! {|s| s.is_a?(acts_as_nested_set_options[:class]) ? s : acts_as_nested_set_options[:class].find(s)}.uniq
|
336
|
+
items.reject! {|e| e.new_record?} # exclude unsaved items, since they don't have left/right values yet
|
337
|
+
|
338
|
+
return "1 != 1" if items.empty? # PostgreSQL didn't like '0', and SQLite3 didn't like 'FALSE'
|
339
|
+
items.map! {|e| "(#{prefixed_left_col_name} BETWEEN #{e[left_col_name]} AND #{e[right_col_name]})" }
|
340
|
+
"(#{items.join(' OR ')})"
|
341
|
+
end
|
342
|
+
|
343
|
+
# Wrap a method with this block to disable the default scope_condition
|
344
|
+
def without_scope_condition(&block)
|
345
|
+
if block_given?
|
346
|
+
disable_scope_condition
|
347
|
+
yield
|
348
|
+
enable_scope_condition
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def use_scope_condition?#:nodoc:
|
353
|
+
base_set_class.acts_as_nested_set_scope_enabled == true
|
354
|
+
end
|
355
|
+
|
356
|
+
def disable_scope_condition#:nodoc:
|
357
|
+
base_set_class.acts_as_nested_set_scope_enabled = false
|
358
|
+
end
|
359
|
+
|
360
|
+
def enable_scope_condition#:nodoc:
|
361
|
+
base_set_class.acts_as_nested_set_scope_enabled = true
|
362
|
+
end
|
363
|
+
|
364
|
+
def left_col_name#:nodoc:
|
365
|
+
acts_as_nested_set_options[:left_column]
|
366
|
+
end
|
367
|
+
def prefixed_left_col_name#:nodoc:
|
368
|
+
"#{table_name}.#{left_col_name}"
|
369
|
+
end
|
370
|
+
def right_col_name#:nodoc:
|
371
|
+
acts_as_nested_set_options[:right_column]
|
372
|
+
end
|
373
|
+
def prefixed_right_col_name#:nodoc:
|
374
|
+
"#{table_name}.#{right_col_name}"
|
375
|
+
end
|
376
|
+
def parent_col_name#:nodoc:
|
377
|
+
acts_as_nested_set_options[:parent_column]
|
378
|
+
end
|
379
|
+
def prefixed_parent_col_name#:nodoc:
|
380
|
+
"#{table_name}.#{parent_col_name}"
|
381
|
+
end
|
382
|
+
def base_set_class#:nodoc:
|
383
|
+
acts_as_nested_set_options[:class] # for single-table inheritance
|
384
|
+
end
|
385
|
+
|
386
|
+
end
|
387
|
+
|
388
|
+
end
|
389
|
+
|
390
|
+
# This module provides instance methods for an enhanced acts_as_nested_set mixin. Please see the README for background information, examples, and tips on usage.
|
391
|
+
module InstanceMethods
|
392
|
+
# convenience methods to make the code more readable
|
393
|
+
def left_col_name#:nodoc:
|
394
|
+
self.class.left_col_name
|
395
|
+
end
|
396
|
+
def prefixed_left_col_name#:nodoc:
|
397
|
+
self.class.prefixed_left_col_name
|
398
|
+
end
|
399
|
+
def right_col_name#:nodoc:
|
400
|
+
self.class.right_col_name
|
401
|
+
end
|
402
|
+
def prefixed_right_col_name#:nodoc:
|
403
|
+
self.class.prefixed_right_col_name
|
404
|
+
end
|
405
|
+
def parent_col_name#:nodoc:
|
406
|
+
self.class.parent_col_name
|
407
|
+
end
|
408
|
+
def prefixed_parent_col_name#:nodoc:
|
409
|
+
self.class.prefixed_parent_col_name
|
410
|
+
end
|
411
|
+
alias parent_column parent_col_name#:nodoc: Deprecated
|
412
|
+
def base_set_class#:nodoc:
|
413
|
+
acts_as_nested_set_options[:class] # for single-table inheritance
|
414
|
+
end
|
415
|
+
|
416
|
+
# This takes care of valid queries when called on a root node
|
417
|
+
def sibling_condition
|
418
|
+
self[parent_col_name] ? "#{prefixed_parent_col_name} = #{self[parent_col_name]}" : "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)"
|
419
|
+
end
|
420
|
+
|
421
|
+
# On creation, automatically add the new node to the right of all existing nodes in this tree.
|
422
|
+
def set_left_right # already protected by a transaction within #create
|
423
|
+
maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0
|
424
|
+
self[left_col_name] = maxright+1
|
425
|
+
self[right_col_name] = maxright+2
|
426
|
+
end
|
427
|
+
|
428
|
+
# On destruction, delete all children and shift the lft/rgt values back to the left so the counts still work.
|
429
|
+
def destroy_descendants # already protected by a transaction within #destroy
|
430
|
+
return if self[right_col_name].nil? || self[left_col_name].nil? || self.skip_before_destroy
|
431
|
+
reloaded = self.reload rescue nil # in case a concurrent move has altered the indexes - rescue if non-existent
|
432
|
+
return unless reloaded
|
433
|
+
dif = self[right_col_name] - self[left_col_name] + 1
|
434
|
+
if acts_as_nested_set_options[:dependent] == :delete_all
|
435
|
+
base_set_class.delete_all( "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})" )
|
436
|
+
else
|
437
|
+
set = base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})", :order => "#{prefixed_right_col_name} DESC")
|
438
|
+
set.each { |child| child.skip_before_destroy = true; remove_descendant(child) }
|
439
|
+
end
|
440
|
+
base_set_class.update_all("#{left_col_name} = CASE \
|
441
|
+
WHEN #{left_col_name} > #{self[right_col_name]} THEN (#{left_col_name} - #{dif}) \
|
442
|
+
ELSE #{left_col_name} END, \
|
443
|
+
#{right_col_name} = CASE \
|
444
|
+
WHEN #{right_col_name} > #{self[right_col_name]} THEN (#{right_col_name} - #{dif} ) \
|
445
|
+
ELSE #{right_col_name} END",
|
446
|
+
scope_condition)
|
447
|
+
end
|
448
|
+
|
449
|
+
# By default, records are compared and sorted using the left column.
|
450
|
+
def <=>(x)
|
451
|
+
self[left_col_name] <=> x[left_col_name]
|
452
|
+
end
|
453
|
+
|
454
|
+
# Deprecated. Returns true if this is a root node.
|
455
|
+
def root?
|
456
|
+
parent_id = self[parent_col_name]
|
457
|
+
(parent_id == 0 || parent_id.nil?) && self[right_col_name] && self[left_col_name] && (self[right_col_name] > self[left_col_name])
|
458
|
+
end
|
459
|
+
|
460
|
+
# Deprecated. Returns true if this is a child node
|
461
|
+
def child?
|
462
|
+
parent_id = self[parent_col_name]
|
463
|
+
!(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name])
|
464
|
+
end
|
465
|
+
|
466
|
+
# Deprecated. Returns true if we have no idea what this is
|
467
|
+
def unknown?
|
468
|
+
!root? && !child?
|
469
|
+
end
|
470
|
+
|
471
|
+
# Returns this record's root ancestor.
|
472
|
+
def root(scope = {})
|
473
|
+
# the BETWEEN clause is needed to ensure we get the right virtual root, if using those
|
474
|
+
self.class.find_in_nested_set(:first, { :conditions => "#{scope_condition} \
|
475
|
+
AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0) AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope)
|
476
|
+
end
|
477
|
+
|
478
|
+
# Returns the root or virtual roots of this record's tree (a tree cannot have more than one real root). See the explanation of virtual roots in the README.
|
479
|
+
def roots(scope = {})
|
480
|
+
self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope)
|
481
|
+
end
|
482
|
+
|
483
|
+
# Returns this record's parent.
|
484
|
+
def parent
|
485
|
+
self.class.find_in_nested_set(self[parent_col_name]) if self[parent_col_name]
|
486
|
+
end
|
487
|
+
|
488
|
+
# Returns an array of all parents, starting with the root.
|
489
|
+
def ancestors(scope = {})
|
490
|
+
self_and_ancestors(scope) - [self]
|
491
|
+
end
|
492
|
+
|
493
|
+
# Returns an array of all parents plus self, starting with the root.
|
494
|
+
def self_and_ancestors(scope = {})
|
495
|
+
self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})", :order => "#{prefixed_left_col_name}" }, scope)
|
496
|
+
end
|
497
|
+
|
498
|
+
# Returns all the children of this node's parent, except self.
|
499
|
+
def siblings(scope = {})
|
500
|
+
self_and_siblings(scope) - [self]
|
501
|
+
end
|
502
|
+
|
503
|
+
# Returns all siblings to the left of self, in descending order, so the first sibling is the one closest to the left of self
|
504
|
+
def previous_siblings(scope = {})
|
505
|
+
self.class.find_in_nested_set(:all,
|
506
|
+
{ :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_right_col_name} < ?", self.id, self[left_col_name]], :order => "#{prefixed_left_col_name} DESC" }, scope)
|
507
|
+
end
|
508
|
+
|
509
|
+
# Returns all siblings to the right of self, in ascending order, so the first sibling is the one closest to the right of self
|
510
|
+
def next_siblings(scope = {})
|
511
|
+
self.class.find_in_nested_set(:all,
|
512
|
+
{ :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_left_col_name} > ?", self.id, self[right_col_name]], :order => "#{prefixed_left_col_name} ASC"}, scope)
|
513
|
+
end
|
514
|
+
|
515
|
+
# Returns first siblings amongst it's siblings.
|
516
|
+
def first_sibling(scope = {})
|
517
|
+
self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} ASC")).first
|
518
|
+
end
|
519
|
+
|
520
|
+
def first_sibling?(scope = {})
|
521
|
+
self == first_sibling(scope)
|
522
|
+
end
|
523
|
+
alias :first? :first_sibling?
|
524
|
+
|
525
|
+
# Returns last siblings amongst it's siblings.
|
526
|
+
def last_sibling(scope = {})
|
527
|
+
self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} DESC")).first
|
528
|
+
end
|
529
|
+
|
530
|
+
def last_sibling?(scope = {})
|
531
|
+
self == last_sibling(scope)
|
532
|
+
end
|
533
|
+
alias :last? :last_sibling?
|
534
|
+
|
535
|
+
# Returns previous sibling of node or nil if there is none.
|
536
|
+
def previous_sibling(num = 1, scope = {})
|
537
|
+
scope[:limit] = num
|
538
|
+
siblings = previous_siblings(scope)
|
539
|
+
num == 1 ? siblings.first : siblings
|
540
|
+
end
|
541
|
+
alias :higher_item :previous_sibling
|
542
|
+
|
543
|
+
# Returns next sibling of node or nil if there is none.
|
544
|
+
def next_sibling(num = 1, scope = {})
|
545
|
+
scope[:limit] = num
|
546
|
+
siblings = next_siblings(scope)
|
547
|
+
num == 1 ? siblings.first : siblings
|
548
|
+
end
|
549
|
+
alias :lower_item :next_sibling
|
550
|
+
|
551
|
+
# Returns all the children of this node's parent, including self.
|
552
|
+
def self_and_siblings(scope = {})
|
553
|
+
if self[parent_col_name].nil? || self[parent_col_name].zero?
|
554
|
+
[self]
|
555
|
+
else
|
556
|
+
self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition}", :order => "#{prefixed_left_col_name}" }, scope)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
# Returns the level of this object in the tree, root level being 0.
|
561
|
+
def level(scope = {})
|
562
|
+
return 0 if self[parent_col_name].nil?
|
563
|
+
self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope) - 1
|
564
|
+
end
|
565
|
+
|
566
|
+
# Returns the number of nested children of this object.
|
567
|
+
def all_children_count(scope = nil)
|
568
|
+
return all_children(scope).length if scope.is_a?(Hash)
|
569
|
+
return (self[right_col_name] - self[left_col_name] - 1)/2
|
570
|
+
end
|
571
|
+
|
572
|
+
# Returns itself and all nested children.
|
573
|
+
# Pass :exclude => item, or id, or [items or id] to exclude one or more items *and* all of their descendants.
|
574
|
+
def full_set(scope = {})
|
575
|
+
if exclude = scope.delete(:exclude)
|
576
|
+
exclude_str = " AND NOT (#{base_set_class.sql_for(exclude)}) "
|
577
|
+
elsif new_record? || self[right_col_name] - self[left_col_name] == 1
|
578
|
+
return [self]
|
579
|
+
end
|
580
|
+
self.class.find_in_nested_set(:all, {
|
581
|
+
:order => "#{prefixed_left_col_name}",
|
582
|
+
:conditions => "#{scope_condition} #{exclude_str} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})"
|
583
|
+
}, scope)
|
584
|
+
end
|
585
|
+
|
586
|
+
# Returns the child for the requested id within the scope of its children, otherwise nil
|
587
|
+
def child_by_id(id, scope = {})
|
588
|
+
children_by_id(id, scope).first
|
589
|
+
end
|
590
|
+
|
591
|
+
# Returns a child collection for the requested ids within the scope of its children, otherwise empty array
|
592
|
+
def children_by_id(*args)
|
593
|
+
scope = args.last.is_a?(Hash) ? args.pop : {}
|
594
|
+
ids = args.flatten.compact.uniq
|
595
|
+
self.class.find_in_nested_set(:all, {
|
596
|
+
:conditions => ["#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids]
|
597
|
+
}, scope)
|
598
|
+
end
|
599
|
+
|
600
|
+
# Returns the child for the requested id within the scope of its immediate children, otherwise nil
|
601
|
+
def direct_child_by_id(id, scope = {})
|
602
|
+
direct_children_by_id(id, scope).first
|
603
|
+
end
|
604
|
+
|
605
|
+
# Returns a child collection for the requested ids within the scope of its immediate children, otherwise empty array
|
606
|
+
def direct_children_by_id(*args)
|
607
|
+
scope = args.last.is_a?(Hash) ? args.pop : {}
|
608
|
+
ids = args.flatten.compact.uniq
|
609
|
+
self.class.find_in_nested_set(:all, {
|
610
|
+
:conditions => ["#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id} AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids]
|
611
|
+
}, scope)
|
612
|
+
end
|
613
|
+
|
614
|
+
# Tests wether self is within scope of parent
|
615
|
+
def child_of?(parent, scope = {})
|
616
|
+
if !scope.empty? && parent.respond_to?(:child_by_id)
|
617
|
+
parent.child_by_id(self.id, scope).is_a?(self.class)
|
618
|
+
else
|
619
|
+
parent.respond_to?(left_col_name) && self[left_col_name] > parent[left_col_name] && self[right_col_name] < parent[right_col_name]
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# Tests wether self is within immediate scope of parent
|
624
|
+
def direct_child_of?(parent, scope = {})
|
625
|
+
if !scope.empty? && parent.respond_to?(:direct_child_by_id)
|
626
|
+
parent.direct_child_by_id(self.id, scope).is_a?(self.class)
|
627
|
+
else
|
628
|
+
parent.respond_to?(parent_col_name) && self[parent_col_name] == parent.id
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Returns all children and nested children.
|
633
|
+
# Pass :exclude => item, or id, or [items or id] to exclude one or more items *and* all of their descendants.
|
634
|
+
def all_children(scope = {})
|
635
|
+
full_set(scope) - [self]
|
636
|
+
end
|
637
|
+
|
638
|
+
def children_count(scope= {})
|
639
|
+
self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}" }, scope)
|
640
|
+
end
|
641
|
+
|
642
|
+
# Returns this record's immediate children.
|
643
|
+
def children(scope = {})
|
644
|
+
self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}", :order => "#{prefixed_left_col_name}" }, scope)
|
645
|
+
end
|
646
|
+
|
647
|
+
def children?(scope = {})
|
648
|
+
children_count(scope) > 0
|
649
|
+
end
|
650
|
+
|
651
|
+
# Deprecated
|
652
|
+
alias direct_children children
|
653
|
+
|
654
|
+
# Returns this record's terminal children (nodes without children).
|
655
|
+
def leaves(scope = {})
|
656
|
+
self.class.find_in_nested_set(:all,
|
657
|
+
{ :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}", :order => "#{prefixed_left_col_name}" }, scope)
|
658
|
+
end
|
659
|
+
|
660
|
+
# Returns the count of this record's terminal children (nodes without children).
|
661
|
+
def leaves_count(scope = {})
|
662
|
+
self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}" }, scope)
|
663
|
+
end
|
664
|
+
|
665
|
+
# All nodes between two nodes, those nodes included
|
666
|
+
# in effect all ancestors until the other is reached
|
667
|
+
def ancestors_and_self_through(other, scope = {})
|
668
|
+
first, last = [self, other].sort
|
669
|
+
self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{last[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name}) AND #{prefixed_left_col_name} >= #{first[left_col_name]}",
|
670
|
+
:order => "#{prefixed_left_col_name}" }, scope)
|
671
|
+
end
|
672
|
+
|
673
|
+
# Ancestors until the other is reached - excluding self
|
674
|
+
def ancestors_through(other, scope = {})
|
675
|
+
ancestors_and_self_through(other, scope) - [self]
|
676
|
+
end
|
677
|
+
|
678
|
+
# All children until the other is reached - excluding self
|
679
|
+
def all_children_through(other, scope = {})
|
680
|
+
full_set_through(other, scope) - [self]
|
681
|
+
end
|
682
|
+
|
683
|
+
# All children until the other is reached - including self
|
684
|
+
def full_set_through(other, scope = {})
|
685
|
+
first, last = [self, other].sort
|
686
|
+
self.class.find_in_nested_set(:all,
|
687
|
+
{ :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{first[right_col_name]}) AND #{prefixed_left_col_name} <= #{last[left_col_name]}", :order => "#{prefixed_left_col_name}" }, scope)
|
688
|
+
end
|
689
|
+
|
690
|
+
# All siblings until the other is reached - including self
|
691
|
+
def self_and_siblings_through(other, scope = {})
|
692
|
+
if self[parent_col_name].nil? || self[parent_col_name].zero?
|
693
|
+
[self]
|
694
|
+
else
|
695
|
+
first, last = [self, other].sort
|
696
|
+
self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{last[right_col_name]})", :order => "#{prefixed_left_col_name}" }, scope)
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
# All siblings until the other is reached - excluding self
|
701
|
+
def siblings_through(other, scope = {})
|
702
|
+
self_and_siblings_through(other, scope) - [self]
|
703
|
+
end
|
704
|
+
|
705
|
+
# Checks the left/right indexes of one node and all descendants.
|
706
|
+
# Throws ActiveRecord::ActiveRecordError if it finds a problem.
|
707
|
+
def check_subtree
|
708
|
+
transaction do
|
709
|
+
self.reload
|
710
|
+
check # this method is implemented via #check, so that we don't generate lots of unnecessary nested transactions
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
# Checks the left/right indexes of the entire tree that this node belongs to,
|
715
|
+
# returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
|
716
|
+
# This method is needed because check_subtree alone cannot find gaps between virtual roots, orphaned nodes or endless loops.
|
717
|
+
def check_full_tree
|
718
|
+
total_nodes = 0
|
719
|
+
transaction do
|
720
|
+
# virtual roots make this method more complex than it otherwise would be
|
721
|
+
n = 1
|
722
|
+
roots.each do |r|
|
723
|
+
raise ActiveRecord::ActiveRecordError, "Gaps between roots in the tree containing record ##{r.id}" if r[left_col_name] != n
|
724
|
+
r.check_subtree
|
725
|
+
n = r[right_col_name] + 1
|
726
|
+
end
|
727
|
+
total_nodes = roots.inject(0) {|sum, r| sum + r.all_children_count + 1 }
|
728
|
+
unless base_set_class.count(:conditions => "#{scope_condition}") == total_nodes
|
729
|
+
raise ActiveRecord::ActiveRecordError, "Orphaned nodes or endless loops in the tree containing record ##{self.id}"
|
730
|
+
end
|
731
|
+
end
|
732
|
+
return total_nodes
|
733
|
+
end
|
734
|
+
|
735
|
+
# Re-calculate the left/right values of all nodes in this record's tree. Can be used to convert an ordinary tree into a nested set.
|
736
|
+
def renumber_full_tree
|
737
|
+
indexes = []
|
738
|
+
n = 1
|
739
|
+
transaction do
|
740
|
+
for r in roots # because we may have virtual roots
|
741
|
+
n = 1 + r.calc_numbers(n, indexes)
|
742
|
+
end
|
743
|
+
for i in indexes
|
744
|
+
base_set_class.update_all("#{left_col_name} = #{i[:lft]}, #{right_col_name} = #{i[:rgt]}", "#{self.class.primary_key} = #{i[:id]}")
|
745
|
+
end
|
746
|
+
end
|
747
|
+
## reload?
|
748
|
+
end
|
749
|
+
|
750
|
+
# Deprecated. Adds a child to this object in the tree. If this object hasn't been initialized,
|
751
|
+
# it gets set up as a root node.
|
752
|
+
#
|
753
|
+
# This method exists only for compatibility and will be removed in future versions.
|
754
|
+
def add_child(child)
|
755
|
+
transaction do
|
756
|
+
self.reload; child.reload # for compatibility with old version
|
757
|
+
# the old version allows records with nil values for lft and rgt
|
758
|
+
unless self[left_col_name] && self[right_col_name]
|
759
|
+
if child[left_col_name] || child[right_col_name]
|
760
|
+
raise ActiveRecord::ActiveRecordError, "If parent lft or rgt are nil, you can't add a child with non-nil lft or rgt"
|
761
|
+
end
|
762
|
+
base_set_class.update_all("#{left_col_name} = CASE \
|
763
|
+
WHEN id = #{self.id} \
|
764
|
+
THEN 1 \
|
765
|
+
WHEN id = #{child.id} \
|
766
|
+
THEN 3 \
|
767
|
+
ELSE #{left_col_name} END, \
|
768
|
+
#{right_col_name} = CASE \
|
769
|
+
WHEN id = #{self.id} \
|
770
|
+
THEN 2 \
|
771
|
+
WHEN id = #{child.id} \
|
772
|
+
THEN 4 \
|
773
|
+
ELSE #{right_col_name} END",
|
774
|
+
scope_condition)
|
775
|
+
self.reload; child.reload
|
776
|
+
end
|
777
|
+
unless child[left_col_name] && child[right_col_name]
|
778
|
+
maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0
|
779
|
+
base_set_class.update_all("#{left_col_name} = CASE \
|
780
|
+
WHEN id = #{child.id} \
|
781
|
+
THEN #{maxright + 1} \
|
782
|
+
ELSE #{left_col_name} END, \
|
783
|
+
#{right_col_name} = CASE \
|
784
|
+
WHEN id = #{child.id} \
|
785
|
+
THEN #{maxright + 2} \
|
786
|
+
ELSE #{right_col_name} END",
|
787
|
+
scope_condition)
|
788
|
+
child.reload
|
789
|
+
end
|
790
|
+
|
791
|
+
child.move_to_child_of(self)
|
792
|
+
# self.reload ## even though move_to calls target.reload, at least one object in the tests was not reloading (near the end of test_common_usage)
|
793
|
+
end
|
794
|
+
# self.reload
|
795
|
+
# child.reload
|
796
|
+
#
|
797
|
+
# if child.root?
|
798
|
+
# raise ActiveRecord::ActiveRecordError, "Adding sub-tree isn\'t currently supported"
|
799
|
+
# else
|
800
|
+
# if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) )
|
801
|
+
# # Looks like we're now the root node! Woo
|
802
|
+
# self[left_col_name] = 1
|
803
|
+
# self[right_col_name] = 4
|
804
|
+
#
|
805
|
+
# # What do to do about validation?
|
806
|
+
# return nil unless self.save
|
807
|
+
#
|
808
|
+
# child[parent_col_name] = self.id
|
809
|
+
# child[left_col_name] = 2
|
810
|
+
# child[right_col_name]= 3
|
811
|
+
# return child.save
|
812
|
+
# else
|
813
|
+
# # OK, we need to add and shift everything else to the right
|
814
|
+
# child[parent_col_name] = self.id
|
815
|
+
# right_bound = self[right_col_name]
|
816
|
+
# child[left_col_name] = right_bound
|
817
|
+
# child[right_col_name] = right_bound + 1
|
818
|
+
# self[right_col_name] += 2
|
819
|
+
# self.class.transaction {
|
820
|
+
# self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
|
821
|
+
# self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
|
822
|
+
# self.save
|
823
|
+
# child.save
|
824
|
+
# }
|
825
|
+
# end
|
826
|
+
# end
|
827
|
+
end
|
828
|
+
|
829
|
+
# Insert a node at a specific position among the children of target.
|
830
|
+
def insert_at(target, index = :last, scope = {})
|
831
|
+
level_nodes = target.children(scope)
|
832
|
+
current_index = level_nodes.index(self)
|
833
|
+
last_index = level_nodes.length - 1
|
834
|
+
as_first = (index == :first)
|
835
|
+
as_last = (index == :last || (index.is_a?(Fixnum) && index > last_index))
|
836
|
+
index = 0 if as_first
|
837
|
+
index = last_index if as_last
|
838
|
+
if last_index < 0
|
839
|
+
move_to_child_of(target)
|
840
|
+
elsif index >= 0 && index <= last_index && level_nodes[index]
|
841
|
+
if as_last && index != current_index
|
842
|
+
move_to_right_of(level_nodes[index])
|
843
|
+
elsif (as_first || index == 0) && index != current_index
|
844
|
+
move_to_left_of(level_nodes[index])
|
845
|
+
elsif !current_index.nil? && index > current_index
|
846
|
+
move_to_right_of(level_nodes[index])
|
847
|
+
elsif !current_index.nil? && index < current_index
|
848
|
+
move_to_left_of(level_nodes[index])
|
849
|
+
elsif current_index.nil?
|
850
|
+
move_to_left_of(level_nodes[index])
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
# Move this node to the left of _target_ (you can pass an object or just an id).
|
856
|
+
# Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
|
857
|
+
def move_to_left_of(target)
|
858
|
+
self.move_to target, :left
|
859
|
+
end
|
860
|
+
|
861
|
+
# Move this node to the right of _target_ (you can pass an object or just an id).
|
862
|
+
# Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
|
863
|
+
def move_to_right_of(target)
|
864
|
+
self.move_to target, :right
|
865
|
+
end
|
866
|
+
|
867
|
+
# Make this node a child of _target_ (you can pass an object or just an id).
|
868
|
+
# Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
|
869
|
+
def move_to_child_of(target)
|
870
|
+
self.move_to target, :child
|
871
|
+
end
|
872
|
+
|
873
|
+
# Moves a node to a certain position amongst its siblings.
|
874
|
+
def move_to_position(index, scope = {})
|
875
|
+
insert_at(self.parent, index, scope)
|
876
|
+
end
|
877
|
+
|
878
|
+
# Moves a node one up amongst its siblings. Does nothing if it's already
|
879
|
+
# the first sibling.
|
880
|
+
def move_lower
|
881
|
+
next_sib = next_sibling
|
882
|
+
move_to_right_of(next_sib) if next_sib
|
883
|
+
end
|
884
|
+
|
885
|
+
# Moves a node one down amongst its siblings. Does nothing if it's already
|
886
|
+
# the last sibling.
|
887
|
+
def move_higher
|
888
|
+
prev_sib = previous_sibling
|
889
|
+
move_to_left_of(prev_sib) if prev_sib
|
890
|
+
end
|
891
|
+
|
892
|
+
# Moves a node one to be the first amongst its siblings. Does nothing if it's already
|
893
|
+
# the first sibling.
|
894
|
+
def move_to_top
|
895
|
+
first_sib = first_sibling
|
896
|
+
move_to_left_of(first_sib) if first_sib && self != first_sib
|
897
|
+
end
|
898
|
+
|
899
|
+
# Moves a node one to be the last amongst its siblings. Does nothing if it's already
|
900
|
+
# the last sibling.
|
901
|
+
def move_to_bottom
|
902
|
+
last_sib = last_sibling
|
903
|
+
move_to_right_of(last_sib) if last_sib && self != last_sib
|
904
|
+
end
|
905
|
+
|
906
|
+
# Swaps the position of two sibling nodes preserving a sibling's descendants.
|
907
|
+
# The current implementation only works amongst siblings.
|
908
|
+
def swap(target, transact = true)
|
909
|
+
move_to(target, :swap, transact)
|
910
|
+
end
|
911
|
+
|
912
|
+
# Reorder children according to an array of ids
|
913
|
+
def reorder_children(*ids)
|
914
|
+
transaction do
|
915
|
+
ordered_ids = ids.flatten.uniq
|
916
|
+
current_children = children({ :conditions => { :id => ordered_ids } })
|
917
|
+
current_children_ids = current_children.map(&:id)
|
918
|
+
ordered_ids = ordered_ids & current_children_ids
|
919
|
+
return [] unless ordered_ids.length > 1 && ordered_ids != current_children_ids
|
920
|
+
perform_reorder_of_children(ordered_ids, current_children)
|
921
|
+
end
|
922
|
+
end
|
923
|
+
|
924
|
+
protected
|
925
|
+
def move_to(target, position, transact = true) #:nodoc:
|
926
|
+
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if new_record?
|
927
|
+
raise ActiveRecord::ActiveRecordError, "You cannot move a node if left or right is nil" unless self[left_col_name] && self[right_col_name]
|
928
|
+
|
929
|
+
with_optional_transaction(transact) do
|
930
|
+
self.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}") # the lft/rgt values could be stale (target is reloaded below)
|
931
|
+
if target.is_a?(base_set_class)
|
932
|
+
target.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}") # could be stale
|
933
|
+
else
|
934
|
+
target = self.class.find_in_nested_set(target) # load object if we were given an ID
|
935
|
+
end
|
936
|
+
|
937
|
+
if (target[left_col_name] >= self[left_col_name]) && (target[right_col_name] <= self[right_col_name])
|
938
|
+
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
|
939
|
+
end
|
940
|
+
|
941
|
+
# prevent moves between different trees
|
942
|
+
if target.scope_condition != scope_condition
|
943
|
+
raise ActiveRecord::ActiveRecordError, "Scope conditions do not match. Is the target in the same tree?"
|
944
|
+
end
|
945
|
+
|
946
|
+
if position == :swap
|
947
|
+
unless self.siblings.include?(target)
|
948
|
+
raise ActiveRecord::ActiveRecordError, "Impossible move, target node should be a sibling."
|
949
|
+
end
|
950
|
+
|
951
|
+
direction = (self[left_col_name] < target[left_col_name]) ? :down : :up
|
952
|
+
|
953
|
+
i0 = (direction == :up) ? target[left_col_name] : self[left_col_name]
|
954
|
+
i1 = (direction == :up) ? target[right_col_name] : self[right_col_name]
|
955
|
+
i2 = (direction == :up) ? self[left_col_name] : target[left_col_name]
|
956
|
+
i3 = (direction == :up) ? self[right_col_name] : target[right_col_name]
|
957
|
+
|
958
|
+
base_set_class.update_all(%[
|
959
|
+
#{left_col_name} = CASE WHEN #{left_col_name} BETWEEN #{i0} AND #{i1} THEN #{i3} + #{left_col_name} - #{i1}
|
960
|
+
WHEN #{left_col_name} BETWEEN #{i2} AND #{i3} THEN #{i0} + #{left_col_name} - #{i2}
|
961
|
+
ELSE #{i0} + #{i3} + #{left_col_name} - #{i1} - #{i2} END,
|
962
|
+
#{right_col_name} = CASE WHEN #{right_col_name} BETWEEN #{i0} AND #{i1} THEN #{i3} + #{right_col_name} - #{i1}
|
963
|
+
WHEN #{right_col_name} BETWEEN #{i2} AND #{i3} THEN #{i0} + #{right_col_name} - #{i2}
|
964
|
+
ELSE #{i0} + #{i3} + #{right_col_name} - #{i1} - #{i2} END ], "#{left_col_name} BETWEEN #{i0} AND #{i3} AND #{i0} < #{i1} AND #{i1} < #{i2} AND #{i2} < #{i3} AND #{scope_condition}")
|
965
|
+
else
|
966
|
+
# the move: we just need to define two adjoining segments of the left/right index and swap their positions
|
967
|
+
bound = case position
|
968
|
+
when :child then target[right_col_name]
|
969
|
+
when :left then target[left_col_name]
|
970
|
+
when :right then target[right_col_name] + 1
|
971
|
+
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left or :right ('#{position}' received)."
|
972
|
+
end
|
973
|
+
|
974
|
+
if bound > self[right_col_name]
|
975
|
+
bound = bound - 1
|
976
|
+
other_bound = self[right_col_name] + 1
|
977
|
+
else
|
978
|
+
other_bound = self[left_col_name] - 1
|
979
|
+
end
|
980
|
+
|
981
|
+
return if bound == self[right_col_name] || bound == self[left_col_name] # there would be no change, and other_bound is now wrong anyway
|
982
|
+
|
983
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
984
|
+
# so sorting puts both the intervals and their boundaries in order
|
985
|
+
a, b, c, d = [self[left_col_name], self[right_col_name], bound, other_bound].sort
|
986
|
+
|
987
|
+
# change nil to NULL for new parent
|
988
|
+
if position == :child
|
989
|
+
new_parent = target.id
|
990
|
+
else
|
991
|
+
new_parent = target[parent_col_name].nil? ? 'NULL' : target[parent_col_name]
|
992
|
+
end
|
993
|
+
|
994
|
+
base_set_class.update_all("\
|
995
|
+
#{left_col_name} = CASE \
|
996
|
+
WHEN #{left_col_name} BETWEEN #{a} AND #{b} THEN #{left_col_name} + #{d - b} \
|
997
|
+
WHEN #{left_col_name} BETWEEN #{c} AND #{d} THEN #{left_col_name} + #{a - c} \
|
998
|
+
ELSE #{left_col_name} END, \
|
999
|
+
#{right_col_name} = CASE \
|
1000
|
+
WHEN #{right_col_name} BETWEEN #{a} AND #{b} THEN #{right_col_name} + #{d - b} \
|
1001
|
+
WHEN #{right_col_name} BETWEEN #{c} AND #{d} THEN #{right_col_name} + #{a - c} \
|
1002
|
+
ELSE #{right_col_name} END, \
|
1003
|
+
#{parent_col_name} = CASE \
|
1004
|
+
WHEN #{self.class.primary_key} = #{self.id} THEN #{new_parent} \
|
1005
|
+
ELSE #{parent_col_name} END",
|
1006
|
+
scope_condition)
|
1007
|
+
end
|
1008
|
+
self.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}")
|
1009
|
+
target.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}")
|
1010
|
+
end
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
def check #:nodoc:
|
1014
|
+
# performance improvements (3X or more for tables with lots of columns) by using :select to load just id, lft and rgt
|
1015
|
+
## i don't use the scope condition here, because it shouldn't be needed
|
1016
|
+
my_children = self.class.find_in_nested_set(:all, :conditions => "#{prefixed_parent_col_name} = #{self.id}",
|
1017
|
+
:order => "#{prefixed_left_col_name}", :select => "#{self.class.primary_key}, #{prefixed_left_col_name}, #{prefixed_right_col_name}")
|
1018
|
+
|
1019
|
+
if my_children.empty?
|
1020
|
+
unless self[left_col_name] && self[right_col_name]
|
1021
|
+
raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} or #{left_col_name} is blank"
|
1022
|
+
end
|
1023
|
+
unless self[right_col_name] - self[left_col_name] == 1
|
1024
|
+
raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} should be 1 greater than #{left_col_name}"
|
1025
|
+
end
|
1026
|
+
else
|
1027
|
+
n = self[left_col_name]
|
1028
|
+
for c in (my_children) # the children come back ordered by lft
|
1029
|
+
unless c[left_col_name] && c[right_col_name]
|
1030
|
+
raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{c.id}.#{right_col_name} or #{left_col_name} is blank"
|
1031
|
+
end
|
1032
|
+
unless c[left_col_name] == n + 1
|
1033
|
+
raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{c.id}.#{left_col_name} should be 1 greater than #{n}"
|
1034
|
+
end
|
1035
|
+
c.check
|
1036
|
+
n = c[right_col_name]
|
1037
|
+
end
|
1038
|
+
unless self[right_col_name] == n + 1
|
1039
|
+
raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} should be 1 greater than #{n}"
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
# used by the renumbering methods
|
1045
|
+
def calc_numbers(n, indexes) #:nodoc:
|
1046
|
+
my_lft = n
|
1047
|
+
# performance improvements (3X or more for tables with lots of columns) by using :select to load just id, lft and rgt
|
1048
|
+
## i don't use the scope condition here, because it shouldn't be needed
|
1049
|
+
my_children = self.class.find_in_nested_set(:all, :conditions => "#{prefixed_parent_col_name} = #{self.id}",
|
1050
|
+
:order => "#{prefixed_left_col_name}", :select => "#{self.class.primary_key}, #{prefixed_left_col_name}, #{prefixed_right_col_name}")
|
1051
|
+
if my_children.empty?
|
1052
|
+
my_rgt = (n += 1)
|
1053
|
+
else
|
1054
|
+
for c in (my_children)
|
1055
|
+
n = c.calc_numbers(n + 1, indexes)
|
1056
|
+
end
|
1057
|
+
my_rgt = (n += 1)
|
1058
|
+
end
|
1059
|
+
indexes << {:id => self.id, :lft => my_lft, :rgt => my_rgt} unless self[left_col_name] == my_lft && self[right_col_name] == my_rgt
|
1060
|
+
return n
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
# Actually perform the ordering using calculated steps
|
1064
|
+
def perform_reorder_of_children(ordered_ids, current)
|
1065
|
+
steps = calculate_reorder_steps(ordered_ids, current)
|
1066
|
+
steps.inject([]) do |result, (source, idx)|
|
1067
|
+
target = current[idx]
|
1068
|
+
if source.id != target.id
|
1069
|
+
source.swap(target, false)
|
1070
|
+
from = current.index(source)
|
1071
|
+
current[from], current[idx] = current[idx], current[from]
|
1072
|
+
result << source
|
1073
|
+
end
|
1074
|
+
result
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
# Calculate the least amount of swap steps to achieve the requested order
|
1079
|
+
def calculate_reorder_steps(ordered_ids, current)
|
1080
|
+
steps = []
|
1081
|
+
current.each_with_index do |source, idx|
|
1082
|
+
new_idx = ordered_ids.index(source.id)
|
1083
|
+
steps << [source, new_idx] if idx != new_idx
|
1084
|
+
end
|
1085
|
+
steps
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
# The following code is my crude method of making things concurrency-safe.
|
1089
|
+
# Basically, we need to ensure that whenever a record is saved, the lft/rgt
|
1090
|
+
# values are _not_ written to the database, because if any changes to the tree
|
1091
|
+
# structure occurrred since the object was loaded, the lft/rgt values could
|
1092
|
+
# be out of date and corrupt the indexes.
|
1093
|
+
# There is an open ticket for this in the Rails Core: http://dev.rubyonrails.org/ticket/6896
|
1094
|
+
|
1095
|
+
private
|
1096
|
+
# override the sql preparation method to exclude the lft/rgt columns
|
1097
|
+
# under the same conditions that the primary key column is excluded
|
1098
|
+
def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) #:nodoc:
|
1099
|
+
left_and_right_column = [acts_as_nested_set_options[:left_column], acts_as_nested_set_options[:right_column]]
|
1100
|
+
quoted = {}
|
1101
|
+
connection = self.class.connection
|
1102
|
+
attribute_names.each do |name|
|
1103
|
+
if column = column_for_attribute(name)
|
1104
|
+
quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && (column.primary || left_and_right_column.include?(column.name))
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
# i couldn't figure out how to call attributes_with_quotes without cutting and pasting this private method in. :(
|
1111
|
+
# Quote strings appropriately for SQL statements.
|
1112
|
+
def quote_value(value, column = nil) #:nodoc:
|
1113
|
+
self.class.connection.quote(value, column)
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
# optionally use a transaction
|
1117
|
+
def with_optional_transaction(bool, &block)
|
1118
|
+
bool ? transaction { yield } : yield
|
1119
|
+
end
|
1120
|
+
|
1121
|
+
# as a seperate method to facilitate custom implementations based on :dependent option
|
1122
|
+
def remove_descendant(descendant)
|
1123
|
+
descendant.destroy
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
end
|
1127
|
+
end
|
1128
|
+
end
|
1129
|
+
end
|