better_nested_set 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +12 -0
- data/Gemfile.lock +18 -0
- data/LICENSE.txt +21 -0
- data/README.rdoc +224 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/app/helpers/better_nested_set_helper.rb +121 -0
- data/better_nested_set.gemspec +75 -0
- data/lib/better_nested_set.rb +16 -0
- data/lib/symetrie_com/acts_as_better_nested_set.rb +1130 -0
- data/pkg/better_nested_set-0.1.0.gem +0 -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 +141 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{better_nested_set}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Chris Bailey", "Jean-Christophe Michel", "Dirk Breuer"]
|
12
|
+
s.date = %q{2011-03-11}
|
13
|
+
s.description = %q{This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the object-relational mapping layer of the framework Ruby on Rails. The original nested set in Rails lacks many important features, such as moving branches within a tree.}
|
14
|
+
s.email = %q{dirk.breuer@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"Gemfile",
|
21
|
+
"Gemfile.lock",
|
22
|
+
"LICENSE.txt",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"app/helpers/better_nested_set_helper.rb",
|
27
|
+
"better_nested_set.gemspec",
|
28
|
+
"lib/better_nested_set.rb",
|
29
|
+
"lib/symetrie_com/acts_as_better_nested_set.rb",
|
30
|
+
"pkg/better_nested_set-0.1.0.gem",
|
31
|
+
"test/RUNNING_UNIT_TESTS",
|
32
|
+
"test/abstract_unit.rb",
|
33
|
+
"test/acts_as_nested_set_test.rb",
|
34
|
+
"test/database.yml",
|
35
|
+
"test/fixtures/mixin.rb",
|
36
|
+
"test/fixtures/mixins.yml",
|
37
|
+
"test/mysql.rb",
|
38
|
+
"test/postgresql.rb",
|
39
|
+
"test/schema.rb",
|
40
|
+
"test/sqlite3.rb"
|
41
|
+
]
|
42
|
+
s.homepage = %q{http://github.com/railsbros-dirk/better_nested_set}
|
43
|
+
s.licenses = ["MIT"]
|
44
|
+
s.require_paths = ["lib"]
|
45
|
+
s.rubygems_version = %q{1.5.0}
|
46
|
+
s.summary = %q{This plugin provides an ehanced acts_as_nested_set mixin for ActiveRecord}
|
47
|
+
s.test_files = [
|
48
|
+
"test/abstract_unit.rb",
|
49
|
+
"test/acts_as_nested_set_test.rb",
|
50
|
+
"test/fixtures/mixin.rb",
|
51
|
+
"test/mysql.rb",
|
52
|
+
"test/postgresql.rb",
|
53
|
+
"test/schema.rb",
|
54
|
+
"test/sqlite3.rb"
|
55
|
+
]
|
56
|
+
|
57
|
+
if s.respond_to? :specification_version then
|
58
|
+
s.specification_version = 3
|
59
|
+
|
60
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
61
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
62
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
63
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
64
|
+
else
|
65
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
66
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
67
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
71
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
72
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SymetrieCom
|
2
|
+
|
3
|
+
if Rails.version >= "3.0.0"
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
end
|
6
|
+
else
|
7
|
+
ActiveSupport::Dependencies.load_paths << File.join(File.dirname(__FILE__), '..', 'app', "helpers")
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
require "symetrie_com/acts_as_better_nested_set"
|
13
|
+
|
14
|
+
ActiveRecord::Base.class_eval do
|
15
|
+
include SymetrieCom::Acts::NestedSet
|
16
|
+
end
|
@@ -0,0 +1,1130 @@
|
|
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
|
1130
|
+
|