closure_tree 3.10.2 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +10 -0
- data/lib/closure_tree/acts_as_tree.rb +9 -38
- data/lib/closure_tree/deterministic_ordering.rb +5 -35
- data/lib/closure_tree/model.rb +77 -83
- data/lib/closure_tree/numeric_deterministic_ordering.rb +21 -21
- data/lib/closure_tree/support.rb +193 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/closure_tree/with_advisory_lock.rb +16 -6
- data/spec/namespace_type_spec.rb +2 -2
- data/spec/parallel_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/tag_spec.rb +4 -4
- data/spec/user_spec.rb +2 -2
- metadata +7 -7
- data/lib/closure_tree/columns.rb +0 -127
data/README.md
CHANGED
@@ -444,6 +444,16 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
|
|
444
444
|
|
445
445
|
## Change log
|
446
446
|
|
447
|
+
### 4.0.0
|
448
|
+
|
449
|
+
* Moved all of closure_tree's implementation-detail methods into a ```ClosureTree::Support```
|
450
|
+
instance, which removes almost all of the namespace pollution in your models that wasn't
|
451
|
+
for normal consumption. If you were using any of these methods, they're now available through
|
452
|
+
the "_ct" class and instance member.
|
453
|
+
|
454
|
+
*This change may break consumers*, so I incremented the major version number, even though no new
|
455
|
+
functionality was released.
|
456
|
+
|
447
457
|
### 3.10.2
|
448
458
|
|
449
459
|
* Prevent faulty SQL statement when ```#siblings``` is called on an unsaved records.
|
@@ -1,55 +1,26 @@
|
|
1
|
-
require 'closure_tree/
|
2
|
-
require 'closure_tree/deterministic_ordering'
|
1
|
+
require 'closure_tree/support'
|
3
2
|
require 'closure_tree/model'
|
3
|
+
require 'closure_tree/deterministic_ordering'
|
4
4
|
require 'closure_tree/numeric_deterministic_ordering'
|
5
5
|
require 'closure_tree/with_advisory_lock'
|
6
6
|
|
7
7
|
module ClosureTree
|
8
8
|
module ActsAsTree
|
9
9
|
def acts_as_tree(options = {})
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
self.closure_tree_options = {
|
14
|
-
:ct_base_class => self,
|
15
|
-
:parent_column_name => 'parent_id',
|
16
|
-
:dependent => :nullify, # or :destroy or :delete_all -- see the README
|
17
|
-
:name_column => 'name',
|
18
|
-
:with_advisory_lock => true
|
19
|
-
}.merge(options)
|
20
|
-
|
21
|
-
raise IllegalArgumentException, "name_column can't be 'path'" if closure_tree_options[:name_column] == 'path'
|
22
|
-
|
23
|
-
include ClosureTree::Columns
|
24
|
-
extend ClosureTree::Columns
|
25
|
-
|
26
|
-
include ClosureTree::WithAdvisoryLock
|
27
|
-
extend ClosureTree::WithAdvisoryLock
|
10
|
+
class_attribute :_ct
|
11
|
+
self._ct = ClosureTree::Support.new(self, options)
|
28
12
|
|
29
13
|
# Auto-inject the hierarchy table
|
30
14
|
# See https://github.com/patshaughnessy/class_factory/blob/master/lib/class_factory/class_factory.rb
|
31
15
|
class_attribute :hierarchy_class
|
32
|
-
self.hierarchy_class =
|
33
|
-
|
34
|
-
self.hierarchy_class.class_eval <<-RUBY
|
35
|
-
belongs_to :ancestor, :class_name => "#{ct_class.to_s}"
|
36
|
-
belongs_to :descendant, :class_name => "#{ct_class.to_s}"
|
37
|
-
attr_accessible :ancestor, :descendant, :generations
|
38
|
-
def ==(other)
|
39
|
-
self.class == other.class && ancestor == other.ancestor && descendant == other.descendant
|
40
|
-
end
|
41
|
-
alias :eql? :==
|
42
|
-
def hash
|
43
|
-
ancestor_id.hash << 31 ^ descendant_id.hash
|
44
|
-
end
|
45
|
-
RUBY
|
46
|
-
|
47
|
-
self.hierarchy_class.table_name = hierarchy_table_name
|
16
|
+
self.hierarchy_class = _ct.hierarchy_class_for_model
|
48
17
|
|
49
18
|
include ClosureTree::Model
|
50
|
-
|
19
|
+
include ClosureTree::WithAdvisoryLock
|
20
|
+
|
21
|
+
if _ct.order_option
|
51
22
|
include ClosureTree::DeterministicOrdering
|
52
|
-
include ClosureTree::DeterministicNumericOrdering if order_is_numeric
|
23
|
+
include ClosureTree::DeterministicNumericOrdering if _ct.order_is_numeric?
|
53
24
|
end
|
54
25
|
end
|
55
26
|
end
|
@@ -1,49 +1,19 @@
|
|
1
1
|
module ClosureTree
|
2
2
|
module DeterministicOrdering
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
module ClassAndInstanceMethods
|
6
|
-
def order_column
|
7
|
-
o = order_option
|
8
|
-
o.split(' ', 2).first if o
|
9
|
-
end
|
10
|
-
|
11
|
-
def require_order_column
|
12
|
-
raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
|
13
|
-
end
|
14
|
-
|
15
|
-
def order_column_sym
|
16
|
-
require_order_column
|
17
|
-
order_column.to_sym
|
18
|
-
end
|
19
|
-
|
20
|
-
def quoted_order_column(include_table_name = true)
|
21
|
-
require_order_column
|
22
|
-
prefix = include_table_name ? "#{quoted_table_name}." : ""
|
23
|
-
"#{prefix}#{connection.quote_column_name(order_column)}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
include ClassAndInstanceMethods
|
28
|
-
|
29
|
-
module ClassMethods
|
30
|
-
include ClassAndInstanceMethods
|
31
|
-
end
|
32
|
-
|
33
3
|
def order_value
|
34
|
-
read_attribute(order_column_sym)
|
4
|
+
read_attribute(_ct.order_column_sym)
|
35
5
|
end
|
36
6
|
|
37
7
|
def order_value=(new_order_value)
|
38
|
-
write_attribute(order_column_sym, new_order_value)
|
8
|
+
write_attribute(_ct.order_column_sym, new_order_value)
|
39
9
|
end
|
40
10
|
|
41
11
|
def siblings_before
|
42
|
-
siblings.where(["#{quoted_order_column} < ?", order_value])
|
12
|
+
siblings.where(["#{_ct.quoted_order_column} < ?", order_value])
|
43
13
|
end
|
44
14
|
|
45
15
|
def siblings_after
|
46
|
-
siblings.where(["#{quoted_order_column} > ?", order_value])
|
16
|
+
siblings.where(["#{_ct.quoted_order_column} > ?", order_value])
|
47
17
|
end
|
48
18
|
end
|
49
|
-
end
|
19
|
+
end
|
data/lib/closure_tree/model.rb
CHANGED
@@ -11,45 +11,50 @@ module ClosureTree
|
|
11
11
|
before_destroy :ct_before_destroy
|
12
12
|
|
13
13
|
belongs_to :parent,
|
14
|
-
:class_name =>
|
15
|
-
:foreign_key => parent_column_name
|
14
|
+
:class_name => _ct.model_class.to_s,
|
15
|
+
:foreign_key => _ct.parent_column_name
|
16
16
|
|
17
17
|
unless defined?(ActiveModel::ForbiddenAttributesProtection) && ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
|
18
18
|
attr_accessible :parent
|
19
19
|
end
|
20
20
|
|
21
|
-
has_many :children, with_order_option(
|
22
|
-
:class_name =>
|
23
|
-
:foreign_key => parent_column_name,
|
24
|
-
:dependent =>
|
25
|
-
)
|
21
|
+
has_many :children, _ct.with_order_option(
|
22
|
+
:class_name => _ct.model_class.to_s,
|
23
|
+
:foreign_key => _ct.parent_column_name,
|
24
|
+
:dependent => _ct.options[:dependent])
|
26
25
|
|
27
26
|
has_many :ancestor_hierarchies,
|
28
|
-
:class_name => hierarchy_class_name,
|
27
|
+
:class_name => _ct.hierarchy_class_name,
|
29
28
|
:foreign_key => "descendant_id",
|
30
|
-
:order => "#{quoted_hierarchy_table_name}.generations asc"
|
29
|
+
:order => "#{_ct.quoted_hierarchy_table_name}.generations asc"
|
31
30
|
|
32
31
|
has_many :self_and_ancestors,
|
33
32
|
:through => :ancestor_hierarchies,
|
34
33
|
:source => :ancestor,
|
35
|
-
:order => "#{quoted_hierarchy_table_name}.generations asc"
|
34
|
+
:order => "#{_ct.quoted_hierarchy_table_name}.generations asc"
|
36
35
|
|
37
36
|
has_many :descendant_hierarchies,
|
38
|
-
:class_name => hierarchy_class_name,
|
37
|
+
:class_name => _ct.hierarchy_class_name,
|
39
38
|
:foreign_key => "ancestor_id",
|
40
|
-
:order => "#{quoted_hierarchy_table_name}.generations asc"
|
39
|
+
:order => "#{_ct.quoted_hierarchy_table_name}.generations asc"
|
40
|
+
|
41
41
|
# TODO: FIXME: this collection currently ignores sort_order
|
42
42
|
# (because the quoted_table_named would need to be joined in to get to the order column)
|
43
43
|
|
44
|
-
has_many :self_and_descendants,
|
44
|
+
has_many :self_and_descendants, _ct.with_order_option(
|
45
45
|
:through => :descendant_hierarchies,
|
46
46
|
:source => :descendant,
|
47
|
-
:order =>
|
47
|
+
:order => "#{_ct.quoted_hierarchy_table_name}.generations asc")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Delegate to the Support instance on the class:
|
51
|
+
def _ct
|
52
|
+
self.class._ct
|
48
53
|
end
|
49
54
|
|
50
55
|
# Returns true if this node has no parents.
|
51
56
|
def root?
|
52
|
-
|
57
|
+
parent_id.nil?
|
53
58
|
end
|
54
59
|
|
55
60
|
# Returns true if this node has a parent, and is not a root.
|
@@ -64,7 +69,7 @@ module ClosureTree
|
|
64
69
|
|
65
70
|
# Returns the farthest ancestor, or self if +root?+
|
66
71
|
def root
|
67
|
-
self_and_ancestors.where(parent_column_name.to_sym => nil).first
|
72
|
+
self_and_ancestors.where(_ct.parent_column_name.to_sym => nil).first
|
68
73
|
end
|
69
74
|
|
70
75
|
def leaves
|
@@ -82,18 +87,18 @@ module ClosureTree
|
|
82
87
|
end
|
83
88
|
|
84
89
|
def ancestor_ids
|
85
|
-
ids_from(ancestors)
|
90
|
+
_ct.ids_from(ancestors)
|
86
91
|
end
|
87
92
|
|
88
93
|
# Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
|
89
94
|
# to the +name_column+.
|
90
95
|
# (so child.ancestry_path == +%w{grandparent parent child}+
|
91
|
-
def ancestry_path(to_s_column = name_column)
|
96
|
+
def ancestry_path(to_s_column = _ct.name_column)
|
92
97
|
self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
|
93
98
|
end
|
94
99
|
|
95
100
|
def child_ids
|
96
|
-
ids_from(children)
|
101
|
+
_ct.ids_from(children)
|
97
102
|
end
|
98
103
|
|
99
104
|
def descendants
|
@@ -101,12 +106,12 @@ module ClosureTree
|
|
101
106
|
end
|
102
107
|
|
103
108
|
def descendant_ids
|
104
|
-
ids_from(descendants)
|
109
|
+
_ct.ids_from(descendants)
|
105
110
|
end
|
106
111
|
|
107
112
|
def self_and_siblings
|
108
|
-
s =
|
109
|
-
order_option.present? ? s.order(quoted_order_column) : s
|
113
|
+
s = _ct.base_class.where(_ct.parent_column_sym => parent)
|
114
|
+
_ct.order_option.present? ? s.order(_ct.quoted_order_column) : s
|
110
115
|
end
|
111
116
|
|
112
117
|
def siblings
|
@@ -114,7 +119,7 @@ module ClosureTree
|
|
114
119
|
end
|
115
120
|
|
116
121
|
def sibling_ids
|
117
|
-
ids_from(siblings)
|
122
|
+
_ct.ids_from(siblings)
|
118
123
|
end
|
119
124
|
|
120
125
|
# Alias for appending to the children collection.
|
@@ -127,8 +132,8 @@ module ClosureTree
|
|
127
132
|
# Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
|
128
133
|
def find_by_path(path)
|
129
134
|
return self if path.empty?
|
130
|
-
parent_constraint = "#{quoted_parent_column_name} = #{
|
131
|
-
|
135
|
+
parent_constraint = "#{_ct.quoted_parent_column_name} = #{_ct.quote(id)}"
|
136
|
+
self.class.ct_scoped_to_path(path, parent_constraint).first
|
132
137
|
end
|
133
138
|
|
134
139
|
# Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
|
@@ -139,8 +144,8 @@ module ClosureTree
|
|
139
144
|
child_name = subpath.shift
|
140
145
|
return self unless child_name
|
141
146
|
child = transaction do
|
142
|
-
attrs = {name_sym => child_name}
|
143
|
-
attrs[:type] = self.type if
|
147
|
+
attrs = {_ct.name_sym => child_name}
|
148
|
+
attrs[:type] = self.type if _ct.subclass? && _ct.has_type?
|
144
149
|
self.children.where(attrs).first || begin
|
145
150
|
child = self.class.new(attributes.merge(attrs))
|
146
151
|
self.children << child
|
@@ -153,22 +158,22 @@ module ClosureTree
|
|
153
158
|
end
|
154
159
|
|
155
160
|
def find_all_by_generation(generation_level)
|
156
|
-
s =
|
161
|
+
s = _ct.base_class.joins(<<-SQL)
|
157
162
|
INNER JOIN (
|
158
163
|
SELECT descendant_id
|
159
|
-
FROM #{quoted_hierarchy_table_name}
|
160
|
-
WHERE ancestor_id = #{
|
164
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
165
|
+
WHERE ancestor_id = #{_ct.quote(self.id)}
|
161
166
|
GROUP BY 1
|
162
|
-
HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
|
163
|
-
) AS descendants ON (#{quoted_table_name}.#{
|
167
|
+
HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
|
168
|
+
) AS descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
|
164
169
|
SQL
|
165
|
-
|
170
|
+
_ct.scope_with_order(s)
|
166
171
|
end
|
167
172
|
|
168
173
|
def hash_tree_scope(limit_depth = nil)
|
169
174
|
scope = self_and_descendants
|
170
175
|
if limit_depth
|
171
|
-
scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
|
176
|
+
scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
|
172
177
|
else
|
173
178
|
scope
|
174
179
|
end
|
@@ -178,15 +183,15 @@ module ClosureTree
|
|
178
183
|
self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
|
179
184
|
end
|
180
185
|
|
181
|
-
def
|
182
|
-
read_attribute(parent_column_sym)
|
186
|
+
def parent_id
|
187
|
+
read_attribute(_ct.parent_column_sym)
|
183
188
|
end
|
184
189
|
|
185
190
|
def ct_validate
|
186
|
-
if changes[parent_column_name] &&
|
191
|
+
if changes[_ct.parent_column_name] &&
|
187
192
|
parent.present? &&
|
188
193
|
parent.self_and_ancestors.include?(self)
|
189
|
-
errors.add(parent_column_sym, "You cannot add an ancestor as a descendant")
|
194
|
+
errors.add(_ct.parent_column_sym, "You cannot add an ancestor as a descendant")
|
190
195
|
end
|
191
196
|
end
|
192
197
|
|
@@ -196,7 +201,7 @@ module ClosureTree
|
|
196
201
|
end
|
197
202
|
|
198
203
|
def ct_after_save
|
199
|
-
rebuild! if changes[parent_column_name] || @was_new_record
|
204
|
+
rebuild! if changes[_ct.parent_column_name] || @was_new_record
|
200
205
|
@was_new_record = false # we aren't new anymore.
|
201
206
|
true # don't cancel anything.
|
202
207
|
end
|
@@ -207,11 +212,11 @@ module ClosureTree
|
|
207
212
|
hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
|
208
213
|
unless root?
|
209
214
|
sql = <<-SQL
|
210
|
-
INSERT INTO #{quoted_hierarchy_table_name}
|
215
|
+
INSERT INTO #{_ct.quoted_hierarchy_table_name}
|
211
216
|
(ancestor_id, descendant_id, generations)
|
212
|
-
SELECT x.ancestor_id, #{
|
213
|
-
FROM #{quoted_hierarchy_table_name} x
|
214
|
-
WHERE x.descendant_id = #{
|
217
|
+
SELECT x.ancestor_id, #{_ct.quote(id)}, x.generations + 1
|
218
|
+
FROM #{_ct.quoted_hierarchy_table_name} x
|
219
|
+
WHERE x.descendant_id = #{_ct.quote(self.parent_id)}
|
215
220
|
SQL
|
216
221
|
connection.execute sql.strip
|
217
222
|
end
|
@@ -221,7 +226,7 @@ module ClosureTree
|
|
221
226
|
|
222
227
|
def ct_before_destroy
|
223
228
|
delete_hierarchy_references
|
224
|
-
if
|
229
|
+
if _ct.options[:dependent] == :nullify
|
225
230
|
children.each { |c| c.rebuild! }
|
226
231
|
end
|
227
232
|
end
|
@@ -232,36 +237,25 @@ module ClosureTree
|
|
232
237
|
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
233
238
|
# Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
|
234
239
|
connection.execute <<-SQL
|
235
|
-
DELETE FROM #{quoted_hierarchy_table_name}
|
240
|
+
DELETE FROM #{_ct.quoted_hierarchy_table_name}
|
236
241
|
WHERE descendant_id IN (
|
237
242
|
SELECT DISTINCT descendant_id
|
238
|
-
FROM (
|
239
|
-
FROM #{quoted_hierarchy_table_name}
|
240
|
-
WHERE ancestor_id = #{
|
243
|
+
FROM (SELECT descendant_id
|
244
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
245
|
+
WHERE ancestor_id = #{_ct.quote(id)}
|
241
246
|
) AS x )
|
242
|
-
OR descendant_id = #{
|
247
|
+
OR descendant_id = #{_ct.quote(id)}
|
243
248
|
SQL
|
244
249
|
end
|
245
250
|
|
246
251
|
def without_self(scope)
|
247
252
|
return scope if self.new_record?
|
248
|
-
scope.where(["#{quoted_table_name}.#{
|
249
|
-
end
|
250
|
-
|
251
|
-
def ids_from(scope)
|
252
|
-
if scope.respond_to? :pluck
|
253
|
-
scope.pluck(:id)
|
254
|
-
else
|
255
|
-
scope.select(:id).collect(&:id)
|
256
|
-
end
|
253
|
+
scope.where(["#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} != ?", self])
|
257
254
|
end
|
258
255
|
|
259
|
-
# TODO: _parent_id will be removed in the next major version
|
260
|
-
alias :_parent_id :ct_parent_id
|
261
|
-
|
262
256
|
module ClassMethods
|
263
257
|
def roots
|
264
|
-
scope_with_order(where(parent_column_name => nil))
|
258
|
+
_ct.scope_with_order(where(_ct.parent_column_name => nil))
|
265
259
|
end
|
266
260
|
|
267
261
|
# Returns an arbitrary node that has no parents.
|
@@ -279,12 +273,12 @@ module ClosureTree
|
|
279
273
|
s = joins(<<-SQL)
|
280
274
|
INNER JOIN (
|
281
275
|
SELECT ancestor_id
|
282
|
-
FROM #{quoted_hierarchy_table_name}
|
276
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
283
277
|
GROUP BY 1
|
284
|
-
HAVING MAX(#{quoted_hierarchy_table_name}.generations) = 0
|
285
|
-
) AS leaves ON (#{quoted_table_name}.#{primary_key} = leaves.ancestor_id)
|
278
|
+
HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
|
279
|
+
) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
|
286
280
|
SQL
|
287
|
-
|
281
|
+
_ct.scope_with_order(s)
|
288
282
|
end
|
289
283
|
|
290
284
|
# Rebuilds the hierarchy table based on the parent_id column in the database.
|
@@ -301,39 +295,39 @@ module ClosureTree
|
|
301
295
|
s = joins(<<-SQL)
|
302
296
|
INNER JOIN (
|
303
297
|
SELECT #{primary_key} as root_id
|
304
|
-
FROM #{quoted_table_name}
|
305
|
-
WHERE #{quoted_parent_column_name} IS NULL
|
298
|
+
FROM #{_ct.quoted_table_name}
|
299
|
+
WHERE #{_ct.quoted_parent_column_name} IS NULL
|
306
300
|
) AS roots ON (1 = 1)
|
307
301
|
INNER JOIN (
|
308
302
|
SELECT ancestor_id, descendant_id
|
309
|
-
FROM #{quoted_hierarchy_table_name}
|
303
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
310
304
|
GROUP BY 1, 2
|
311
305
|
HAVING MAX(generations) = #{generation_level.to_i}
|
312
306
|
) AS descendants ON (
|
313
|
-
#{quoted_table_name}.#{primary_key} = descendants.descendant_id
|
307
|
+
#{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
|
314
308
|
AND roots.root_id = descendants.ancestor_id
|
315
309
|
)
|
316
310
|
SQL
|
317
|
-
|
311
|
+
_ct.scope_with_order(s)
|
318
312
|
end
|
319
313
|
|
320
314
|
# Find the node whose +ancestry_path+ is +path+
|
321
315
|
def find_by_path(path)
|
322
|
-
parent_constraint = "#{quoted_parent_column_name} IS NULL"
|
316
|
+
parent_constraint = "#{_ct.quoted_parent_column_name} IS NULL"
|
323
317
|
ct_scoped_to_path(path, parent_constraint).first
|
324
318
|
end
|
325
319
|
|
326
320
|
def ct_scoped_to_path(path, parent_constraint)
|
327
321
|
path = path.is_a?(Enumerable) ? path.dup : [path]
|
328
|
-
scope = scoped.where(name_sym => path.last).readonly(false)
|
322
|
+
scope = scoped.where(_ct.name_sym => path.last).readonly(false)
|
329
323
|
path[0..-2].reverse.each_with_index do |ea, idx|
|
330
|
-
subtable = idx == 0 ? quoted_table_name : "p#{idx - 1}"
|
324
|
+
subtable = idx == 0 ? _ct.quoted_table_name : "p#{idx - 1}"
|
331
325
|
scope = scope.joins(<<-SQL)
|
332
|
-
INNER JOIN #{quoted_table_name} AS p#{idx} ON p#{idx}.id = #{subtable}.#{parent_column_name}
|
326
|
+
INNER JOIN #{_ct.quoted_table_name} AS p#{idx} ON p#{idx}.id = #{subtable}.#{_ct.parent_column_name}
|
333
327
|
SQL
|
334
|
-
scope = scope.where("p#{idx}.#{quoted_name_column} = #{
|
328
|
+
scope = scope.where("p#{idx}.#{_ct.quoted_name_column} = #{_ct.quote(ea)}")
|
335
329
|
end
|
336
|
-
root_table_name = path.size > 1 ? "p#{path.size - 2}" : quoted_table_name
|
330
|
+
root_table_name = path.size > 1 ? "p#{path.size - 2}" : _ct.quoted_table_name
|
337
331
|
scope.where("#{root_table_name}.#{parent_constraint}")
|
338
332
|
end
|
339
333
|
|
@@ -345,8 +339,8 @@ module ClosureTree
|
|
345
339
|
ct_with_advisory_lock do
|
346
340
|
# shenanigans because find_or_create can't infer we want the same class as this:
|
347
341
|
# Note that roots will already be constrained to this subclass (in the case of polymorphism):
|
348
|
-
root = roots.where(name_sym => root_name).first
|
349
|
-
root ||= create!(attributes.merge(name_sym => root_name))
|
342
|
+
root = roots.where(_ct.name_sym => root_name).first
|
343
|
+
root ||= create!(attributes.merge(_ct.name_sym => root_name))
|
350
344
|
root.find_or_create_by_path(subpath, attributes)
|
351
345
|
end
|
352
346
|
end
|
@@ -358,13 +352,13 @@ module ClosureTree
|
|
358
352
|
generation_depth = <<-SQL
|
359
353
|
INNER JOIN (
|
360
354
|
SELECT descendant_id, MAX(generations) as depth
|
361
|
-
FROM #{quoted_hierarchy_table_name}
|
355
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
362
356
|
GROUP BY descendant_id
|
363
357
|
#{limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ""}
|
364
358
|
) AS generation_depth
|
365
|
-
ON #{quoted_table_name}.#{primary_key} = generation_depth.descendant_id
|
359
|
+
ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
|
366
360
|
SQL
|
367
|
-
|
361
|
+
_ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
|
368
362
|
end
|
369
363
|
|
370
364
|
# Builds nested hash structure using the scope returned from the passed in scope
|
@@ -377,7 +371,7 @@ module ClosureTree
|
|
377
371
|
if ea.root? || tree.empty? # We're at the top of the tree.
|
378
372
|
tree[ea] = h
|
379
373
|
else
|
380
|
-
id_to_hash[ea.
|
374
|
+
id_to_hash[ea.parent_id][ea] = h
|
381
375
|
end
|
382
376
|
end
|
383
377
|
tree
|
@@ -9,21 +9,21 @@ module ClosureTree
|
|
9
9
|
SELECT
|
10
10
|
count(*) as total_descendants,
|
11
11
|
max(generations) as max_depth
|
12
|
-
FROM #{quoted_hierarchy_table_name}
|
13
|
-
WHERE ancestor_id = #{
|
12
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
13
|
+
WHERE ancestor_id = #{_ct.quote(self.id)}
|
14
14
|
SQL
|
15
15
|
join_sql = <<-SQL
|
16
|
-
JOIN #{quoted_hierarchy_table_name} anc_hier
|
17
|
-
ON anc_hier.descendant_id = #{quoted_hierarchy_table_name}.descendant_id
|
18
|
-
JOIN #{quoted_table_name} anc
|
16
|
+
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
17
|
+
ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
|
18
|
+
JOIN #{_ct.quoted_table_name} anc
|
19
19
|
ON anc.id = anc_hier.ancestor_id
|
20
|
-
JOIN #{quoted_hierarchy_table_name} depths
|
21
|
-
ON depths.ancestor_id = #{
|
20
|
+
JOIN #{_ct.quoted_hierarchy_table_name} depths
|
21
|
+
ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.id
|
22
22
|
SQL
|
23
|
-
node_score = "(1 + anc.#{quoted_order_column(false)}) * " +
|
23
|
+
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
|
24
24
|
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.generations)"
|
25
25
|
order_by = "sum(#{node_score})"
|
26
|
-
self_and_descendants.joins(join_sql).group("#{quoted_table_name}.id").reorder(order_by)
|
26
|
+
self_and_descendants.joins(join_sql).group("#{_ct.quoted_table_name}.id").reorder(order_by)
|
27
27
|
end
|
28
28
|
|
29
29
|
module ClassMethods
|
@@ -32,23 +32,23 @@ module ClosureTree
|
|
32
32
|
SELECT
|
33
33
|
count(*) as total_descendants,
|
34
34
|
max(generations) as max_depth
|
35
|
-
FROM #{quoted_hierarchy_table_name}
|
35
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
36
36
|
SQL
|
37
37
|
join_sql = <<-SQL
|
38
|
-
JOIN #{quoted_hierarchy_table_name} anc_hier
|
39
|
-
ON anc_hier.descendant_id = #{quoted_table_name}.id
|
40
|
-
JOIN #{quoted_table_name} anc
|
38
|
+
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
39
|
+
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.id
|
40
|
+
JOIN #{_ct.quoted_table_name} anc
|
41
41
|
ON anc.id = anc_hier.ancestor_id
|
42
42
|
JOIN (
|
43
43
|
SELECT descendant_id, max(generations) AS max_depth
|
44
|
-
FROM #{quoted_hierarchy_table_name}
|
44
|
+
FROM #{_ct.quoted_hierarchy_table_name}
|
45
45
|
GROUP BY 1
|
46
46
|
) AS depths ON depths.descendant_id = anc.id
|
47
47
|
SQL
|
48
|
-
node_score = "(1 + anc.#{quoted_order_column(false)}) * " +
|
48
|
+
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
|
49
49
|
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.max_depth)"
|
50
50
|
order_by = "sum(#{node_score})"
|
51
|
-
joins(join_sql).group("#{quoted_table_name}.id").reorder(order_by)
|
51
|
+
joins(join_sql).group("#{_ct.quoted_table_name}.id").reorder(order_by)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -69,12 +69,12 @@ module ClosureTree
|
|
69
69
|
sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
|
70
70
|
# We need to incr the before_siblings to make room for sibling_node:
|
71
71
|
if use_update_all
|
72
|
-
col = quoted_order_column(false)
|
72
|
+
col = _ct.quoted_order_column(false)
|
73
73
|
# issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
|
74
|
-
|
74
|
+
_ct.base_class.update_all(
|
75
75
|
["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
|
76
|
-
["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
|
77
|
-
|
76
|
+
["#{_ct.quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
|
77
|
+
parent_id,
|
78
78
|
sibling_node.order_value])
|
79
79
|
else
|
80
80
|
last_value = sibling_node.order_value.to_i
|
@@ -90,4 +90,4 @@ module ClosureTree
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
end
|
93
|
-
end
|
93
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module ClosureTree
|
2
|
+
class Support
|
3
|
+
|
4
|
+
attr_reader :model_class
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
def initialize(model_class, options)
|
8
|
+
@model_class = model_class
|
9
|
+
@options = {
|
10
|
+
:base_class => model_class,
|
11
|
+
:parent_column_name => 'parent_id',
|
12
|
+
:dependent => :nullify, # or :destroy or :delete_all -- see the README
|
13
|
+
:name_column => 'name',
|
14
|
+
:with_advisory_lock => true
|
15
|
+
}.merge(options)
|
16
|
+
|
17
|
+
raise IllegalArgumentException, "name_column can't be 'path'" if options[:name_column] == 'path'
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def connection
|
22
|
+
model_class.connection
|
23
|
+
end
|
24
|
+
|
25
|
+
def hierarchy_class_for_model
|
26
|
+
hierarchy_class = model_class.parent.const_set(short_hierarchy_class_name, Class.new(ActiveRecord::Base))
|
27
|
+
hierarchy_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
28
|
+
belongs_to :ancestor, :class_name => "#{model_class}"
|
29
|
+
belongs_to :descendant, :class_name => "#{model_class}"
|
30
|
+
attr_accessible :ancestor, :descendant, :generations
|
31
|
+
def ==(other)
|
32
|
+
self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
|
33
|
+
end
|
34
|
+
alias :eql? :==
|
35
|
+
def hash
|
36
|
+
ancestor_id.hash << 31 ^ descendant_id.hash
|
37
|
+
end
|
38
|
+
RUBY
|
39
|
+
hierarchy_class.table_name = hierarchy_table_name
|
40
|
+
hierarchy_class
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent_column_name
|
44
|
+
options[:parent_column_name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def parent_column_sym
|
48
|
+
parent_column_name.to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
def has_name?
|
52
|
+
model_class.new.attributes.include? options[:name_column]
|
53
|
+
end
|
54
|
+
|
55
|
+
def name_column
|
56
|
+
options[:name_column]
|
57
|
+
end
|
58
|
+
|
59
|
+
def name_sym
|
60
|
+
name_column.to_sym
|
61
|
+
end
|
62
|
+
|
63
|
+
def hierarchy_table_name
|
64
|
+
# We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
|
65
|
+
# because they may have overridden the table name, which is what we want to be consistent with
|
66
|
+
# in order for the schema to make sense.
|
67
|
+
tablename = options[:hierarchy_table_name] ||
|
68
|
+
remove_prefix_and_suffix(table_name).singularize + "_hierarchies"
|
69
|
+
|
70
|
+
ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
|
71
|
+
end
|
72
|
+
|
73
|
+
def hierarchy_class_name
|
74
|
+
options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the constant name of the hierarchy_class
|
78
|
+
#
|
79
|
+
# @return [String] the constant name
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# Namespace::Model.hierarchy_class_name # => "Namespace::ModelHierarchy"
|
83
|
+
# Namespace::Model.short_hierarchy_class_name # => "ModelHierarchy"
|
84
|
+
def short_hierarchy_class_name
|
85
|
+
hierarchy_class_name.split('::').last
|
86
|
+
end
|
87
|
+
|
88
|
+
def quoted_hierarchy_table_name
|
89
|
+
connection.quote_table_name hierarchy_table_name
|
90
|
+
end
|
91
|
+
|
92
|
+
def quoted_parent_column_name
|
93
|
+
connection.quote_column_name parent_column_name
|
94
|
+
end
|
95
|
+
|
96
|
+
def quoted_name_column
|
97
|
+
connection.quote_column_name name_column
|
98
|
+
end
|
99
|
+
|
100
|
+
def quote(field)
|
101
|
+
connection.quote(field)
|
102
|
+
end
|
103
|
+
|
104
|
+
def order_option
|
105
|
+
options[:order]
|
106
|
+
end
|
107
|
+
|
108
|
+
def with_order_option(options)
|
109
|
+
order_option ? options.merge(:order => order_option) : options
|
110
|
+
end
|
111
|
+
|
112
|
+
def scope_with_order(scope, additional_order_by = nil)
|
113
|
+
if order_option
|
114
|
+
scope.order(*([additional_order_by, order_option].compact))
|
115
|
+
else
|
116
|
+
scope
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def with_order_option(options)
|
121
|
+
if order_option
|
122
|
+
options[:order] = [options[:order], order_option].compact.join(",")
|
123
|
+
end
|
124
|
+
options
|
125
|
+
end
|
126
|
+
|
127
|
+
def order_is_numeric?
|
128
|
+
# The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
|
129
|
+
return false if order_option.nil? || !model_class.table_exists?
|
130
|
+
c = model_class.columns_hash[order_option]
|
131
|
+
c && c.type == :integer
|
132
|
+
end
|
133
|
+
|
134
|
+
def order_column
|
135
|
+
o = order_option
|
136
|
+
o.split(' ', 2).first if o
|
137
|
+
end
|
138
|
+
|
139
|
+
def require_order_column
|
140
|
+
raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
|
141
|
+
end
|
142
|
+
|
143
|
+
def order_column_sym
|
144
|
+
require_order_column
|
145
|
+
order_column.to_sym
|
146
|
+
end
|
147
|
+
|
148
|
+
def quoted_order_column(include_table_name = true)
|
149
|
+
require_order_column
|
150
|
+
prefix = include_table_name ? "#{quoted_table_name}." : ""
|
151
|
+
"#{prefix}#{connection.quote_column_name(order_column)}"
|
152
|
+
end
|
153
|
+
|
154
|
+
# This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
|
155
|
+
def base_class
|
156
|
+
options[:base_class]
|
157
|
+
end
|
158
|
+
|
159
|
+
def subclass?
|
160
|
+
model_class != model_class.base_class
|
161
|
+
end
|
162
|
+
|
163
|
+
def attribute_names
|
164
|
+
@attribute_names ||= model_class.new.attributes.keys - model_class.protected_attributes.to_a
|
165
|
+
end
|
166
|
+
|
167
|
+
def has_type?
|
168
|
+
attribute_names.include? 'type'
|
169
|
+
end
|
170
|
+
|
171
|
+
def table_name
|
172
|
+
model_class.table_name
|
173
|
+
end
|
174
|
+
|
175
|
+
def quoted_table_name
|
176
|
+
connection.quote_table_name table_name
|
177
|
+
end
|
178
|
+
|
179
|
+
def remove_prefix_and_suffix(table_name)
|
180
|
+
prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
|
181
|
+
suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
|
182
|
+
table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
|
183
|
+
end
|
184
|
+
|
185
|
+
def ids_from(scope)
|
186
|
+
if scope.respond_to? :pluck
|
187
|
+
scope.pluck(:id)
|
188
|
+
else
|
189
|
+
scope.select(:id).collect(&:id)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
data/lib/closure_tree/version.rb
CHANGED
@@ -1,18 +1,28 @@
|
|
1
1
|
require 'with_advisory_lock'
|
2
|
+
require 'active_support/concern'
|
2
3
|
|
3
4
|
module ClosureTree
|
4
5
|
module WithAdvisoryLock
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
5
8
|
def ct_with_advisory_lock(&block)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
+
self.class.ct_with_advisory_lock(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
included do
|
13
|
+
class_eval do
|
14
|
+
def self.ct_with_advisory_lock(&block)
|
15
|
+
if _ct.options[:with_advisory_lock]
|
16
|
+
with_advisory_lock("closure_tree") do
|
17
|
+
transaction do
|
18
|
+
yield
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
9
22
|
yield
|
10
23
|
end
|
11
24
|
end
|
12
|
-
else
|
13
|
-
yield
|
14
25
|
end
|
15
26
|
end
|
16
27
|
end
|
17
28
|
end
|
18
|
-
|
data/spec/namespace_type_spec.rb
CHANGED
@@ -5,8 +5,8 @@ describe Namespace::Type do
|
|
5
5
|
context "class injection" do
|
6
6
|
it "should build hierarchy classname correctly" do
|
7
7
|
Namespace::Type.hierarchy_class.to_s.should == "Namespace::TypeHierarchy"
|
8
|
-
Namespace::Type.hierarchy_class_name.should == "Namespace::TypeHierarchy"
|
9
|
-
Namespace::Type.short_hierarchy_class_name.should == "TypeHierarchy"
|
8
|
+
Namespace::Type._ct.hierarchy_class_name.should == "Namespace::TypeHierarchy"
|
9
|
+
Namespace::Type._ct.short_hierarchy_class_name.should == "TypeHierarchy"
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
data/spec/parallel_spec.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
data/spec/tag_spec.rb
CHANGED
@@ -259,12 +259,12 @@ shared_examples_for "Tag (2)" do
|
|
259
259
|
context "class injection" do
|
260
260
|
it "should build hierarchy classname correctly" do
|
261
261
|
Tag.hierarchy_class.to_s.should == "TagHierarchy"
|
262
|
-
Tag.hierarchy_class_name.should == "TagHierarchy"
|
263
|
-
Tag.short_hierarchy_class_name.should == "TagHierarchy"
|
262
|
+
Tag._ct.hierarchy_class_name.should == "TagHierarchy"
|
263
|
+
Tag._ct.short_hierarchy_class_name.should == "TagHierarchy"
|
264
264
|
end
|
265
265
|
|
266
266
|
it "should have a correct parent column name" do
|
267
|
-
Tag.parent_column_name.should == "parent_id"
|
267
|
+
Tag._ct.parent_column_name.should == "parent_id"
|
268
268
|
end
|
269
269
|
end
|
270
270
|
|
@@ -469,7 +469,7 @@ describe "Tag with UUID" do
|
|
469
469
|
TagHierarchy.reset_column_information
|
470
470
|
|
471
471
|
# We have to reset a few other caches
|
472
|
-
Tag.
|
472
|
+
Tag._ct.options[:hierarchy_table_name] = 'tag_hierarchies_uuid'
|
473
473
|
Tag.reflections.each do |key, ref|
|
474
474
|
ref.instance_variable_set('@table_name', nil)
|
475
475
|
ref.instance_variable_set('@quoted_table_name', nil)
|
data/spec/user_spec.rb
CHANGED
@@ -116,7 +116,7 @@ describe "empty db" do
|
|
116
116
|
end
|
117
117
|
|
118
118
|
it "supports siblings" do
|
119
|
-
User.order_option.should be_nil
|
119
|
+
User._ct.order_option.should be_nil
|
120
120
|
a = User.create(:email => "a")
|
121
121
|
b1 = a.children.create(:email => "b1")
|
122
122
|
b2 = a.children.create(:email => "b2")
|
@@ -127,7 +127,7 @@ describe "empty db" do
|
|
127
127
|
|
128
128
|
context "when a user is not yet saved" do
|
129
129
|
it "supports siblings" do
|
130
|
-
User.order_option.should be_nil
|
130
|
+
User._ct.order_option.should be_nil
|
131
131
|
a = User.create(:email => "a")
|
132
132
|
b1 = a.children.new(:email => "b1")
|
133
133
|
b2 = a.children.create(:email => "b2")
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 63
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
|
-
-
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version:
|
7
|
+
- 4
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 4.0.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Matthew McEachen
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2013-
|
18
|
+
date: 2013-05-18 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -201,10 +201,10 @@ extra_rdoc_files: []
|
|
201
201
|
|
202
202
|
files:
|
203
203
|
- lib/closure_tree/acts_as_tree.rb
|
204
|
-
- lib/closure_tree/columns.rb
|
205
204
|
- lib/closure_tree/deterministic_ordering.rb
|
206
205
|
- lib/closure_tree/model.rb
|
207
206
|
- lib/closure_tree/numeric_deterministic_ordering.rb
|
207
|
+
- lib/closure_tree/support.rb
|
208
208
|
- lib/closure_tree/version.rb
|
209
209
|
- lib/closure_tree/with_advisory_lock.rb
|
210
210
|
- lib/closure_tree.rb
|
data/lib/closure_tree/columns.rb
DELETED
@@ -1,127 +0,0 @@
|
|
1
|
-
module ClosureTree
|
2
|
-
|
3
|
-
# Mixed into both classes and instances to provide easy access to the column names
|
4
|
-
module Columns
|
5
|
-
|
6
|
-
def parent_column_name
|
7
|
-
closure_tree_options[:parent_column_name]
|
8
|
-
end
|
9
|
-
|
10
|
-
def parent_column_sym
|
11
|
-
parent_column_name.to_sym
|
12
|
-
end
|
13
|
-
|
14
|
-
def has_name?
|
15
|
-
ct_class.new.attributes.include? closure_tree_options[:name_column]
|
16
|
-
end
|
17
|
-
|
18
|
-
def name_column
|
19
|
-
closure_tree_options[:name_column]
|
20
|
-
end
|
21
|
-
|
22
|
-
def name_sym
|
23
|
-
name_column.to_sym
|
24
|
-
end
|
25
|
-
|
26
|
-
def hierarchy_table_name
|
27
|
-
# We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
|
28
|
-
# because they may have overridden the table name, which is what we want to be consistent with
|
29
|
-
# in order for the schema to make sense.
|
30
|
-
tablename = closure_tree_options[:hierarchy_table_name] ||
|
31
|
-
remove_prefix_and_suffix(ct_table_name).singularize + "_hierarchies"
|
32
|
-
|
33
|
-
ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
|
34
|
-
end
|
35
|
-
|
36
|
-
def hierarchy_class_name
|
37
|
-
closure_tree_options[:hierarchy_class_name] || ct_class.to_s + "Hierarchy"
|
38
|
-
end
|
39
|
-
|
40
|
-
|
41
|
-
#
|
42
|
-
# Returns the constant name of the hierarchy_class
|
43
|
-
#
|
44
|
-
# @return [String] the constant name
|
45
|
-
#
|
46
|
-
# @example
|
47
|
-
# Namespace::Model.hierarchy_class_name # => "Namespace::ModelHierarchy"
|
48
|
-
# Namespace::Model.short_hierarchy_class_name # => "ModelHierarchy"
|
49
|
-
def short_hierarchy_class_name
|
50
|
-
hierarchy_class_name.split('::').last
|
51
|
-
end
|
52
|
-
|
53
|
-
def quoted_hierarchy_table_name
|
54
|
-
connection.quote_table_name hierarchy_table_name
|
55
|
-
end
|
56
|
-
|
57
|
-
def quoted_parent_column_name
|
58
|
-
connection.quote_column_name parent_column_name
|
59
|
-
end
|
60
|
-
|
61
|
-
def quoted_name_column
|
62
|
-
connection.quote_column_name name_column
|
63
|
-
end
|
64
|
-
|
65
|
-
def ct_quote(field)
|
66
|
-
connection.quote(field)
|
67
|
-
end
|
68
|
-
|
69
|
-
def order_option
|
70
|
-
closure_tree_options[:order]
|
71
|
-
end
|
72
|
-
|
73
|
-
def with_order_option(options)
|
74
|
-
order_option ? options.merge(:order => order_option) : options
|
75
|
-
end
|
76
|
-
|
77
|
-
def scope_with_order(scope)
|
78
|
-
order_option ? scope.order(order_option) : scope
|
79
|
-
end
|
80
|
-
|
81
|
-
def append_order(order_by)
|
82
|
-
order_option ? "#{order_by}, #{order_option}" : order_by
|
83
|
-
end
|
84
|
-
|
85
|
-
def order_is_numeric
|
86
|
-
# The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
|
87
|
-
return false if order_option.nil? || !self.table_exists?
|
88
|
-
c = ct_class.columns_hash[order_option]
|
89
|
-
c && c.type == :integer
|
90
|
-
end
|
91
|
-
|
92
|
-
def ct_class
|
93
|
-
(self.is_a?(Class) ? self : self.class)
|
94
|
-
end
|
95
|
-
|
96
|
-
# This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
|
97
|
-
def ct_base_class
|
98
|
-
ct_class.closure_tree_options[:ct_base_class]
|
99
|
-
end
|
100
|
-
|
101
|
-
def ct_subclass?
|
102
|
-
ct_class != ct_class.base_class
|
103
|
-
end
|
104
|
-
|
105
|
-
def ct_attribute_names
|
106
|
-
@ct_attr_names ||= ct_class.new.attributes.keys - ct_class.protected_attributes.to_a
|
107
|
-
end
|
108
|
-
|
109
|
-
def ct_has_type?
|
110
|
-
ct_attribute_names.include? 'type'
|
111
|
-
end
|
112
|
-
|
113
|
-
def ct_table_name
|
114
|
-
ct_class.table_name
|
115
|
-
end
|
116
|
-
|
117
|
-
def quoted_table_name
|
118
|
-
connection.quote_table_name ct_table_name
|
119
|
-
end
|
120
|
-
|
121
|
-
def remove_prefix_and_suffix(table_name)
|
122
|
-
prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
|
123
|
-
suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
|
124
|
-
table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|