better_nested_set 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|