closure_tree 4.0.1 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Example tree](https://raw.github.com/mceachen/closure_tree/master/img/example.png)
|
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
|
+
![Preordered test tree](https://raw.github.com/mceachen/closure_tree/master/img/preorder.png)
|
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
|