closure_tree 4.0.1 → 4.1.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 +30 -4
- data/Rakefile +5 -1
- data/lib/closure_tree/acts_as_tree.rb +1 -1
- data/lib/closure_tree/model.rb +63 -33
- data/lib/closure_tree/numeric_deterministic_ordering.rb +2 -2
- data/lib/closure_tree/support.rb +52 -16
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/schema.rb +4 -4
- data/spec/label_spec.rb +3 -3
- data/spec/parallel_spec.rb +12 -6
- data/spec/spec_helper.rb +27 -6
- data/spec/support/models.rb +27 -5
- data/spec/tag_examples.rb +421 -0
- data/spec/tag_fixture_examples.rb +136 -0
- data/spec/tag_spec.rb +4 -480
- data/spec/user_spec.rb +8 -8
- data/spec/uuid_tag_spec.rb +6 -0
- metadata +173 -172
- checksums.yaml +0 -15
- data/spec/hash_tree_spec.rb +0 -91
data/README.md
CHANGED
@@ -23,12 +23,14 @@ closure_tree has some great features:
|
|
23
23
|
* __Best-in-class mutation performance__:
|
24
24
|
* 2 SQL INSERTs on node creation
|
25
25
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
26
|
+
* __Support for Rails 3.0, 3.1, 3.2, and 4.0.0.rc1__
|
26
27
|
* Support for reparenting children (and all their progeny)
|
27
28
|
* Support for [concurrency](#concurrency) (using [with_advisory_lock](https://github/mceachen/with_advisory_lock))
|
28
29
|
* Support for polymorphism [STI](#sti) within the hierarchy
|
29
30
|
* ```find_or_create_by_path``` for [building out hierarchies quickly and conveniently](#find_or_create_by_path)
|
30
31
|
* Support for [deterministic ordering](#deterministic-ordering) of children
|
31
32
|
* Support for [preordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order) traversal of descendants
|
33
|
+
* Support for rendering trees in [DOT format](http://en.wikipedia.org/wiki/DOT_(graph_description_language)), using [Graphviz](http://www.graphviz.org/)
|
32
34
|
* Excellent [test coverage](#testing) in a variety of environments
|
33
35
|
|
34
36
|
See [Bill Karwin](http://karwin.blogspot.com/)'s excellent
|
@@ -210,7 +212,25 @@ server may not be happy trying to do this.
|
|
210
212
|
|
211
213
|
HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/mceachen/closure_tree/issues/11)
|
212
214
|
|
213
|
-
###
|
215
|
+
### Graph visualization
|
216
|
+
|
217
|
+
```to_dot_digraph``` is suitable for passing into [Graphviz](http://www.graphviz.org/).
|
218
|
+
|
219
|
+
For example, for the above tree, write out the DOT file with ruby:
|
220
|
+
```ruby
|
221
|
+
File.open("example.dot", "w") { |f| f.write(Tag.root.to_dot_digraph) }
|
222
|
+
```
|
223
|
+
Then, in a shell, ```dot -Tpng example.dot > example.png```, which produces:
|
224
|
+
|
225
|
+

|
226
|
+
|
227
|
+
If you want to customize the label value, override the ```#to_digraph_label``` instance method in your model.
|
228
|
+
|
229
|
+
Just for kicks, this is the test tree I used for proving that preordered tree traversal was correct:
|
230
|
+
|
231
|
+

|
232
|
+
|
233
|
+
### Available options
|
214
234
|
|
215
235
|
When you include ```acts_as_tree``` in your model, you can provide a hash to override the following defaults:
|
216
236
|
|
@@ -433,10 +453,10 @@ rather than using fixtures? [Lots of people have written about this already](htt
|
|
433
453
|
|
434
454
|
## Testing
|
435
455
|
|
436
|
-
Closure tree is [tested under every combination](http://travis-ci.org/#!/mceachen/closure_tree) of
|
456
|
+
Closure tree is [tested under every valid combination](http://travis-ci.org/#!/mceachen/closure_tree) of
|
437
457
|
|
438
|
-
* Ruby 1.8.7
|
439
|
-
* The latest Rails 3.0, 3.1,
|
458
|
+
* Ruby 1.8.7, Ruby 1.9.3, and Ruby 2.0.0
|
459
|
+
* The latest Rails 3.0, 3.1, 3.2, and 4.0 branches, and
|
440
460
|
* MySQL and PostgreSQL. SQLite works in a single-threaded environment.
|
441
461
|
|
442
462
|
Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
|
@@ -445,8 +465,14 @@ run the test matrix locally.
|
|
445
465
|
Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
|
446
466
|
[known issue](https://github.com/rails/rails/issues/7538).
|
447
467
|
|
468
|
+
|
448
469
|
## Change log
|
449
470
|
|
471
|
+
### 4.1.0
|
472
|
+
|
473
|
+
* Added support for Rails 4.0.0.rc1 and Ruby 2.0.0 (while maintaining backward compatibility with Rails 3, BOOYA)
|
474
|
+
* Added ```#to_dot_digraph```, suitable for Graphviz rendering
|
475
|
+
|
450
476
|
### 4.0.1
|
451
477
|
|
452
478
|
* Numeric, deterministically ordered siblings will always be [0..#{self_and_siblings.count}]
|
data/Rakefile
CHANGED
@@ -16,10 +16,14 @@ RSpec::Core::RakeTask.new(:spec)
|
|
16
16
|
|
17
17
|
task :default => :spec
|
18
18
|
|
19
|
-
task :
|
19
|
+
task :all_spec_flavors do
|
20
20
|
[["", ""], ["db_prefix_", ""], ["", "_db_suffix"], ["abc_", "_123"]].each do |prefix, suffix|
|
21
21
|
fail unless system("rake spec DB_PREFIX=#{prefix} DB_SUFFIX=#{suffix}")
|
22
22
|
end
|
23
|
+
require 'active_record/version'
|
24
|
+
if ActiveRecord::VERSION::MAJOR == 3
|
25
|
+
system("rake spec ATTR_ACCESSIBLE=1")
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
# Run the specs using all the different database engines:
|
@@ -18,7 +18,7 @@ module ClosureTree
|
|
18
18
|
include ClosureTree::Model
|
19
19
|
include ClosureTree::WithAdvisoryLock
|
20
20
|
|
21
|
-
if _ct.order_option
|
21
|
+
if _ct.order_option?
|
22
22
|
include ClosureTree::DeterministicOrdering
|
23
23
|
include ClosureTree::DeterministicNumericOrdering if _ct.order_is_numeric?
|
24
24
|
end
|
data/lib/closure_tree/model.rb
CHANGED
@@ -5,50 +5,50 @@ module ClosureTree
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
validate :
|
9
|
-
before_save :
|
10
|
-
after_save :
|
11
|
-
before_destroy :
|
8
|
+
validate :_ct_validate
|
9
|
+
before_save :_ct_before_save
|
10
|
+
after_save :_ct_after_save
|
11
|
+
before_destroy :_ct_before_destroy
|
12
12
|
|
13
13
|
belongs_to :parent,
|
14
14
|
:class_name => _ct.model_class.to_s,
|
15
15
|
:foreign_key => _ct.parent_column_name
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
attr_accessible :parent if _ct.use_attr_accessible?
|
18
|
+
|
19
|
+
order_by_generations = "#{_ct.quoted_hierarchy_table_name}.generations asc"
|
20
20
|
|
21
|
-
has_many :children, _ct.
|
21
|
+
has_many :children, *_ct.has_many_with_order_option(
|
22
22
|
:class_name => _ct.model_class.to_s,
|
23
23
|
:foreign_key => _ct.parent_column_name,
|
24
24
|
:dependent => _ct.options[:dependent])
|
25
25
|
|
26
|
-
has_many :ancestor_hierarchies,
|
26
|
+
has_many :ancestor_hierarchies, *_ct.has_many_without_order_option(
|
27
27
|
:class_name => _ct.hierarchy_class_name,
|
28
28
|
:foreign_key => "descendant_id",
|
29
|
-
:order =>
|
29
|
+
:order => order_by_generations)
|
30
30
|
|
31
|
-
has_many :self_and_ancestors,
|
31
|
+
has_many :self_and_ancestors, *_ct.has_many_without_order_option(
|
32
32
|
:through => :ancestor_hierarchies,
|
33
33
|
:source => :ancestor,
|
34
|
-
:order =>
|
34
|
+
:order => order_by_generations)
|
35
35
|
|
36
|
-
has_many :descendant_hierarchies,
|
36
|
+
has_many :descendant_hierarchies, *_ct.has_many_without_order_option(
|
37
37
|
:class_name => _ct.hierarchy_class_name,
|
38
38
|
:foreign_key => "ancestor_id",
|
39
|
-
:order =>
|
39
|
+
:order => order_by_generations)
|
40
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, _ct.
|
44
|
+
has_many :self_and_descendants, *_ct.has_many_with_order_option(
|
45
45
|
:through => :descendant_hierarchies,
|
46
46
|
:source => :descendant,
|
47
|
-
:order =>
|
47
|
+
:order => order_by_generations)
|
48
48
|
|
49
49
|
scope :without, lambda { |instance|
|
50
50
|
if instance.new_record?
|
51
|
-
|
51
|
+
all
|
52
52
|
else
|
53
53
|
where(["#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} != ?", instance.id])
|
54
54
|
end
|
@@ -62,7 +62,7 @@ module ClosureTree
|
|
62
62
|
|
63
63
|
# Returns true if this node has no parents.
|
64
64
|
def root?
|
65
|
-
|
65
|
+
_ct_parent_id.nil?
|
66
66
|
end
|
67
67
|
|
68
68
|
# Returns true if this node has a parent, and is not a root.
|
@@ -118,7 +118,7 @@ module ClosureTree
|
|
118
118
|
end
|
119
119
|
|
120
120
|
def self_and_siblings
|
121
|
-
_ct.scope_with_order(_ct.base_class.where(_ct.parent_column_sym =>
|
121
|
+
_ct.scope_with_order(_ct.base_class.where(_ct.parent_column_sym => _ct_parent_id))
|
122
122
|
end
|
123
123
|
|
124
124
|
def siblings
|
@@ -190,11 +190,20 @@ module ClosureTree
|
|
190
190
|
self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
|
191
191
|
end
|
192
192
|
|
193
|
-
|
193
|
+
# override this method in your model class if you want a different digraph label.
|
194
|
+
def to_digraph_label
|
195
|
+
_ct.has_name? ? read_attribute(_ct.name_column) : to_s
|
196
|
+
end
|
197
|
+
|
198
|
+
def _ct_parent_id
|
194
199
|
read_attribute(_ct.parent_column_sym)
|
195
200
|
end
|
196
201
|
|
197
|
-
def
|
202
|
+
def _ct_id
|
203
|
+
read_attribute(_ct.model_class.primary_key)
|
204
|
+
end
|
205
|
+
|
206
|
+
def _ct_validate
|
198
207
|
if changes[_ct.parent_column_name] &&
|
199
208
|
parent.present? &&
|
200
209
|
parent.self_and_ancestors.include?(self)
|
@@ -202,12 +211,12 @@ module ClosureTree
|
|
202
211
|
end
|
203
212
|
end
|
204
213
|
|
205
|
-
def
|
214
|
+
def _ct_before_save
|
206
215
|
@was_new_record = new_record?
|
207
216
|
true # don't cancel the save
|
208
217
|
end
|
209
218
|
|
210
|
-
def
|
219
|
+
def _ct_after_save
|
211
220
|
rebuild! if changes[_ct.parent_column_name] || @was_new_record
|
212
221
|
@was_new_record = false # we aren't new anymore.
|
213
222
|
true # don't cancel anything.
|
@@ -221,17 +230,17 @@ module ClosureTree
|
|
221
230
|
sql = <<-SQL
|
222
231
|
INSERT INTO #{_ct.quoted_hierarchy_table_name}
|
223
232
|
(ancestor_id, descendant_id, generations)
|
224
|
-
SELECT x.ancestor_id, #{_ct.quote(
|
233
|
+
SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1
|
225
234
|
FROM #{_ct.quoted_hierarchy_table_name} x
|
226
|
-
WHERE x.descendant_id = #{_ct.quote(
|
235
|
+
WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)}
|
227
236
|
SQL
|
228
|
-
connection.execute sql.strip
|
237
|
+
_ct.connection.execute sql.strip
|
229
238
|
end
|
230
239
|
children.each { |c| c.rebuild! }
|
231
240
|
end
|
232
241
|
end
|
233
242
|
|
234
|
-
def
|
243
|
+
def _ct_before_destroy
|
235
244
|
delete_hierarchy_references
|
236
245
|
if _ct.options[:dependent] == :nullify
|
237
246
|
children.each { |c| c.rebuild! }
|
@@ -243,7 +252,7 @@ module ClosureTree
|
|
243
252
|
# It shouldn't affect performance of postgresql.
|
244
253
|
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
245
254
|
# Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
|
246
|
-
connection.execute <<-SQL
|
255
|
+
_ct.connection.execute <<-SQL
|
247
256
|
DELETE FROM #{_ct.quoted_hierarchy_table_name}
|
248
257
|
WHERE descendant_id IN (
|
249
258
|
SELECT DISTINCT descendant_id
|
@@ -259,6 +268,10 @@ module ClosureTree
|
|
259
268
|
scope.without(self)
|
260
269
|
end
|
261
270
|
|
271
|
+
def to_dot_digraph
|
272
|
+
self.class.to_dot_digraph(self_and_descendants)
|
273
|
+
end
|
274
|
+
|
262
275
|
module ClassMethods
|
263
276
|
def roots
|
264
277
|
_ct.scope_with_order(where(_ct.parent_column_name => nil))
|
@@ -284,7 +297,7 @@ module ClosureTree
|
|
284
297
|
HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
|
285
298
|
) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
|
286
299
|
SQL
|
287
|
-
_ct.scope_with_order(s)
|
300
|
+
_ct.scope_with_order(s.readonly(false))
|
288
301
|
end
|
289
302
|
|
290
303
|
# Rebuilds the hierarchy table based on the parent_id column in the database.
|
@@ -325,11 +338,12 @@ module ClosureTree
|
|
325
338
|
|
326
339
|
def ct_scoped_to_path(path, parent_constraint)
|
327
340
|
path = path.is_a?(Enumerable) ? path.dup : [path]
|
328
|
-
scope =
|
341
|
+
scope = where(_ct.name_sym => path.last).readonly(false)
|
329
342
|
path[0..-2].reverse.each_with_index do |ea, idx|
|
330
343
|
subtable = idx == 0 ? _ct.quoted_table_name : "p#{idx - 1}"
|
331
344
|
scope = scope.joins(<<-SQL)
|
332
|
-
INNER JOIN #{_ct.quoted_table_name} AS p#{idx}
|
345
|
+
INNER JOIN #{_ct.quoted_table_name} AS p#{idx}
|
346
|
+
ON p#{idx}.#{_ct.quoted_id_column_name} = #{subtable}.#{_ct.parent_column_name}
|
333
347
|
SQL
|
334
348
|
scope = scope.where("p#{idx}.#{_ct.quoted_name_column} = #{_ct.quote(ea)}")
|
335
349
|
end
|
@@ -355,12 +369,13 @@ module ClosureTree
|
|
355
369
|
def hash_tree_scope(limit_depth = nil)
|
356
370
|
# Deepest generation, within limit, for each descendant
|
357
371
|
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
372
|
+
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
|
358
373
|
generation_depth = <<-SQL
|
359
374
|
INNER JOIN (
|
360
375
|
SELECT descendant_id, MAX(generations) as depth
|
361
376
|
FROM #{_ct.quoted_hierarchy_table_name}
|
362
377
|
GROUP BY descendant_id
|
363
|
-
#{
|
378
|
+
#{having_clause}
|
364
379
|
) AS generation_depth
|
365
380
|
ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
|
366
381
|
SQL
|
@@ -377,11 +392,26 @@ module ClosureTree
|
|
377
392
|
if ea.root? || tree.empty? # We're at the top of the tree.
|
378
393
|
tree[ea] = h
|
379
394
|
else
|
380
|
-
id_to_hash[ea.
|
395
|
+
id_to_hash[ea._ct_parent_id][ea] = h
|
381
396
|
end
|
382
397
|
end
|
383
398
|
tree
|
384
399
|
end
|
400
|
+
|
401
|
+
# Renders the given scope as a DOT digraph, suitable for rendering by Graphviz
|
402
|
+
def to_dot_digraph(tree_scope)
|
403
|
+
id_to_instance = tree_scope.inject({}) { |h, ea| h[ea.id] = ea; h }
|
404
|
+
output = StringIO.new
|
405
|
+
output << "digraph G {\n"
|
406
|
+
tree_scope.each do |ea|
|
407
|
+
if id_to_instance.has_key? ea._ct_parent_id
|
408
|
+
output << " #{ea._ct_parent_id} -> #{ea._ct_id}\n"
|
409
|
+
end
|
410
|
+
output << " #{ea._ct_id} [label=\"#{ea.to_digraph_label}\"]\n"
|
411
|
+
end
|
412
|
+
output << "}\n"
|
413
|
+
output.string
|
414
|
+
end
|
385
415
|
end
|
386
416
|
end
|
387
417
|
end
|
@@ -5,7 +5,7 @@ module ClosureTree
|
|
5
5
|
|
6
6
|
def self_and_descendants_preordered
|
7
7
|
# TODO: raise NotImplementedError if sort_order is not numeric and not null?
|
8
|
-
h = connection.select_one(<<-SQL)
|
8
|
+
h = _ct.connection.select_one(<<-SQL)
|
9
9
|
SELECT
|
10
10
|
count(*) as total_descendants,
|
11
11
|
max(generations) as max_depth
|
@@ -28,7 +28,7 @@ module ClosureTree
|
|
28
28
|
|
29
29
|
module ClassMethods
|
30
30
|
def roots_and_descendants_preordered
|
31
|
-
h = connection.select_one(<<-SQL)
|
31
|
+
h = _ct.connection.select_one(<<-SQL)
|
32
32
|
SELECT
|
33
33
|
count(*) as total_descendants,
|
34
34
|
max(generations) as max_depth
|
data/lib/closure_tree/support.rb
CHANGED
@@ -20,12 +20,27 @@ module ClosureTree
|
|
20
20
|
model_class.connection
|
21
21
|
end
|
22
22
|
|
23
|
+
def use_attr_accessible?
|
24
|
+
ActiveRecord::VERSION::MAJOR == 3 &&
|
25
|
+
defined?(ActiveModel::MassAssignmentSecurity) &&
|
26
|
+
model_class.ancestors.include?(ActiveModel::MassAssignmentSecurity)
|
27
|
+
end
|
28
|
+
|
29
|
+
def include_forbidden_attributes_protection?
|
30
|
+
ActiveRecord::VERSION::MAJOR == 3 &&
|
31
|
+
defined?(ActiveModel::ForbiddenAttributesProtection) &&
|
32
|
+
model_class.ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
|
33
|
+
end
|
34
|
+
|
23
35
|
def hierarchy_class_for_model
|
24
36
|
hierarchy_class = model_class.parent.const_set(short_hierarchy_class_name, Class.new(ActiveRecord::Base))
|
37
|
+
use_attr_accessible = use_attr_accessible?
|
38
|
+
include_forbidden_attributes_protection = include_forbidden_attributes_protection?
|
25
39
|
hierarchy_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
40
|
+
include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
|
26
41
|
belongs_to :ancestor, :class_name => "#{model_class}"
|
27
42
|
belongs_to :descendant, :class_name => "#{model_class}"
|
28
|
-
attr_accessible :ancestor, :descendant, :generations
|
43
|
+
attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
|
29
44
|
def ==(other)
|
30
45
|
self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
|
31
46
|
end
|
@@ -87,6 +102,10 @@ module ClosureTree
|
|
87
102
|
connection.quote_table_name hierarchy_table_name
|
88
103
|
end
|
89
104
|
|
105
|
+
def quoted_id_column_name
|
106
|
+
connection.quote_column_name model_class.primary_key
|
107
|
+
end
|
108
|
+
|
90
109
|
def quoted_parent_column_name
|
91
110
|
connection.quote_column_name parent_column_name
|
92
111
|
end
|
@@ -103,38 +122,55 @@ module ClosureTree
|
|
103
122
|
!options[:order].nil?
|
104
123
|
end
|
105
124
|
|
106
|
-
def
|
107
|
-
|
125
|
+
def with_order_option(opts)
|
126
|
+
if order_option?
|
127
|
+
opts[:order] = [opts[:order], options[:order]].compact.join(",")
|
128
|
+
end
|
129
|
+
opts
|
108
130
|
end
|
109
131
|
|
110
|
-
def
|
111
|
-
order_option? ?
|
132
|
+
def scope_with_order(scope, additional_order_by = nil)
|
133
|
+
order_option? ? scope.order(*([additional_order_by, options[:order]].compact)) : scope
|
112
134
|
end
|
113
135
|
|
114
|
-
|
115
|
-
|
136
|
+
# lambda-ize the order, but don't apply the default order_option
|
137
|
+
def has_many_without_order_option(opts)
|
138
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
139
|
+
[lambda { order(opts[:order]) }, opts.except(:order)]
|
140
|
+
else
|
141
|
+
[opts]
|
142
|
+
end
|
116
143
|
end
|
117
144
|
|
118
|
-
def
|
119
|
-
if
|
120
|
-
|
145
|
+
def has_many_with_order_option(opts)
|
146
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
147
|
+
order_options = [opts[:order], options[:order]].compact
|
148
|
+
[lambda { order(order_options) }, opts.except(:order)]
|
149
|
+
else
|
150
|
+
[with_order_option(opts)]
|
121
151
|
end
|
122
|
-
options
|
123
152
|
end
|
124
153
|
|
125
154
|
def order_is_numeric?
|
126
155
|
# The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
|
127
156
|
return false if !order_option? || !model_class.table_exists?
|
128
|
-
c = model_class.columns_hash[
|
157
|
+
c = model_class.columns_hash[order_column]
|
129
158
|
c && c.type == :integer
|
130
159
|
end
|
131
160
|
|
132
161
|
def order_column
|
133
|
-
|
162
|
+
o = options[:order]
|
163
|
+
if o.nil?
|
164
|
+
nil
|
165
|
+
elsif o.is_a?(String)
|
166
|
+
o.split(' ', 2).first
|
167
|
+
else
|
168
|
+
o.to_s
|
169
|
+
end
|
134
170
|
end
|
135
171
|
|
136
172
|
def require_order_column
|
137
|
-
raise ":order value, '#{
|
173
|
+
raise ":order value, '#{options[:order]}', isn't a column" if order_column.nil?
|
138
174
|
end
|
139
175
|
|
140
176
|
def order_column_sym
|
@@ -181,9 +217,9 @@ module ClosureTree
|
|
181
217
|
|
182
218
|
def ids_from(scope)
|
183
219
|
if scope.respond_to? :pluck
|
184
|
-
scope.pluck(
|
220
|
+
scope.pluck(model_class.primary_key)
|
185
221
|
else
|
186
|
-
scope.select(
|
222
|
+
scope.select(model_class.primary_key).map { |ea| ea._ct_id }
|
187
223
|
end
|
188
224
|
end
|
189
225
|
end
|