closure_tree 3.10.2 → 4.0.0
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/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
|