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 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
@@ -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